From 8c10e49d5eaa71423c3649d1b6c05c80d3e95607 Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Thu, 28 Nov 2024 11:56:55 +1100 Subject: [PATCH] chore: converge on docker compose up rather than docker api --- .github/workflows/ci.yml | 12 +-- Justfile | 12 +++ backend/provisioner/dev_provisioner.go | 17 ++-- docker-compose.yml | 52 ------------ frontend/cli/cmd_dev.go | 3 +- frontend/cli/cmd_serve.go | 3 +- internal/buildengine/engine.go | 2 +- internal/container/container.go | 32 ++++++++ internal/dev/db.go | 100 ++++++----------------- internal/dev/docker-compose.grafana.yml | 17 ++++ internal/dev/docker-compose.mysql.yml | 16 ++++ internal/dev/docker-compose.postgres.yml | 20 +++++ internal/dev/grafana.go | 42 ++-------- internal/dev/redpanda.go | 25 +----- internal/pgproxy/pgproxy_test.go | 4 +- 15 files changed, 148 insertions(+), 209 deletions(-) create mode 100644 internal/dev/docker-compose.grafana.yml create mode 100644 internal/dev/docker-compose.mysql.yml create mode 100644 internal/dev/docker-compose.postgres.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 41d76f29f7..8669de092d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: - name: Build Cache uses: ./.github/actions/build-cache - name: Docker Compose - run: docker compose up -d --wait + run: just compose-up - name: Build Language Plugins run: just build-language-plugins - name: Test @@ -35,7 +35,7 @@ jobs: - name: Build Cache uses: ./.github/actions/build-cache - name: Docker Compose - run: docker compose up -d --wait + run: just compose-up - name: Build Language Plugins run: just build-language-plugins - name: Test README @@ -63,7 +63,7 @@ jobs: - name: Build Cache uses: ./.github/actions/build-cache - name: Docker Compose - run: docker compose up -d --wait + run: just compose-up - name: Initialise database run: just init-db - name: Vet SQL @@ -188,7 +188,7 @@ jobs: - name: Build Cache uses: ./.github/actions/build-cache - name: Docker Compose - run: docker compose up -d --wait + run: just compose-up - name: Init DB run: just init-db - name: Rebuild All @@ -332,7 +332,7 @@ jobs: - name: Build Cache uses: ./.github/actions/build-cache - name: Docker Compose - run: docker compose --profile integration up -d --wait + run: just compose-up - name: Create DB run: just init-db - name: Download Go Modules @@ -383,7 +383,7 @@ jobs: - name: Build Cache uses: ./.github/actions/build-cache - name: Docker Compose - run: docker compose --profile integration up -d --wait + run: just compose-up - name: Create DB run: just init-db - name: Download Go Modules diff --git a/Justfile b/Justfile index 5342c02a49..70c3d2377d 100644 --- a/Justfile +++ b/Justfile @@ -296,6 +296,18 @@ build-docker name: -t ftl0/ftl-{{name}}:latest \ -f Dockerfile.{{name}} . +# Run docker compose up with all docker compose files +compose-up: + #!/bin/bash + set -eo pipefail + docker_compose_files=" + -f docker-compose.yml + -f internal/dev/docker-compose.grafana.yml + -f internal/dev/docker-compose.mysql.yml + -f internal/dev/docker-compose.postgres.yml + -f internal/dev/docker-compose.redpanda.yml" + docker compose -p "ftl" $docker_compose_files up -d --wait + # Run a Just command in the Helm charts directory chart *args: @cd charts && just {{args}} diff --git a/backend/provisioner/dev_provisioner.go b/backend/provisioner/dev_provisioner.go index 78b2e1fa50..5438d8cfc0 100644 --- a/backend/provisioner/dev_provisioner.go +++ b/backend/provisioner/dev_provisioner.go @@ -44,7 +44,7 @@ func provisionMysql(mysqlPort int) InMemResourceProvisionerFn { dbName := strcase.ToLowerSnake(module) + "_" + strcase.ToLowerSnake(id) // We assume that the DB hsas already been started when running in dev mode - mysqlDSN, err := dev.SetupMySQL(ctx, "mysql:8.4.3", mysqlPort) + mysqlDSN, err := dev.SetupMySQL(ctx, mysqlPort) if err != nil { return nil, fmt.Errorf("failed to wait for mysql to be ready: %w", err) } @@ -64,7 +64,6 @@ func provisionMysql(mysqlPort int) InMemResourceProvisionerFn { } return ret, nil } - } } } @@ -84,13 +83,13 @@ func establishMySQLDB(ctx context.Context, rc *provisioner.ResourceContext, mysq if res.Next() { _, err = conn.ExecContext(ctx, "DROP DATABASE "+dbName) if err != nil { - return nil, fmt.Errorf("failed to drop database: %w", err) + return nil, fmt.Errorf("failed to drop database %q: %w", dbName, err) } } _, err = conn.ExecContext(ctx, "CREATE DATABASE "+dbName) if err != nil { - return nil, fmt.Errorf("failed to create database: %w", err) + return nil, fmt.Errorf("failed to create database %q: %w", dbName, err) } if mysql.Mysql == nil { @@ -153,10 +152,8 @@ func provisionPostgres(postgresPort int) func(ctx context.Context, rc *provision dbName := strcase.ToLowerSnake(module) + "_" + strcase.ToLowerSnake(id) // We assume that the DB has already been started when running in dev mode - postgresDSN, err := dev.WaitForPostgresReady(ctx, postgresPort) - if err != nil { - return nil, fmt.Errorf("failed to wait for postgres to be ready: %w", err) - } + postgresDSN := dsn.PostgresDSN(dbName, dsn.Port(postgresPort)) + conn, err := otelsql.Open("pgx", postgresDSN) if err != nil { return nil, fmt.Errorf("failed to connect to postgres: %w", err) @@ -180,12 +177,12 @@ func provisionPostgres(postgresPort int) func(ctx context.Context, rc *provision } _, err = conn.ExecContext(ctx, "DROP DATABASE "+dbName) if err != nil { - return nil, fmt.Errorf("failed to create database: %w", err) + return nil, fmt.Errorf("failed to drop database %q: %w", dbName, err) } } _, err = conn.ExecContext(ctx, "CREATE DATABASE "+dbName) if err != nil { - return nil, fmt.Errorf("failed to create database: %w", err) + return nil, fmt.Errorf("failed to create database %q: %w", dbName, err) } if pg.Postgres == nil { diff --git a/docker-compose.yml b/docker-compose.yml index f7774963d4..d4e53f598b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,53 +1,4 @@ services: - db: - image: postgres:15.10 - command: postgres - user: postgres - # For local debugging - # -c logging_collector=on -c log_destination=stderr -c log_directory=/logs -c log_statement=all - # volumes: - # - ./logs:/logs - restart: always - environment: - POSTGRES_PASSWORD: secret - ports: - - 15432:5432 - healthcheck: - test: ["CMD-SHELL", "pg_isready"] - interval: 1s - timeout: 60s - retries: 60 - start_period: 80s - mysql: - profiles: - - mysql - image: mysql:8.4.3 - environment: - MYSQL_ROOT_PASSWORD: secret - MYSQL_USER: mysql - MYSQL_PASSWORD: secret - MYSQL_DATABASE: ftl - ports: - - "13306:3306" - healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "--password=secret"] - interval: 1s - timeout: 60s - retries: 60 - start_period: 80s - otel-lgtm: - image: grafana/otel-lgtm - platform: linux/amd64 - ports: - - 3000:3000 # Portal Endpoint - - 9090:9090 # Prometheus - - ${OTEL_GRPC_PORT:-4317}:4317 # OTLP GRPC Collector - - ${OTEL_HTTP_PORT:-4317}:4318 # OTLP HTTP Collector - environment: - - ENABLE_LOGS_ALL=true - - GF_AUTH_ANONYMOUS_ENABLED=true - - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin - - GF_AUTH_DISABLE_LOGIN_FORM=true localstack: image: localstack/localstack profiles: @@ -61,6 +12,3 @@ services: image: registry:2 ports: - "15000:5000" - -volumes: - grafana-storage: {} diff --git a/frontend/cli/cmd_dev.go b/frontend/cli/cmd_dev.go index 3e62b8484a..f18421ad9f 100644 --- a/frontend/cli/cmd_dev.go +++ b/frontend/cli/cmd_dev.go @@ -73,11 +73,10 @@ func (d *devCmd) Run( } if d.InitDB { - dsn, err := dev.SetupPostgres(ctx, d.ServeCmd.DatabaseImage, d.ServeCmd.DBPort, true) + err := dev.SetupPostgres(ctx, optional.Some(d.ServeCmd.DatabaseImage), d.ServeCmd.DBPort, true) if err != nil { return fmt.Errorf("failed to setup database: %w", err) } - fmt.Println(dsn) return nil } statusManager := terminal.FromContext(ctx) diff --git a/frontend/cli/cmd_serve.go b/frontend/cli/cmd_serve.go index c170753ecf..d8eeee93e1 100644 --- a/frontend/cli/cmd_serve.go +++ b/frontend/cli/cmd_serve.go @@ -163,7 +163,8 @@ func (s *serveCommonConfig) run( registry.AllowInsecure = true registry.Registry = fmt.Sprintf("127.0.0.1:%d/ftl", s.RegistryPort) // Bring up the DB and DAL. - dsn, err := dev.SetupPostgres(ctx, s.DatabaseImage, s.DBPort, recreate) + dsn := dev.PostgresDSN(ctx, s.DBPort) + err = dev.SetupPostgres(ctx, optional.Some(s.DatabaseImage), s.DBPort, recreate) if err != nil { return err } diff --git a/internal/buildengine/engine.go b/internal/buildengine/engine.go index bfc1f8d5dd..a25e5d6424 100644 --- a/internal/buildengine/engine.go +++ b/internal/buildengine/engine.go @@ -519,7 +519,7 @@ func (e *Engine) watchForModuleChanges(ctx context.Context, period time.Duration // Build and deploy all modules first. err = e.BuildAndDeploy(ctx, 1, true) if err != nil { - logger.Errorf(err, "initial deploy failed") + logger.Errorf(err, "Initial deploy failed") } moduleHashes := map[string][]byte{} diff --git a/internal/container/container.go b/internal/container/container.go index 44b02e86e7..9e46a0959b 100644 --- a/internal/container/container.go +++ b/internal/container/container.go @@ -1,10 +1,12 @@ package container import ( + "bytes" "context" "fmt" "io" "os" + "path/filepath" "strconv" "time" @@ -18,7 +20,10 @@ import ( "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/go-connections/nat" + "github.com/TBD54566975/ftl/internal/exec" + "github.com/TBD54566975/ftl/internal/flock" "github.com/TBD54566975/ftl/internal/log" + "github.com/TBD54566975/ftl/internal/projectconfig" ) var dockerClient = once.Once(func(ctx context.Context) (*client.Client, error) { @@ -343,3 +348,30 @@ func PollContainerHealth(ctx context.Context, containerName string, timeout time } } } + +// ComposeUp runs docker-compose up with the given compose YAML. +// +// Make sure you obtain the compose yaml from a string literal or an embedded file, rather than +// reading from disk. The project file will not be included in the release build. +func ComposeUp(ctx context.Context, name, composeYAML string, envars ...string) error { + // A flock is used to provent Docker compose getting confused, which happens when we call `docker compose up` + // multiple times simultaneously for the same services. + projCfg, ok := projectconfig.DefaultConfigPath().Get() + if !ok { + return fmt.Errorf("failed to get project config path") + } + release, err := flock.Acquire(ctx, filepath.Join(filepath.Dir(projCfg), ".ftl", fmt.Sprintf(".docker.%v.lock", name)), 1*time.Minute) + if err != nil { + return fmt.Errorf("failed to acquire lock: %w", err) + } + defer release() + + envars = append(envars, "COMPOSE_IGNORE_ORPHANS=True") + + cmd := exec.CommandWithEnv(ctx, log.Debug, ".", envars, "docker", "compose", "-f", "-", "-p", "ftl", "up", "-d", "--wait") + cmd.Stdin = bytes.NewReader([]byte(composeYAML)) + if err := cmd.RunStderrError(ctx); err != nil { + return fmt.Errorf("failed to run docker compose up: %w", err) + } + return nil +} diff --git a/internal/dev/db.go b/internal/dev/db.go index ec642739cb..d53d283786 100644 --- a/internal/dev/db.go +++ b/internal/dev/db.go @@ -2,104 +2,50 @@ package dev import ( "context" + _ "embed" "fmt" - "net" - "time" - - "github.com/alecthomas/types/optional" + "strconv" "github.com/TBD54566975/ftl/backend/controller/sql/databasetesting" "github.com/TBD54566975/ftl/internal/container" "github.com/TBD54566975/ftl/internal/dsn" "github.com/TBD54566975/ftl/internal/log" + "github.com/alecthomas/types/optional" ) -const postgresContainerName = "ftl-db-1" -const mysqlContainerName = "ftl-mysql-1" +//go:embed docker-compose.mysql.yml +var mysqlDockerCompose string -func SetupPostgres(ctx context.Context, image string, port int, recreate bool) (string, error) { - dsn, err := SetupDatabase(ctx, image, port, postgresContainerName, 5432, WaitForPostgresReady, container.RunPostgres) - if err != nil { - return "", fmt.Errorf("failed to create database: %w", err) - } - _, err = databasetesting.CreateForDevel(ctx, dsn, recreate) - if err != nil { - return "", fmt.Errorf("failed to create database: %w", err) - } - return dsn, nil -} +//go:embed docker-compose.postgres.yml +var postgresDockerCompose string -func SetupMySQL(ctx context.Context, image string, port int) (string, error) { - return SetupDatabase(ctx, image, port, mysqlContainerName, 3306, WaitForMySQLReady, container.RunMySQL) +func PostgresDSN(ctx context.Context, port int) string { + return dsn.PostgresDSN("ftl", dsn.Port(port)) } -func SetupDatabase(ctx context.Context, image string, port int, containerName string, containerPort int, waitForReady func(ctx context.Context, port int) (string, error), runContainer func(ctx context.Context, name string, port int, image string) error) (string, error) { - logger := log.FromContext(ctx) - - exists, err := container.DoesExist(ctx, containerName, optional.Some(image)) - if err != nil { - return "", fmt.Errorf("failed to check if container exists: %w", err) - } - - if !exists { - logger.Debugf("Creating docker container '%s' for db", containerName) - - // check if port s.DBPort is already in use - if l, err := net.Listen("tcp", fmt.Sprintf(":%d", port)); err != nil { - return "", fmt.Errorf("port %d is already in use", port) - } else if err = l.Close(); err != nil { - return "", fmt.Errorf("failed to close listener: %w", err) - } - - err = runContainer(ctx, containerName, port, image) - if err != nil { - return "", fmt.Errorf("failed to run db container: %w", err) - } - - } else { - // Start the existing container - err = container.Start(ctx, containerName) - if err != nil { - return "", fmt.Errorf("failed to start existing db container: %w", err) - } - - // Grab the port from the existing container - port, err = container.GetContainerPort(ctx, containerName, containerPort) - if err != nil { - return "", fmt.Errorf("failed to get port from existing db container: %w", err) - } - - logger.Debugf("Reusing existing docker container %s on port %d for db", containerName, port) +func SetupPostgres(ctx context.Context, image optional.Option[string], port int, recreate bool) error { + envars := []string{"POSTGRES_PORT=" + strconv.Itoa(port)} + if i, ok := image.Get(); ok { + envars = append(envars, "POSTGRES_IMAGE="+i) } - - dsn, err := waitForReady(ctx, port) + err := container.ComposeUp(ctx, "postgres", postgresDockerCompose, envars...) if err != nil { - return "", fmt.Errorf("db container failed to be healthy: %w", err) + return fmt.Errorf("could not start postgres: %w", err) } - - return dsn, nil -} - -func WaitForPostgresReady(ctx context.Context, port int) (string, error) { - logger := log.FromContext(ctx) - err := container.PollContainerHealth(ctx, postgresContainerName, 10*time.Minute) + dsn := PostgresDSN(ctx, port) + _, err = databasetesting.CreateForDevel(ctx, dsn, recreate) if err != nil { - return "", fmt.Errorf("db container failed to be healthy: %w", err) + return fmt.Errorf("failed to create database: %w", err) } - - dsn := dsn.PostgresDSN("ftl", dsn.Port(port)) - logger.Debugf("Postgres DSN: %s", dsn) - return dsn, nil + return nil } -func WaitForMySQLReady(ctx context.Context, port int) (string, error) { - logger := log.FromContext(ctx) - err := container.PollContainerHealth(ctx, mysqlContainerName, 10*time.Minute) +func SetupMySQL(ctx context.Context, port int) (string, error) { + err := container.ComposeUp(ctx, "mysql", mysqlDockerCompose, "MYSQL_PORT="+strconv.Itoa(port)) if err != nil { - return "", fmt.Errorf("db container failed to be healthy: %w", err) + return "", fmt.Errorf("could not start mysql: %w", err) } - dsn := dsn.MySQLDSN("ftl", dsn.Port(port)) - logger.Debugf("MySQL DSN: %s", dsn) + log.FromContext(ctx).Debugf("MySQL DSN: %s", dsn) return dsn, nil } diff --git a/internal/dev/docker-compose.grafana.yml b/internal/dev/docker-compose.grafana.yml new file mode 100644 index 0000000000..0d4009d4e9 --- /dev/null +++ b/internal/dev/docker-compose.grafana.yml @@ -0,0 +1,17 @@ +services: + otel-lgtm: + image: grafana/otel-lgtm + platform: linux/amd64 + ports: + - 3000:3000 # Portal Endpoint + - 9090:9090 # Prometheus + - ${OTEL_GRPC_PORT:-4317}:4317 # OTLP GRPC Collector + - ${OTEL_HTTP_PORT:-4318}:4318 # OTLP HTTP Collector + environment: + - ENABLE_LOGS_ALL=true + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin + - GF_AUTH_DISABLE_LOGIN_FORM=true + +volumes: + grafana-storage: {} diff --git a/internal/dev/docker-compose.mysql.yml b/internal/dev/docker-compose.mysql.yml new file mode 100644 index 0000000000..b7cbc8d228 --- /dev/null +++ b/internal/dev/docker-compose.mysql.yml @@ -0,0 +1,16 @@ +services: + mysql: + image: mysql:8.4.3 + environment: + MYSQL_ROOT_PASSWORD: secret + MYSQL_USER: mysql + MYSQL_PASSWORD: secret + MYSQL_DATABASE: ftl + ports: + - "${MYSQL_PORT:-13306}:3306" + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "--password=secret"] + interval: 1s + timeout: 60s + retries: 60 + start_period: 80s diff --git a/internal/dev/docker-compose.postgres.yml b/internal/dev/docker-compose.postgres.yml new file mode 100644 index 0000000000..59b9de27be --- /dev/null +++ b/internal/dev/docker-compose.postgres.yml @@ -0,0 +1,20 @@ +services: + db: + image: ${POSTGRES_IMAGE:-postgres:15.10} + command: postgres + user: postgres + # For local debugging + # -c logging_collector=on -c log_destination=stderr -c log_directory=/logs -c log_statement=all + # volumes: + # - ./logs:/logs + restart: always + environment: + POSTGRES_PASSWORD: secret + ports: + - ${POSTGRES_PORT:-15432}:5432 + healthcheck: + test: ["CMD-SHELL", "pg_isready"] + interval: 1s + timeout: 60s + retries: 60 + start_period: 80s diff --git a/internal/dev/grafana.go b/internal/dev/grafana.go index 8a0b603f18..0d10d883f7 100644 --- a/internal/dev/grafana.go +++ b/internal/dev/grafana.go @@ -2,55 +2,23 @@ package dev import ( "context" + _ "embed" "fmt" - "net" - - "github.com/alecthomas/types/optional" "github.com/TBD54566975/ftl/internal/container" - "github.com/TBD54566975/ftl/internal/log" ) -const ftlGrafanaName = "ftl-otel-lgtm-1" +//go:embed docker-compose.grafana.yml +var grafanaDockerCompose string func SetupGrafana(ctx context.Context, image string) error { - logger := log.FromContext(ctx) - - exists, err := container.DoesExist(ctx, ftlGrafanaName, optional.Some(image)) + err := container.ComposeUp(ctx, "grafana", grafanaDockerCompose) if err != nil { - return fmt.Errorf("failed to check if container exists: %w", err) - } - - if !exists { - logger.Debugf("Creating docker container '%s' for grafana", ftlGrafanaName) - // check if port is already in use - ports := []int{3000, 4317, 4318} - for _, port := range ports { - if l, err := net.Listen("tcp", fmt.Sprintf(":%d", port)); err != nil { - return fmt.Errorf("port %d is already in use", port) - } else if err = l.Close(); err != nil { - return fmt.Errorf("failed to close listener: %w", err) - } - } - err = container.Run(ctx, image, ftlGrafanaName, map[int]int{3000: 3000, 4317: 4317, 4318: 4318}, optional.None[string](), "ENABLE_LOGS_ALL=true", "GF_PATHS_DATA=/data/grafana") - if err != nil { - return fmt.Errorf("failed to run grafana container: %w", err) - } - - } else { - // Start the existing container - err = container.Start(ctx, ftlGrafanaName) - if err != nil { - return fmt.Errorf("failed to start existing registry container: %w", err) - } - - logger.Debugf("Reusing existing docker container %s for grafana", ftlGrafanaName) + return fmt.Errorf("could not start grafana: %w", err) } - err = WaitForPortReady(ctx, 3000) if err != nil { return fmt.Errorf("registry container failed to be healthy: %w", err) } - return nil } diff --git a/internal/dev/redpanda.go b/internal/dev/redpanda.go index fc655fe57e..8fa899d1d8 100644 --- a/internal/dev/redpanda.go +++ b/internal/dev/redpanda.go @@ -1,39 +1,20 @@ package dev import ( - "bytes" "context" _ "embed" "fmt" - "sync" - "time" "github.com/TBD54566975/ftl/internal/container" - "github.com/TBD54566975/ftl/internal/exec" - "github.com/TBD54566975/ftl/internal/log" ) -const redPandaContainerName = "ftl-redpanda-1" - //go:embed docker-compose.redpanda.yml -var dockerCompose []byte -var dockerComposeLock sync.Mutex +var redpandaDockerCompose string func SetUpRedPanda(ctx context.Context) error { - // A lock is used to provent Docker compose getting confused, which happens when we bring redpanda up - // multiple times simultaneously. - dockerComposeLock.Lock() - defer dockerComposeLock.Unlock() - - cmd := exec.Command(ctx, log.Debug, ".", "docker", "compose", "-f", "-", "-p", "ftl", "up", "-d", "--wait") - cmd.Stdin = bytes.NewReader(dockerCompose) - if err := cmd.RunStderrError(ctx); err != nil { - return fmt.Errorf("failed to run docker compose up: %w", err) - } - - err := container.PollContainerHealth(ctx, redPandaContainerName, 10*time.Minute) + err := container.ComposeUp(ctx, "redpanda", redpandaDockerCompose) if err != nil { - return fmt.Errorf("redpanda container failed to be healthy: %w", err) + return fmt.Errorf("could not start redpanda: %w", err) } return nil } diff --git a/internal/pgproxy/pgproxy_test.go b/internal/pgproxy/pgproxy_test.go index 5d73f0d7f3..d097c3ce4c 100644 --- a/internal/pgproxy/pgproxy_test.go +++ b/internal/pgproxy/pgproxy_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/alecthomas/assert/v2" + "github.com/alecthomas/types/optional" "github.com/jackc/pgx/v5/pgproto3" "github.com/TBD54566975/ftl/internal/dev" @@ -17,7 +18,8 @@ func TestPgProxy(t *testing.T) { ctx := log.ContextWithNewDefaultLogger(context.Background()) client, proxy := net.Pipe() - dsn, err := dev.SetupPostgres(ctx, "postgres:15.8", 0, false) + dsn := dev.PostgresDSN(ctx, 0) + err := dev.SetupPostgres(ctx, optional.None[string](), 0, false) assert.NoError(t, err) frontend := pgproto3.NewFrontend(client, client)