From f5b7d48500e235dced949b546120ad5841a94909 Mon Sep 17 00:00:00 2001 From: slayerjain Date: Fri, 4 Sep 2026 14:51:42 +0530 Subject: [PATCH 1/2] fix(compose): probe postgres over TCP so dependents aren't released early postgres' docker-entrypoint runs a temporary server reachable only over the Unix socket (listen_addresses='') once initdb has finished, so it can create the database and run docker-entrypoint-initdb.d. A pg_isready probe without -h talks to that socket, so it answers "accepting connections" while no TCP listener exists at all. compose then marks the service healthy and depends_on: service_healthy releases the dependent straight into ECONNREFUSED. Measured at ~235ms with no initdb.d scripts, and at 1.2s of false-healthy followed by a 2.7s shutdown checkpoint in the CI failure this was diagnosed from. Reproduced on postgres 13.3, 15 and 16. Only bites on a fresh volume, since a populated one skips the temporary server entirely -- which is why it surfaced as a rare flake rather than a consistent failure. Passing -h 127.0.0.1 forces a TCP probe, which tracks the real listener in every phase: the stock images ship listen_addresses = '*', and during startup or recovery pg_isready reports "rejecting connections", so the probe correctly keeps failing until the server can actually serve. Signed-off-by: slayerjain --- go-memory-load/docker-compose.yml | 16 +++++++++++++++- http-postgres/docker-compose.yml | 16 +++++++++++++++- postgres-wire-features/docker-compose.yml | 16 +++++++++++++++- proxy-stress-test/docker-compose.yml | 16 +++++++++++++++- ps-cache-postgres/docker-compose.yml | 16 +++++++++++++++- 5 files changed, 75 insertions(+), 5 deletions(-) diff --git a/go-memory-load/docker-compose.yml b/go-memory-load/docker-compose.yml index 56f1ba33..cbcdff2a 100644 --- a/go-memory-load/docker-compose.yml +++ b/go-memory-load/docker-compose.yml @@ -12,7 +12,21 @@ services: - postgres_data:/var/lib/postgresql/data - ./db/init:/docker-entrypoint-initdb.d:ro healthcheck: - test: ["CMD-SHELL", "pg_isready -U app_user -d orderdb"] + # -h 127.0.0.1 is load-bearing: it forces a TCP probe. + # + # Without it pg_isready talks to the Unix socket, and postgres' + # docker-entrypoint runs a *temporary* server reachable only over that + # socket (listen_addresses='') once initdb has finished, to create the + # database and run docker-entrypoint-initdb.d. The socket therefore + # answers "accepting connections" while no TCP listener exists at all, + # so compose marks this service healthy and depends_on: + # service_healthy releases the dependent straight into ECONNREFUSED. + # + # Short but real: ~235ms with no initdb.d scripts, and 1.2s of + # false-healthy plus a 2.7s shutdown checkpoint in the CI failure this + # was diagnosed from. Only bites on a fresh volume -- a populated one + # skips the temporary server entirely, which is why it failed rarely. + test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U app_user -d orderdb"] interval: 5s timeout: 5s retries: 20 diff --git a/http-postgres/docker-compose.yml b/http-postgres/docker-compose.yml index 7e95445b..13f01690 100644 --- a/http-postgres/docker-compose.yml +++ b/http-postgres/docker-compose.yml @@ -10,7 +10,21 @@ services: volumes: - pgdata:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] + # -h 127.0.0.1 is load-bearing: it forces a TCP probe. + # + # Without it pg_isready talks to the Unix socket, and postgres' + # docker-entrypoint runs a *temporary* server reachable only over that + # socket (listen_addresses='') once initdb has finished, to create the + # database and run docker-entrypoint-initdb.d. The socket therefore + # answers "accepting connections" while no TCP listener exists at all, + # so compose marks this service healthy and depends_on: + # service_healthy releases the dependent straight into ECONNREFUSED. + # + # Short but real: ~235ms with no initdb.d scripts, and 1.2s of + # false-healthy plus a 2.7s shutdown checkpoint in the CI failure this + # was diagnosed from. Only bites on a fresh volume -- a populated one + # skips the temporary server entirely, which is why it failed rarely. + test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U postgres"] interval: 2s timeout: 5s retries: 5 diff --git a/postgres-wire-features/docker-compose.yml b/postgres-wire-features/docker-compose.yml index 323b38ca..9ab6e4df 100644 --- a/postgres-wire-features/docker-compose.yml +++ b/postgres-wire-features/docker-compose.yml @@ -20,7 +20,21 @@ services: volumes: - pgdata:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] + # -h 127.0.0.1 is load-bearing: it forces a TCP probe. + # + # Without it pg_isready talks to the Unix socket, and postgres' + # docker-entrypoint runs a *temporary* server reachable only over that + # socket (listen_addresses='') once initdb has finished, to create the + # database and run docker-entrypoint-initdb.d. The socket therefore + # answers "accepting connections" while no TCP listener exists at all, + # so compose marks this service healthy and depends_on: + # service_healthy releases the dependent straight into ECONNREFUSED. + # + # Short but real: ~235ms with no initdb.d scripts, and 1.2s of + # false-healthy plus a 2.7s shutdown checkpoint in the CI failure this + # was diagnosed from. Only bites on a fresh volume -- a populated one + # skips the temporary server entirely, which is why it failed rarely. + test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U postgres"] interval: 2s timeout: 5s retries: 5 diff --git a/proxy-stress-test/docker-compose.yml b/proxy-stress-test/docker-compose.yml index d54d0803..d2fbdf47 100644 --- a/proxy-stress-test/docker-compose.yml +++ b/proxy-stress-test/docker-compose.yml @@ -10,7 +10,21 @@ services: volumes: - ./init.sql:/docker-entrypoint-initdb.d/01-init.sql healthcheck: - test: ["CMD-SHELL", "pg_isready -U repro -d reprodb"] + # -h 127.0.0.1 is load-bearing: it forces a TCP probe. + # + # Without it pg_isready talks to the Unix socket, and postgres' + # docker-entrypoint runs a *temporary* server reachable only over that + # socket (listen_addresses='') once initdb has finished, to create the + # database and run docker-entrypoint-initdb.d. The socket therefore + # answers "accepting connections" while no TCP listener exists at all, + # so compose marks this service healthy and depends_on: + # service_healthy releases the dependent straight into ECONNREFUSED. + # + # Short but real: ~235ms with no initdb.d scripts, and 1.2s of + # false-healthy plus a 2.7s shutdown checkpoint in the CI failure this + # was diagnosed from. Only bites on a fresh volume -- a populated one + # skips the temporary server entirely, which is why it failed rarely. + test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U repro -d reprodb"] interval: 2s timeout: 5s retries: 10 diff --git a/ps-cache-postgres/docker-compose.yml b/ps-cache-postgres/docker-compose.yml index 6a7e6d8a..a61d2590 100644 --- a/ps-cache-postgres/docker-compose.yml +++ b/ps-cache-postgres/docker-compose.yml @@ -10,7 +10,21 @@ services: volumes: - pgdata:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] + # -h 127.0.0.1 is load-bearing: it forces a TCP probe. + # + # Without it pg_isready talks to the Unix socket, and postgres' + # docker-entrypoint runs a *temporary* server reachable only over that + # socket (listen_addresses='') once initdb has finished, to create the + # database and run docker-entrypoint-initdb.d. The socket therefore + # answers "accepting connections" while no TCP listener exists at all, + # so compose marks this service healthy and depends_on: + # service_healthy releases the dependent straight into ECONNREFUSED. + # + # Short but real: ~235ms with no initdb.d scripts, and 1.2s of + # false-healthy plus a 2.7s shutdown checkpoint in the CI failure this + # was diagnosed from. Only bites on a fresh volume -- a populated one + # skips the temporary server entirely, which is why it failed rarely. + test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U postgres"] interval: 2s timeout: 5s retries: 5 From 3c005861f3526eeb0d311e54d7069d575e09fff6 Mon Sep 17 00:00:00 2001 From: slayerjain Date: Fri, 4 Sep 2026 15:30:05 +0530 Subject: [PATCH 2/2] ci(lint): restore golangci-lint, which a floating Go toolchain had broken Every one of the 39 lint matrix jobs was failing, on PRs that touch no Go at all: buildir: failed to load package goarch: could not load export data: cannot decode "internal/goarch", export data version 4 is greater than maximum supported version 2 golangci-lint exit with code 3 The workflow paired a PINNED linter (v1.63.4) with a FLOATING toolchain (go-version: 'stable', now go1.27.1). golangci-lint reads the compiler's export data, and Go 1.27 emits version 4; the x/tools vendored into v1.63.4 reads at most version 2. Nothing in the repo changed -- the last green run was 2026-08-03, and Go moved underneath it. Reproduced locally: identical error under go1.27.1, clean under go1.24.4. The fix is to stop the pair drifting: - golangci-lint v1.63.4 -> v2.13.2, whose released binary is built with go1.27 and reads export-data v4. - golangci-lint-action v3 -> v8. v8 is required for golangci-lint v2 (it rejects v1 outright), and its default binary install-mode downloads that official release instead of recompiling the linter with whatever Go the runner happens to have -- which is what made the old setup fragile. - go-version pinned to '1.27' for the lint job only. build.yml deliberately stays on 'stable', and both files now say why: building on current Go is that job's signal, while a linter must not be outrun by the compiler. Every sample's go.mod is <= 1.27, so the lint toolchain builds them all. .golangci.yml is migrated to the v2 schema. Two v1 settings have no direct v2 spelling and are preserved structurally, so the file now carries comments explaining both -- they are invisible in the YAML and a future re-run of `golangci-lint migrate` will quietly undo them: - `issues.exclude-use-default: false` survives as the ABSENCE of `linters.exclusions.presets`. Adding the presets most v2 templates ship would silently switch errcheck back off for ignored Close() errors, which is the exact thing the v1 setting existed to prevent. - gosimple and typecheck are not missing: S1xxx moved inside staticcheck in v2, and compile errors are always surfaced. Verified 255 -> 0: with the two exclusion rules stripped the suppressed classes reappear across 13 samples, and with them present every sample is clean, with no config-load errors in either arm. v2's staticcheck also runs the ST/QF checks that v1 never did, which surfaces two real findings, fixed here: - go-docker-timefreeze: QF1008, redundant embedded .Time selector. jwt.NumericDate embeds time.Time and defines no Before, so the promoted method is identical. - mux-mysql: ST1005, capitalized error string. It reaches only log.Print; the 404 body is a separate literal in controller.go, and no recorded keploy artifact contains it. All 39 matrix samples pass under golangci-lint v2.13.2 + Go 1.27.1, with GOFLAGS=-mod=readonly as CI runs them. Signed-off-by: slayerjain --- .github/workflows/build.yml | 8 +++ .github/workflows/golangci-lint.yml | 20 +++---- .golangci.yml | 82 +++++++++++++++++++---------- go-docker-timefreeze/main.go | 2 +- mux-mysql/db/database.go | 2 +- 5 files changed, 75 insertions(+), 39 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6334b521..8dfdb33e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,6 +18,14 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: + # 'stable' on purpose, and deliberately NOT the pinned version used + # by golangci-lint.yml. Building on the current Go is this job's + # whole point: it is the signal that these samples still compile + # against the latest release. The lint job pins instead, because a + # linter has to understand the compiler's export-data format and a + # floating toolchain there silently outruns it. Every sample's + # go.mod is <= the lint pin, so the lint toolchain can still build + # all of them. go-version: 'stable' - name: Build projects diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index f916a81d..f56b593f 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -70,18 +70,20 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: 'stable' + # Pinned deliberately, NOT 'stable'. golangci-lint reads the + # compiler's export data, and each Go release can bump that format: + # a floating toolchain silently outran the pinned linter and broke + # every job in this matrix with "export data version 4 is greater + # than maximum supported version 2" -- with no change to any sample. + # Bumping Go here must be a deliberate edit paired with the linter + # version below. + go-version: '1.27' cache: false - name: golangci-lint - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v8 with: - # Require: The version of golangci-lint to use. - # When `install-mode` is `binary` (default) the value can be v1.2 or v1.2.3 or `latest` to use the latest version. - # When `install-mode` is `goinstall` the value can be v1.2.3, `latest`, or the hash of a commit. - version: v1.63.4 + # Keep in step with go-version above; see the note there. + version: v2.13.2 # Optional: working directory, useful for monorepos working-directory: ${{matrix.working-directory}} - - # Optional: The mode to install golangci-lint. It can be 'binary' or 'goinstall'. - install-mode: "goinstall" diff --git a/.golangci.yml b/.golangci.yml index 1d2e6c2c..a462a02c 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,48 +1,74 @@ -# This is the configuration for golangci-lint for the restic project. +# golangci-lint configuration, v2 schema. # -# A sample config with all settings is here: -# https://github.com/golangci/golangci-lint/blob/master/.golangci.example.yml +# Migrated from the v1 schema with `golangci-lint migrate`. Two v1 settings +# have no direct v2 spelling and are preserved structurally instead -- see the +# notes on `linters.exclusions` below before editing. +version: "2" linters: - # only enable the linters listed below - disable-all: true + # v1 said `disable-all: true`; v2 spells it `default: none`. + default: none enable: # make sure all errors returned by functions are handled - errcheck - # show how code can be simplified - - gosimple - - # # make sure code is formatted - - gofmt - # examine code and report suspicious constructs, such as Printf calls whose # arguments do not align with the format string - govet - # make sure names and comments are used according to the conventions - - revive - # detect when assignments to existing variables are not used - ineffassign - # run static analysis and find errors + # make sure names and comments are used according to the conventions + - revive + + # run static analysis and find errors. + # + # Also covers what v1 listed separately as `gosimple`: the S1xxx checks + # moved inside staticcheck in v2, so gosimple is not a separate linter + # any more. v1's `typecheck` is likewise gone as a linter -- compile + # errors are always surfaced -- so neither entry is missing here. - staticcheck # find unused variables, functions, structs, types, etc. - unused - # parse and typecheck code - - typecheck + exclusions: + # DELIBERATELY NO `presets:` KEY HERE. + # + # This is how v1's `issues.exclude-use-default: false` survives the + # migration: v2 applies no exclusion presets unless you list them. Adding + # `presets: [comments, std-error-handling, common-false-positives, legacy]` + # -- which is what most v2 templates and a re-run of `golangci-lint + # migrate` will hand you -- silently switches OFF errcheck for ignored + # errors from Close() calls, among others. That was the original reason + # for the v1 setting. Leave this key absent. + generated: lax + rules: + # The `path` on each rule is REQUIRED, not decoration: v2 rejects a rule + # with fewer than 2 of (text, source, path, linters) set -- + # "at least 2 of (text, source, path[-except], linters) should be set". + # `(.+)\.go$` is the widest path that satisfies it, keeping these + # equivalent to v1's file-agnostic `issues.exclude` entries. + # + # revive: do not warn about missing comments for exported stuff + - path: (.+)\.go$ + text: exported (function|method|var|type|const) .* should have comment or be unexported + # revive: ignore constants in all caps + - path: (.+)\.go$ + text: don't use ALL_CAPS in Go names; use CamelCase + paths: + - third_party$ + - builtin$ + - examples$ -issues: - # don't use the default exclude rules, this hides (among others) ignored - # errors from Close() calls - exclude-use-default: false - - # list of things to not warn about - exclude: - # revive: do not warn about missing comments for exported stuff - - exported (function|method|var|type|const) .* should have comment or be unexported - # revive: ignore constants in all caps - - don't use ALL_CAPS in Go names; use CamelCase \ No newline at end of file +formatters: + # v1 listed gofmt under `linters`; v2 moves formatters to their own section. + enable: + - gofmt + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/go-docker-timefreeze/main.go b/go-docker-timefreeze/main.go index af4e7759..b5a5c743 100644 --- a/go-docker-timefreeze/main.go +++ b/go-docker-timefreeze/main.go @@ -69,7 +69,7 @@ func insecureExpiryOnlyMiddleware(next http.Handler) http.Handler { return } - if claims.ExpiresAt.Time.Before(time.Now()) { + if claims.ExpiresAt.Before(time.Now()) { http.Error(w, fmt.Sprintf("Token is expired. Current timestamp: %d", time.Now().Unix()), http.StatusUnauthorized) return } diff --git a/mux-mysql/db/database.go b/mux-mysql/db/database.go index 813bfcd4..3997f963 100644 --- a/mux-mysql/db/database.go +++ b/mux-mysql/db/database.go @@ -31,7 +31,7 @@ func GetWebsiteFromID(id string, db *sql.DB) (string, error) { err := row.Scan(&link) if err != nil { if err == sql.ErrNoRows { - return "", fmt.Errorf("Website not found") + return "", fmt.Errorf("website not found") } return "", err }