Skip to content

ETag-aware conditional poll + Go 1.26 bump#76

Merged
smartinov merged 8 commits intomainfrom
feature/etag-conditional-poll-and-go-1.26-bump
Apr 29, 2026
Merged

ETag-aware conditional poll + Go 1.26 bump#76
smartinov merged 8 commits intomainfrom
feature/etag-conditional-poll-and-go-1.26-bump

Conversation

@smartinov
Copy link
Copy Markdown
Contributor

@smartinov smartinov commented Apr 29, 2026

Hey — small PR to add ETag-aware conditional polling to the loader, plus the Go bump that makes Green Tea GC reachable for downstream services.

What this is for

We've been hitting a sharp edge with --poll=true: the URL-in-body skip works great when the upstream returns a stable URL, but the moment the upstream uses signed URLs (which change every request) every poll downloads and re-applies the whole repo even when nothing has changed. ETag is the standard HTTP fix for exactly this — server tags responses with a stable revision, client sends If-None-Match, server returns 304 Not Modified if unchanged, and we skip the body entirely.

Importantly: this change is purely additive. If your upstream doesn't speak ETag, nothing changes.

How the loader behaves now

  • First poll after process start: no If-None-Match (we haven't seen an ETag yet). Same wire as today.
  • Server sends ETag on a 200: we remember it. Same response handling otherwise.
  • Subsequent poll: we send If-None-Match: <last seen>. If the server replies 304 Not Modified we skip the body read entirely. If it replies 200 with a new ETag, we update our copy and proceed.
  • Server doesn't send ETag at all, ever: `lastETag` stays empty, no `If-None-Match` is ever sent on the wire, behaviour is byte-for-byte the same as before.
  • The existing URL-in-body skip is still there and still works as a fallback for non-ETag-aware upstreams.

What's in the diff

  • `pkg/repo/repo.go` — one new field on `Repo`: `lastETag string`. Zero-value safe so the constructor doesn't change.
  • `pkg/repo/loader.go` — the `if r.poll { ... }` block in `update()` now does the conditional-request dance.
  • `pkg/repo/loader_test.go` — new test file covering the four cases the change introduces:
    • upstream never sends ETag, loader behaves like today
    • upstream sends ETag, second call gets `304`, body is skipped
    • upstream changes ETag, loader picks up the new one
    • first call must not include `If-None-Match`
  • `go.mod` — bumped from `1.25.4` to `1.26.0`. Two latent format-string bugs (`%q` used with `int` arguments in `responses/error.go` and `pkg/repo/loader.go`) that Go 1.26's stricter vet now refuses to compile are fixed in the same commit.
  • `.golangci.yml` — `go:` analysis version bumped to match.

Test plan

  • `go build ./...` passes
  • `go test ./... -race` passes (all existing tests plus the four new ones)
  • The new tests run with `t.Parallel()` and use `httptest.Server`, no global state.

A note on the Go bump

Some downstream services (us included) want to enable Green Tea GC via `GOEXPERIMENT=greentea`, which is a build-time experiment that Go 1.25+ understands. Bumping the toolchain here unblocks that without forcing every downstream to also fork. Happy to split the bump into a separate PR if you'd prefer.

Let me know if anything in the design rubs you wrong — happy to iterate.

Rolled-in dependency bumps

To ship a single secure, up-to-date PR rather than three serialized ones, this branch also rolls up the open Dependabot bumps. The same upgrades land here at the highest (newest) version from each:

go test ./... -race and golangci-lint run ./... (v2.11.4) both clean against the rolled-up go.mod.

PRs #69, #73, #77 will be closed once this merges.

@smartinov smartinov changed the title Add backward-compatible ETag-aware conditional poll + bump Go to 1.26.0 ETag-aware conditional poll + Go 1.26 bump Apr 29, 2026
Updates go.mod from 1.25.4 to 1.26.0. Tests pass under the new toolchain.
The bump enables Go 1.26 runtime improvements for downstream users (notably
Green Tea GC via GOEXPERIMENT=greentea at build time).

Fixes two format-string errors that Go 1.26's stricter vet now catches:
- responses/error.go: use %d for int Status/Code fields (was %q)
- pkg/repo/loader.go: use %d for http.StatusOK int argument (was %q)
When --poll=true is enabled, the loader now sends If-None-Match with the
last-seen ETag on each poll request. On 304 Not Modified the body fetch is
skipped entirely.

Behavior is fully backward compatible: if the upstream server does not send
ETag headers, lastETag stays empty, no If-None-Match header is ever sent,
and the existing URL-in-body comparison continues to work unchanged.

The two skip mechanisms (ETag and URL-in-body) coexist: ETag short-circuits
earliest (no body read), URL-in-body remains the fallback for servers
without ETag support.

Adds test coverage for: no-ETag backward compat, ETag-then-304 skip,
ETag value change handling, and absent If-None-Match on first call.
Test file picked up four lint categories that CI rejected: goconst (extract /poll and /repo paths to constants), gofmt (collapse stray double-space before nolint comment), gosec G705 (test-server response writes flagged as XSS-via-taint; nolint with rationale since r.Host is the test server's own address), and testifylint (assert.Empty replaces assert.Equal-with-empty-string for clarity). Also bump .golangci.yml's go directive from 1.24.3 to 1.26.0 so the linter analyses code under the same Go version go.mod now declares.
@smartinov smartinov force-pushed the feature/etag-conditional-poll-and-go-1.26-bump branch from 9922251 to 8b438cc Compare April 29, 2026 14:31
@smartinov smartinov merged commit 767593a into main Apr 29, 2026
2 checks passed
@smartinov smartinov deleted the feature/etag-conditional-poll-and-go-1.26-bump branch April 29, 2026 14:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants