Skip to content

Log tail: a rotation in the same second no longer leaves the tail reading the outgoing file (#4719) - #4738

Merged
erikdarlingdata merged 4 commits into
devfrom
fix/4719-log-tail-newest-tiebreak
Sep 29, 2026
Merged

erikdarlingdata merged 4 commits into
devfrom
fix/4719-log-tail-newest-tiebreak

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

Fixes #4723. Refs #4719.

Why

pg_ls_logdir() reports modification as a stat mtime in whole seconds. When a size or age rotation puts the outgoing file's last write and the incoming file's first write in the same second, the two files tie. All six newest CTEs in PgServerLogTail.cs ordered by modification DESC alone, so LIMIT 1 could return the OUTGOING file. The tail then treated the old file as current, read nothing new, and stayed there until the new file's mtime moved to a later second. Measured in CI: the resume marker named the older file (postgresql-..._060231.log) while the awaited line was in the newer one, and 54 attempts over 30 s found 0 rows.

Two measurements on a local PostgreSQL 18.6 server on Windows (logging_collector on, log_destination = 'stderr', default log_filename):

  • Does an open log file's modification advance while it is written? Yes. 14 log writes 300 ms apart over 4.4 s, reading the current file's modification after each: 06:35:20, 20, 20, 21, 21, 21, 22, 22, 22, 23, 23, 23, 24, 24 (whole seconds, following the wall clock; the size grew 1331 to 11736 bytes). So the stall on the outgoing file lasts until the next line is logged in a later second, not until the next rotation. That decides the changelog wording below.
  • A same-second rotation really ties, and the old order really picks the outgoing file. 20 real rotations (write a line, pg_rotate_logfile(), write a line, all inside one second): 20 of 20 tied on modification; ORDER BY modification DESC LIMIT 1 returned the OUTGOING file 20 of 20; ORDER BY modification DESC, name DESC returned the new file 20 of 20. (A one-off probe; not committed, because a committed version would depend on landing inside one second.)

What changes

  • PerformanceMonitor.Collectors/PgServerLogTail.cs: all six newest CTEs (TailCteSql, TailCteBinarySql, TailCsvCteSql, TailCsvCteBinarySql, TailJsonCteSql, TailJsonCteBinarySql) now ORDER BY modification DESC, name DESC. The name is only a tiebreak behind modification, never the primary order. The existing Target log tail can pick the csvlog/jsonlog sibling instead of the stderr log #3997 paragraph says why, that it is safe where log_filename sorts chronologically (default postgresql-%Y-%m-%d_%H%M%S, RDS postgresql.log.YYYY-MM-DD-HH), and that where a pattern doesn't sort by time a tie was already a coin flip, so nothing gets worse. StoreLogSweep already breaks its listing's ties on name (ORDER BY l.modification, l.name).
  • Tests that pinned the old text pin the new: the six normalized-SQL baselines in TsqlConventionGuardTests, the byte-for-byte pins in PgLogEventsPipelineTests and PgServerLogTailBinaryRouteTests, and the newest-file query in PgLogEventsLivePostgresTests. PgDeadlocksResumeLiveTests already had the tiebreak.
  • New PgServerLogTailNewestTiebreakTests (text pin) and PgServerLogTailNewestTiebreakLivePostgresTests (live pin), described below.
  • Lite: nothing to change. Lite reads the same shared PerformanceMonitor.Collectors tail, and grep finds no other modification DESC pick in Lite or Lite.Tests.
  • docs/postgres-first-target-runbook.md: the manual permissions proof says it mirrors the collectors' own read, so it picks the newest file with the same modification DESC, name DESC.
  • No store migration, no other product file. RdsLogSource.cs and the rotation live tests are untouched.

Test plan

  • RED against dev's text: the new text pin (6 cases) and live pin (6 cases) failed 12 of 12. The live pin returned the older name (postgresql-2026-09-29_060231.*) when it was listed first.
  • GREEN with the change: those 12 pass.
  • Text pin: each of the six constants has exactly one newest CTE, ordered modification DESC, name DESC.
  • Live pin: for each constant, its own listing and newest CTEs run over a two-row VALUES list in place of pg_ls_logdir() (two names, the later one newer by the default pattern). Equal modification: the later name wins with the rows in either order. A strictly newer modification beats a later name in either order, so the name can't become the primary order. The two current_setting gates in listing are server-start settings, so the swapped SQL is given the values of a matching target; the shared DARLING_TEST_PG server is enough and no file is read.
  • Every class in each touched test file, the neighbouring log-tail classes (PgServerLogTail*, PgDeadlocks*, PgPlanCapture*) and the census gates (DocCommentHygiene, StartupCommandTimeoutTests, StorageCommandTimeoutTests, AlertReadFailureSurfaceTests, LivePostgresCollectionHygieneTests, LiveCleanupConversionRatchetTests, McpPayloadContractCensusTests): 453 tests, 0 failed, 23 skipped (they need clusters this run didn't start). The first pass failed LivePostgresCollectionHygieneTests because the live pin sat in a class with no live-postgres collection; splitting it into its own ...LivePostgresTests class fixed that.
  • The rotation live classes (PgServerLogTailRotationLiveTests, PgDeadlocksResumeLiveTests) on a second local cluster with the logging collector on: 9 of 9 pass, 0 skipped.
  • Lite: the five classes that read the shared tail (LogTailOverlapThresholdPinTests, PgDeadlockLogParserTests, PgServerLogPathPinTests, PgPlanCaptureCollectorDefinitionTests, PgLoggingCollectorGateTests): 105 tests, 0 failed.
  • Darling.Tests and Lite.Tests build with 0 warnings and 0 errors, after merging the latest dev.
  • Full Darling.Tests suite once against a local PostgreSQL 18.6 server (run on the tree after the first dev merge): 17096 tests, 2 failed, 89 skipped, 1 not run. Neither failure is from this change, and both pass when re-run alone on a freshly created database (2 of 2):
    • StoreSelfMetricsTickRecoveryLiveTests.AFailedStoreLogCapture_... expects pg_ls_logdir() to raise on a server with no log directory; my server still had a log folder left by the earlier measurement run. That was rig state, not code.
    • DarlingCliCommandsHostCheckTests.CheckSettingsAsync_ByoStore_NothingEverStale_... returned exit code 2 in the full run and did not reproduce alone. I did not find the cause; other suites were loading the same machine.
  • After the last dev merge (which brought the rotation-test change and the Lite archive change) I rebuilt with 0 warnings and re-ran the rotation live classes, PgLogBurstAndRotationLiveTests, both new classes, TsqlConventionGuardTests and LivePostgresCollectionHygieneTests: 54 tests, 0 failed, 3 skipped (they need the csvlog and jsonlog rotation clusters). The full suite was not repeated after that merge.
  • Not built: the optional end-to-end tie test (write, rotate, write inside one second, then one tail cycle). A committed version has to land inside one second to tie, and there was no time to show it passes 20 of 20 runs; the two pins above cover the ordering deterministically, and the 20-rotation probe covers the real listing.

CHANGELOG

SECTION: Fixed
ENTRY:

…he outgoing file (#4719)

pg_ls_logdir() reports modification as a stat mtime in whole seconds. When a
size or age rotation puts the outgoing file's last write and the incoming
file's first write in the same second, the two files tie, and the six
`newest` CTEs (stderr, csvlog and jsonlog routes, each with a text-read and
a binary-read constant) ordered by modification DESC alone, so LIMIT 1 could
return the outgoing file. The tail then read nothing new until the new
file's mtime moved to a later second.

Each `newest` now orders by modification DESC, name DESC. The name is only a
tiebreak behind modification. It is safe where log_filename sorts
chronologically (the default postgresql-%Y-%m-%d_%H%M%S, RDS
postgresql.log.YYYY-MM-DD-HH); where a pattern does not sort by time a tie
was already a coin flip, so nothing gets worse. StoreLogSweep already breaks
its listing's ties on name.

Tests: a text pin over all six constants, and a live pin that runs each
constant's own listing and newest CTEs over a two-row listing standing in
for pg_ls_logdir() (equal modification, both row orders, plus a strictly
newer modification beating a later name). The normalized-SQL baselines and
the two byte-for-byte pins of the tail text carry the new order, and the
pipeline live test that picks the newest file by hand breaks ties on name too.

Refs #4719
@erikdarlingdata
erikdarlingdata marked this pull request as ready for review September 29, 2026 07:32
@erikdarlingdata
erikdarlingdata merged commit dba249f into dev Sep 29, 2026
17 of 18 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/4719-log-tail-newest-tiebreak branch September 29, 2026 07:55
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.

1 participant