Skip wpgx cache invalidation when DML affects zero rows#15
Open
jaxxjj wants to merge 1 commit into
Open
Conversation
The exec/execrows/execresult invalidation hooks run unconditionally: an UPDATE guarded by a WHERE that matches nothing still evicts every listed cache key. On hot write paths this evicts entries that back unrelated hot reads, forcing them to the database for no benefit. Gate the PostExec hook on the command tag: skip when RowsAffected() is zero and the statement is row-level DML. The DML-type check keeps zero-row-tag commands that do mutate state (e.g. TRUNCATE) invalidating. Queries without an invalidate annotation generate byte-identical code, and no signatures change.
Owner
bad case ^ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The wpgx
:exec/:execrows/:execresultinvalidation hooks run unconditionally. An UPDATE guarded by a WHERE clause that matches nothing (a common idempotent-write pattern, e.g.SET x = $1 WHERE id = $2 AND x <> $1) still evicts every cache key in itsinvalidatelist.When such a no-op write sits on a hot path, it repeatedly evicts entries that back unrelated hot reads — every cached lookup behind those keys goes back to the database even though nothing changed. We hit this in production: a per-inbound-message bookkeeping UPDATE kept evicting the cached binding lookup that the same message pipeline reads, making that 60s cache permanently cold.
Fix
Gate the
PostExecinvalidation hook on the command tag the generated code already receives fromWExec(and currently discards in the:execarm):TRUNCATE, DDL) invalidating as before.pgconn.CommandTagvalue directly, so no new imports are needed in files without:execresult.invalidateannotation generate byte-identical code.Validation
go test ./internal/codegen/...passes.invalidateannotations): only those files change, output is gofmt-clean, the codebase compiles, and its integration suites (real Postgres + Redis via testcontainers, exercising dcache invalidation) pass.Known semantic edge
A statement-level trigger that mutates other tables can fire even when the triggering statement matches zero rows; such side effects would no longer invalidate. Row-level triggers are unaffected (they don't fire on zero rows). This felt like the right trade-off for a caching layer keyed to the queried tables, but happy to put the gate behind an opt-in option if you'd rather keep the old default.