Repository navigation
RDS PostgreSQL log reads: finish rotated files in one cycle, keep positions across a restart, report skipped and missing files (#4708) - #4713
Merged
Conversation
…, then read the new file from its first line (#4708) RdsLogSource opened only the newest log file, so whatever the previous file received after the last read was never read, and a new stderr file was read from its last 10,000 lines instead of its start. When the held position names a file that is no longer the newest, the read now finishes that file from its marker first, and the position moves to the start of the newest file only when the old file's remainder has been stored. Files that rotated in between are counted on the chunk, and a held file RDS no longer lists is reported as missing.
…ated file and the newest in one cycle, report skipped and missing files (#4708) The RDS log source's positions lived only in memory, so a restart re-read just the last 10,000 lines of the newest file. Each ingestor now saves its position to collect.collector_state after the chunk's rows are stored (rds|1|instance|marker|file under the log_resume and log_resume_csv keys the self-hosted tail already uses) and loads it once per server before the first read. A saved file RDS no longer lists is reported as a missing resume file and the newest file's tail is read; a value that is not an RDS position is ignored. A rotated file is finished and the newest file opened from its first line in the same cycle, at most four reads per cycle. The outcome carries the files a rotation skipped and a missing resume file to collection_log as log_files_skipped_by_rotation and log_resume_file_missing, with RDS-specific note sentences, and the stale in-memory-by-design comments are corrected.
erikdarlingdata
marked this pull request as ready for review
September 29, 2026 06:31
erikdarlingdata
marked this pull request as draft
September 29, 2026 06:38
…ile and a saved position's file are found past the first page (#4708) DescribeDBLogFiles answers in pages once an instance holds more log files than one page carries (about 144 for three days of hourly stderr and csv files). The read asked for one page, so the newest file, or the file a saved position holds, could be missing from the list it chose from. ListLogFilesAsync now calls DescribeDBLogFiles again with the response's Marker (same instance and name filter each time) until a page carries none, and gathers every page into one list. The stderr/csv filter, the newest file, the held file and the stale-csv check all work on that whole list. A page with no files but a Marker is followed, and a Marker equal to the one the request carried ends the loop. The loop stops at 20 pages (MaxLogListingPages), goes on with the files it has, and logs one Warning naming the instance and the cap, at most once an hour for each instance. The hour is kept for each logger, because the three ingestors each own a source and share one logger. RdsLogSource takes an optional logger, and the three ingestors pass theirs. Eight new RdsLogSourceTests: the newest file on page 2, a restored position whose file is on page 2, the newest csv on page 2, the stale-csv check across pages, an empty page with a Marker, a repeated Marker, the page cap and its Warning, and a listing with no Marker requested once.
erikdarlingdata
marked this pull request as ready for review
September 29, 2026 07:10
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.
Fixes #4708.
Why
RDS and Aurora PostgreSQL log reads (deadlocks, log events and plan capture, stderr and csvlog) opened only the newest log file. When RDS rotates the log, whatever the previous file received after the last read was never read, and a new stderr file was read from its last 10,000 lines instead of its first line. The read position also lived only in memory, so a restart re-read just the last 10,000 lines of the newest file. #4708 expects the old file to be finished, the new stderr file to be read from its start, and positions to survive a restart. The list of log files was also read as a single page, but RDS answers in pages once an instance holds more files than one page carries (three days of hourly stderr and csv files is about 144), so the newest file, or the file a saved position holds, could be missing from the list the read chose from.
What changes
RdsLogSource(the source all three RDS ingestors share):ReadAgain. The position moves to the newest file only when the chunk that reaches the end of the old file is committed (ResumeMarker.NextKey), so a failed store write leaves the position on the old file.Marker "0"), stderr as well as csv, instead of its last 10,000 lines.FilesSkipped). A held file RDS no longer lists is flagged (ResumeFileMissing) and the read falls back to the newest file's bounded tail.RestorePositionseeds a saved position (never over one the source already holds),CommittedPositiongives the position a commit leaves,MaxPassesPerCycle(4) andRunPassesAsyncbound the reads per cycle.ListLogFilesAsynccallsDescribeDBLogFilesagain with the response'sMarker(same instance andpostgresqlfilter each time) until a page carries noMarker, and gathers every page into one list. Every choice after that works on the whole list: the stderr/csv filter, the newest file, the held file and the stale-csv check. A page with no files but aMarkeris followed, and aMarkerequal to the one the request carried ends the loop, so a bad answer cannot spin. The loop is capped at 20 pages (MaxLogListingPages); at the cap the read goes on with the files it has and logs one Warning that names the instance and the cap, at most once an hour for each instance (no newcollection_logmeasurement).RdsLogSourcetakes an optional logger for that, and the three ingestors pass theirs to the source they create.ListLogFilesAsyncsends noMaxRecords, so each page is the service's default size, which the API reference does not state. A listing that fits on one page was never affected; this guards the listings that page, such as csvlog files alongside stderr or a longer log retention.Item 1, positions survive a restart (
RdsResumeStore.cs, no schema change):collect.collector_stateafter the chunk's rows are stored and the in-process position has moved. It loads once per server before that server's first read.pg_plan_capture,pg_deadlocksorpg_log_events; state keylog_resume(stderr file) orlog_resume_csv(csv file), the keys those collectors already declare for the self-hosted tail; valuerds|1|<instance>|<marker>|<file>. The file name is last so a|in it cannot break the parse.<offset>|<file>and starts with digits, so neither reader takes the other's value. An unrecognised value is ignored, which means a first read of the newest file.ResumeFileMissingand today's start rule; listed but older than the newest gives the finish-then-open sequence. A saved position for a different instance (a writer failover) is not used.Item 2, one cycle: each ingestor loops while the chunk says
ReadAgain, at most 4 reads, adding the outcomes. A rotation now finishes the old file and reads the new one in the same call (an hourly plan capture no longer lags a cycle). A file with more data than a call returns keeps the cycle reading it up to the bound, and the position stays on it for the next cycle.Item 3, measurements and wording:
RdsIngestOutcomecarriesFilesSkippedandResumeFileMissing(summed over the passes). The runner records them on thecollection_logrow aslog_files_skipped_by_rotationandlog_resume_file_missing, the labels the self-hosted tail uses, throughMeasurementsFor.WithLogResumeNotestakes amanagedRouteflag, which the worker sets for Aurora and RDS targets. Those targets get their own two sentences (RdsLogResumeLostNote,RdsLogFilesSkippedNote) instead of the self-hosted wording aboutlog_directoryand a 4 MB window._rdsPlansblock and inRdsCpuIngestor, and the paragraph in theRdsLogSourceheader, now describe the saved position. The comments in the deadlock and log-event ingestors and the csv carry that say the source keeps an in-memory marker per (instance, file) are still true and are unchanged.Test plan
RdsResumePersistenceTests: restart resumes from the saved position (all three ingestors), csv position underlog_resume_csv, a saved file RDS no longer lists is reported missing and the tail is read, non-RDS values ignored, the self-hosted tail ignores an RDS value, load once per server, nothing saved when the store write fails, rotation finished and newest read in one call (all three), at most 4 reads per cycle (all three), files skipped carried on the outcome (all three), measurements only when they happened, the RDS sentences, and a source pin that the runner passes the outcome fields and the worker passes the flag.RdsResumeStoreLiveTestsagainst a local PostgreSQL 18.6 cluster (DARLING_TEST_PG): the runner's own state methods write exactly one row (pg_plan_capture,log_resume,rds|1|...), and a new ingestor over the same store resumes from its marker.RdsLogSourceTests(a fake that answers in pages with aMarker): the newest file on page 2 is the one opened, a restored position whose file is on page 2 is finished and then the newest is opened, the newest.csvon page 2 is chosen, the stale-csv check compares the whole list, a page with no files but aMarkeris followed, a repeatedMarkerends the loop, a listing that never ends stops at 20 pages and logs the Warning once (again only an hour later, once for each instance, and not again from a second source over the same logger), and a listing with noMarkeris requested once. Red first, with the list read as a single page (the source as it was, plus the new logger argument so the cap test compiles): 7 of the 39 tests in the class fail, the eighth new test holds either way. Green with the fix: 39 of 39. The cap test's fake returns a newMarkeron every page, because a fixedMarkerends the loop on the second page.RdsLogEventIngestorCsvlogTests.ARotation_ReadsTheNewFileFromItsStart_AndWalksItForwardandARotationMidSkip_CountsTheDroppedRecordOnceupdated: one call now finishes the old file and opens the next.Rds*classes,CollectorMeasurementSeamTests,DocCommentHygiene,LivePostgresCollectionHygieneTests,LiveCleanupConversionRatchetTests,PgLogEventsPipelineTests,RepoFileAdoptionTests: 298 run, 1 failed (LivePostgresCollectionHygieneTests.EveryClassUsingTheSharedStore_IsSerializedOrDocumentsWhyNot, because the new live class was not in thelive-postgrescollection); fixed, and that class plus the live class andLiveCleanupConversionRatchetTestsre-ran clean (27 tests, 0 failed).Darling.Testsin CI at7d74d8554: every test passed exceptPgServerLogTailRotationLiveTests.ALogThatGrewPastTheWindow_DisclosesTheBytesSkipped_AndCollectsTheLastWindow, the intermittent test tracked in Flaky live test: ALogThatGrewPastTheWindow times out with 0 rows read on unrelated PR runs #4719, which this change does not touch.Rds*classes plusDocCommentHygiene,RepoFileAdoption,LivePostgresCollectionHygieneTests,LiveCleanupConversionRatchetTests,CollectorMeasurementSeamTestsandPgLogEventsPipelineTests: 456 run, 0 failed, 4 skipped (live).Darling.Testsrun locally withoutDARLING_TEST_PG(live tests skip): 17087 total, 0 failed, 1112 skipped, 1 not run (the one[Fact(Explicit = true)]test).Darling.Testsin CI, live tests included, at the new head.Worth double-checking
GetCollectorStateAsyncandSaveCollectorStateAsyncswallow failures by design (no state, keep the older value). A failed load reads as a first contact; a failed save leaves the older saved position. Both are the at-least-once direction.newestStderr - newestCsv > 5 minutes) no longer testsHasValueon each side, because both sides are plain numbers over the gathered list. The old chains sat under a null-conditional, so on a listing that was present but had no.csvfile the empty side already read as 0 (lifted to a nullable number with a value), and theHasValuetest only ever held back an omitted list, which now arrives as an empty list and gives 0 minus 0. By reading the code the result is the same in every case, including a listing with stderr files and no.csvfile, which reads as stale after 5 minutes as before; no test pins that case, and it was not run against the old code.DescribeDBLogFilescall for each page: an instance whose files fit on one page still costs one call, a larger one costs a few more on every read (at most 20). The three ingestors each own a source, so the once-an-hour Warning is kept for each logger and not for each source; otherwise an instance over the cap would be reported up to three times an hour.CHANGELOG
SECTION: Fixed
ENTRY:
REF:
[RDS PostgreSQL log reads: finish rotated files in one cycle, keep positions across a restart, report skipped and missing files (#4708) #4713]: RDS PostgreSQL log reads: finish rotated files in one cycle, keep positions across a restart, report skipped and missing files (#4708) #4713