Repository navigation
Hold the audit export behind the oldest open transaction - #595
Merged
Merged
Conversation
audit_logs.created_at is now(), the transaction start, so a transaction open longer than AUDIT_EXPORT_LAG_SECONDS committed rows behind the export cursor and they were never sent. Each batch now reads only rows dated before the oldest open transaction in pg_stat_activity, measured on the database clock. Worker connections also set server-side TCP keepalives, and a guard ties the documented run budget to _RUN_BUDGET_SECONDS.
A role without pg_read_all_stats is not shown xact_start for other roles, so an owner or superuser transaction writing audit rows could still be passed. Such a transaction is detected by its virtualxid lock and the export reads nothing until it ends. Idle runs now report real pending and a held_back status, keepalives also cover the API engine, and the lag floor is one second.
haksungjang
force-pushed
the
fix/587-audit-export-commit-time
branch
from
October 25, 2026 00:40
4bd5655 to
de8e0b3
Compare
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.
Closes #587
An audit row's
created_atisnow(), the start of its transaction rather than its commit. A transaction open longer thanAUDIT_EXPORT_LAG_SECONDStherefore committed rows dated behind a cursor that had already passed them.clock_timestamp()would still stamp them before the commit, so it does not close the gap. Committing audit rows in their own short transaction would break the rule that an audit row and its change commit or roll back together. A fixed lag needs an upper bound on transaction length, and there is none: the scan retention sweep, for one, deletes many scans with cascades in a single transaction. So each batch now readspg_stat_activity(afterpg_stat_clear_snapshot()) and takes only rows dated strictly before the oldest open client transaction. The lag floor is measured on the database clock, read in the statement before that one. Rows from transactions that commit later are then either inside that bound or visible to the next READ COMMITTED snapshot. There is no schema change.A role without
pg_read_all_statsis not shownxact_startfor other roles' sessions. In a role-separated deployment those include the owner role (the anonymisation script) and superusers. They still appear with their pid, and theirvirtualxidlock is listed inpg_locks. While any such transaction is open, the export reads nothing. This is safe by induction: the transaction was not open at the previous read, so all of its rows are dated after anything already sent. Vacuums are excluded throughpg_stat_progress_vacuum. The lag now has a floor of 1 s, because a transaction is dated slightly before it appears in the activity list. Withtrack_activitiesoff the export reads only when nothing is open, and logs a WARNING. Prepared transactions are outside the guarantee; the portal does not use them. The guides now state the guarantee and these limits instead of saying "no row is lost".When a run is held back with rows waiting, it ends with status
held_backand the realpendingcount, and logsaudit_export_held_back_by_open_transactionat WARNING. Every run logsaudit_export_run_finished. Server-sidetcp_keepalives_*are set after connect on both the worker and API engines (not as startup parameters, which some poolers reject). A refusal is logged instead of failing the connection. A unit test ties the "four minutes" in both guides (EN and KO) to_RUN_BUDGET_SECONDS.New integration tests run the issue's sequence through the task, and run the same sequence with a hidden superuser transaction read by a freshly created plain role. They also cover a committed row at the exact start instant, a vacuum hidden from the role, a stale activity copy, the export's own session, a worker clock running ahead, a long transaction with nothing waiting (no warning,
idle),track_activitiesoff, and keepalives on both engines, including a rejected setting. Each guarded line was mutated back, and in every case the matching test failed.