Read queue depths with Find, not Scan (closes #234) #237

Merged
clawbot merged 1 commits from issue-234-queue-depth-no-gorm-scan into next 2026-08-20 07:55:20 +02:00
Collaborator

Closes #234.

next fails make check on its own. Reproduced on a clean checkout of
origin/next at aba02bc with nothing applied:

    scan_guard_test.go:205:
        Error:      Should be empty, but was [internal/delivery/queue_depth.go:109:3 internal/delivery/queue_depth.go:161:3]
        Test:       TestGormScanIsNeverCalledOutsideTests
--- FAIL: TestGormScanIsNeverCalledOutsideTests (0.22s)
FAIL    sneak.berlin/go/webhooker/internal/gormlog      1.097s

The guard is right and it stays as it is. (*gorm.DB).Scan swaps
GORM's own traceRecorder in for the configured logger for the
duration of the statement, and that recorder does not implement
gorm.ParamsFilter, so the statement reaches the log with its bound
values interpolated. Both call sites move to Find.

The SQL does not change

callbacks.Query (Find) and callbacks.RowQuery (Scan, via Rows)
both start with the same BuildQuerySQL, and both call sites set
Model and Select explicitly, so the table, the column list, the
WHERE, the GROUP BY and the soft-delete clause are built by the
same code either way. Result scanning is also the same path: Scan
reaches gorm.Scan through ScanRows, Find through the query
callback, and both re-parse the anonymous destination struct's schema
because its type differs from the model's.

Captured from the adapter, same test, before and after:

Scan: SELECT `target_id`,`status`,count(*) as depth FROM `deliveries` WHERE status IN ("pending","retrying") AND `deliveries`.`deleted_at` IS NULL GROUP BY target_id, status
Find: SELECT `target_id`,`status`,count(*) as depth FROM `deliveries` WHERE status IN (?,?) AND `deliveries`.`deleted_at` IS NULL GROUP BY target_id, status

Identical apart from the placeholders, which is the whole point. Both
reported rows=2 on the same fixture.

Metrics

Unchanged, and the existing assertions still cover the properties:

  • TestDeliveryMetrics_QueueDepthGauges — per-type counts, and a
    drained bucket reading 0 rather than holding its last value. PASS.
  • TestDeliveryMetrics_QueueDepthDeletedTarget — a backlog behind
    a deleted target still lands in unknown. PASS. This is the case the
    rewrite could have broken, since it depends on rows whose target_id
    resolves to no target row; the query shape is untouched, so it does
    not.
  • Folding through normalizeTargetType with summing, the full label
    domain written on every sample, and the unknown series materialised
    at registration all live in internal/metrics and are not touched
    here.

New test

TestQueueDepthSample_LogsNoBoundValue (internal/delivery) drives one
sample through the real adapter and asserts the aggregate keeps its
placeholders and carries no status literal. Negative control: restoring
either Scan fails it with

Error: "...status IN (\"pending\",\"retrying\")..." does not contain "?"
Error: "...status IN (\"pending\",\"retrying\")..." should not contain "pending"

so it is discriminating rather than vacuous, and it catches at runtime
what scan_guard_test.go catches statically.

To capture that SQL the test needs the per-webhook databases to log
through a buffer, so database.NewTestWebhookDBManagerWithLogger takes
the logger; NewTestWebhookDBManager keeps its signature and delegates.

Gate

Host load average 29.69 / 40.07 / 51.49 on 48 cores at the start of the
build.

make check on the branch: green, exit 0, 17 packages ok.

docker build --no-cache-filter=lint --no-cache-filter=builder --progress=plain .
exit 0:

#17 [lint 7/9] RUN make fmt-check                                     DONE 0.9s
#18 [lint 8/9] RUN golangci-lint config verify --config .golangci.yml DONE 1.4s
#19 [lint 9/9] RUN golangci-lint run --config .golangci.yml ./...     DONE 84.0s
#19 80.51 0 issues.
#32 [builder  9/11] RUN make test                                     DONE 90.8s
#33 [builder 10/11] RUN make build                                    DONE 60.5s

Real durations on both cache-defeated stages, golangci-lint's own
summary line present, 17 ok lines under #32 and zero of them
(cached). Inside the container:

--- PASS: TestGormScanIsNeverCalledOutsideTests (0.17s)
--- PASS: TestScanGuard_ReportsPlantedCalls (0.01s)
--- PASS: TestBoundValues_NeverReachTheLog (0.02s)
--- PASS: TestQueueDepthSample_LogsNoBoundValue (1.60s)
--- PASS: TestDeliveryMetrics_QueueDepthGauges (1.61s)
--- PASS: TestDeliveryMetrics_QueueDepthDeletedTarget (0.45s)

Neither #225 nor
#230 fired on any run here;
internal/handlers finished in 20.3s in the container. The failure
fixed by this branch was deterministic, not either of those flakes.

Rebased onto origin/next at aba02bc immediately before pushing. The
gate image was removed and no containers were left behind.

Closes https://git.eeqj.de/sneak/webhooker/issues/234. `next` fails `make check` on its own. Reproduced on a clean checkout of `origin/next` at `aba02bc` with nothing applied: ``` scan_guard_test.go:205: Error: Should be empty, but was [internal/delivery/queue_depth.go:109:3 internal/delivery/queue_depth.go:161:3] Test: TestGormScanIsNeverCalledOutsideTests --- FAIL: TestGormScanIsNeverCalledOutsideTests (0.22s) FAIL sneak.berlin/go/webhooker/internal/gormlog 1.097s ``` The guard is right and it stays as it is. `(*gorm.DB).Scan` swaps GORM's own `traceRecorder` in for the configured logger for the duration of the statement, and that recorder does not implement `gorm.ParamsFilter`, so the statement reaches the log with its bound values interpolated. Both call sites move to `Find`. ## The SQL does not change `callbacks.Query` (Find) and `callbacks.RowQuery` (Scan, via `Rows`) both start with the same `BuildQuerySQL`, and both call sites set `Model` and `Select` explicitly, so the table, the column list, the `WHERE`, the `GROUP BY` and the soft-delete clause are built by the same code either way. Result scanning is also the same path: `Scan` reaches `gorm.Scan` through `ScanRows`, `Find` through the query callback, and both re-parse the anonymous destination struct's schema because its type differs from the model's. Captured from the adapter, same test, before and after: ``` Scan: SELECT `target_id`,`status`,count(*) as depth FROM `deliveries` WHERE status IN ("pending","retrying") AND `deliveries`.`deleted_at` IS NULL GROUP BY target_id, status Find: SELECT `target_id`,`status`,count(*) as depth FROM `deliveries` WHERE status IN (?,?) AND `deliveries`.`deleted_at` IS NULL GROUP BY target_id, status ``` Identical apart from the placeholders, which is the whole point. Both reported `rows=2` on the same fixture. ## Metrics Unchanged, and the existing assertions still cover the properties: - `TestDeliveryMetrics_QueueDepthGauges` — per-type counts, and a drained bucket reading 0 rather than holding its last value. PASS. - `TestDeliveryMetrics_QueueDepthDeletedTarget` — a backlog behind a deleted target still lands in `unknown`. PASS. This is the case the rewrite could have broken, since it depends on rows whose `target_id` resolves to no target row; the query shape is untouched, so it does not. - Folding through `normalizeTargetType` with summing, the full label domain written on every sample, and the `unknown` series materialised at registration all live in `internal/metrics` and are not touched here. ## New test `TestQueueDepthSample_LogsNoBoundValue` (`internal/delivery`) drives one sample through the real adapter and asserts the aggregate keeps its placeholders and carries no status literal. Negative control: restoring either `Scan` fails it with ``` Error: "...status IN (\"pending\",\"retrying\")..." does not contain "?" Error: "...status IN (\"pending\",\"retrying\")..." should not contain "pending" ``` so it is discriminating rather than vacuous, and it catches at runtime what `scan_guard_test.go` catches statically. To capture that SQL the test needs the per-webhook databases to log through a buffer, so `database.NewTestWebhookDBManagerWithLogger` takes the logger; `NewTestWebhookDBManager` keeps its signature and delegates. ## Gate Host load average 29.69 / 40.07 / 51.49 on 48 cores at the start of the build. `make check` on the branch: green, exit 0, 17 packages ok. `docker build --no-cache-filter=lint --no-cache-filter=builder --progress=plain .` exit 0: ``` #17 [lint 7/9] RUN make fmt-check DONE 0.9s #18 [lint 8/9] RUN golangci-lint config verify --config .golangci.yml DONE 1.4s #19 [lint 9/9] RUN golangci-lint run --config .golangci.yml ./... DONE 84.0s #19 80.51 0 issues. #32 [builder 9/11] RUN make test DONE 90.8s #33 [builder 10/11] RUN make build DONE 60.5s ``` Real durations on both cache-defeated stages, golangci-lint's own summary line present, 17 `ok` lines under `#32` and zero of them `(cached)`. Inside the container: ``` --- PASS: TestGormScanIsNeverCalledOutsideTests (0.17s) --- PASS: TestScanGuard_ReportsPlantedCalls (0.01s) --- PASS: TestBoundValues_NeverReachTheLog (0.02s) --- PASS: TestQueueDepthSample_LogsNoBoundValue (1.60s) --- PASS: TestDeliveryMetrics_QueueDepthGauges (1.61s) --- PASS: TestDeliveryMetrics_QueueDepthDeletedTarget (0.45s) ``` Neither https://git.eeqj.de/sneak/webhooker/issues/225 nor https://git.eeqj.de/sneak/webhooker/issues/230 fired on any run here; `internal/handlers` finished in 20.3s in the container. The failure fixed by this branch was deterministic, not either of those flakes. Rebased onto `origin/next` at `aba02bc` immediately before pushing. The gate image was removed and no containers were left behind.
clawbot added the needs-review label 2026-08-20 07:47:13 +02:00
clawbot added 1 commit 2026-08-20 07:47:13 +02:00
Read queue depths with Find, not Scan (closes #234)
All checks were successful
check / check (push) Successful in 3m22s
354b271d35
(*gorm.DB).Scan swaps GORM's own traceRecorder in for the configured
logger for the duration of the statement, and that recorder does not
implement gorm.ParamsFilter. The statement therefore reaches the log
with its bound values interpolated, which is the one path
(*gormlog.Logger).ParamsFilter cannot reach. internal/gormlog's
scan_guard_test.go exists to keep that path out of production code;
the queue-depth sampler landed with two calls on it, so next has been
failing make check on its own.

Both call sites now use Find, which goes through the normal query
callback. The emitted SQL is otherwise unchanged -- callbacks.Query
and callbacks.RowQuery share BuildQuerySQL, and both call sites set
Model and Select explicitly, so the table, the column list and the
soft-delete clause are built identically. Only the log line differs:

  Scan: ... WHERE status IN ("pending","retrying") AND ...
  Find: ... WHERE status IN (?,?) AND ...

TestQueueDepthSample_LogsNoBoundValue drives one sample through the
adapter and asserts the aggregate keeps its placeholders and carries
no status literal. Restoring either Scan fails it as well as the
static guard.

database.NewTestWebhookDBManagerWithLogger lets that test capture the
SQL the per-webhook databases emit; NewTestWebhookDBManager keeps its
signature and delegates to it.
clawbot self-assigned this 2026-08-20 07:47:16 +02:00
Author
Collaborator

PASS — independently verified at 354b271: metrics unchanged (negative control reproduced, guard untouched and green), merges cleanly into next, gate fully green with every lint/builder step cache-defeated.

Disclosure, not attributed to this PR and not blocking it: my gate measured make test at 72.1s, over the 60s hard cap in REPO_POLICIES.md. It is pre-existing on next and load-dependent (host load average 50-57 on 48 cores during the run); this branch adds ~1.7s. Worth a separate improvement issue per the policy's own instruction, not a rework of #237.

PASS — independently verified at `354b271`: metrics unchanged (negative control reproduced, guard untouched and green), merges cleanly into `next`, gate fully green with every `lint`/`builder` step cache-defeated. Disclosure, not attributed to this PR and not blocking it: my gate measured `make test` at 72.1s, over the 60s hard cap in `REPO_POLICIES.md`. It is pre-existing on `next` and load-dependent (host load average 50-57 on 48 cores during the run); this branch adds ~1.7s. Worth a separate improvement issue per the policy's own instruction, not a rework of https://git.eeqj.de/sneak/webhooker/pulls/237.
clawbot merged commit 89b2dadd48 into next 2026-08-20 07:55:20 +02:00
clawbot deleted branch issue-234-queue-depth-no-gorm-scan 2026-08-20 07:55:21 +02:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/webhooker#237