diff --git a/Darling/Darling.Tests/DarlingManagedPostgresTests.cs b/Darling/Darling.Tests/DarlingManagedPostgresTests.cs index 5e369a7f50..8ef3ff875e 100644 --- a/Darling/Darling.Tests/DarlingManagedPostgresTests.cs +++ b/Darling/Darling.Tests/DarlingManagedPostgresTests.cs @@ -427,8 +427,9 @@ public void EveryConfMarker_IsDistinct_AndNoneIsASubstringOfAnother() assertion below vacuously true, and a reflection filter that stopped matching is exactly the silent failure this shape invites. Eleven blocks as of #3175; twelve as of #3802 (v12 WAL sizing); thirteen as of #3899 (v13 statement statistics); fourteen as of #3909 (v14 PostgreSQL 17 - maintenance_work_mem limit); fifteen as of #4246 (v15 WAL compression). */ - Assert.Equal(15, markers.Length); + maintenance_work_mem limit); fifteen as of #4246 (v15 WAL compression); sixteen as of #4246 (v16 + checkpoint interval). */ + Assert.Equal(16, markers.Length); Assert.Equal(markers.Length, markers.Select(m => m.Value).Distinct(StringComparer.Ordinal).Count()); @@ -2267,6 +2268,75 @@ public void EnsureConfAppended_AppendsV15Once_AndNotAgainOnANextStart() } } + /* ===================== v16 checkpoint interval (#4246) ===================== */ + + /// + /// The v16 block (#4246): checkpoint_timeout = 15min only. See + /// for the trial and the sync-bar risk, and why the + /// block deliberately says nothing about max_wal_size. + /// + [Fact] + public void CheckpointIntervalConfAppend_PinsV16Marker_AndSetsCheckpointTimeout() + { + var block = DarlingManagedPostgres.BuildCheckpointIntervalConfAppend(); + + Assert.Contains(DarlingManagedPostgres.ConfMarkerV16, block, StringComparison.Ordinal); + Assert.Equal("15min", LastSettingValue(block, "checkpoint_timeout")); + + /* No fingerprint or stamp line, or the v8/v12 staleness checks would misread what they scan. */ + Assert.DoesNotContain(DarlingManagedPostgres.ConfHardwareFingerprintPrefix, block, StringComparison.Ordinal); + Assert.DoesNotContain(DarlingManagedPostgres.ConfWalSizingStampPrefix, block, StringComparison.Ordinal); + + /* v12 (#3802) is still the only thing that ever sets these: this block leaves the disk-derived + ceiling exactly where it is. */ + Assert.Null(LastSettingValue(block, "max_wal_size")); + Assert.Null(LastSettingValue(block, "min_wal_size")); + Assert.Null(LastSettingValue(block, "checkpoint_completion_target")); + } + + /// The generic per-setting source scan (#4214) walks every marker in this list; a block absent + /// from it would be invisible to that scan even though it is live. + [Fact] + public void ConfMarkerV16_IsInAllManagedConfMarkers() + => Assert.Contains(DarlingManagedPostgres.ConfMarkerV16, DarlingManagedPostgres.AllManagedConfMarkers); + + /// + /// The heal on real files (#4246): a cluster whose conf carries every earlier marker but not v16 gains + /// exactly one v16 block, with checkpoint_timeout live in the file, and a second start appends + /// nothing more -- the same once-only shape v9-v11, v13 and v15 prove elsewhere (), exercised here through the real + /// rather than string concatenation. + /// + [Fact] + public void EnsureConfAppended_AppendsV16Once_AndNotAgainOnANextStart() + { + var root = Directory.CreateTempSubdirectory("darling-v16-"); + try + { + var dataDirectory = Path.Combine(root.FullName, "pg"); + Directory.CreateDirectory(dataDirectory); + var confPath = Path.Combine(dataDirectory, "postgresql.conf"); + File.WriteAllText(confPath, DarlingManagedPostgres.BuildConfAppend(5994)); + + var pg = new DarlingManagedPostgres( + new PostgresConfig { Managed = true, Port = 5994, DataDirectory = dataDirectory }, NullLogger.Instance); + + pg.EnsureConfAppended(dataDirectory); + var first = File.ReadAllText(confPath); + Assert.Equal(1, CountOccurrences(first, DarlingManagedPostgres.ConfMarkerV16)); + Assert.Equal("15min", LastSettingValue(first, "checkpoint_timeout")); + + pg.EnsureConfAppended(dataDirectory); + var second = File.ReadAllText(confPath); + Assert.Equal(1, CountOccurrences(second, DarlingManagedPostgres.ConfMarkerV16)); + Assert.Equal("15min", LastSettingValue(second, "checkpoint_timeout")); + } + finally + { + root.Delete(recursive: true); + } + } + /* ===================== v12 wal sizing (#3802) ===================== */ private const long OneGb = 1024L * 1024 * 1024; diff --git a/Darling/Darling.Tests/ManagedConfFileTests.cs b/Darling/Darling.Tests/ManagedConfFileTests.cs index aeb1ba74b1..275d621eda 100644 --- a/Darling/Darling.Tests/ManagedConfFileTests.cs +++ b/Darling/Darling.Tests/ManagedConfFileTests.cs @@ -323,6 +323,160 @@ public void RenderWithValues_ExtraKey_AppendedAfterOwnedKeys_ResultIsNotHandEdit Assert.False(ManagedConfFile.IsHandEdited(rendered)); } + /// #4246: v16's checkpoint interval is one of the builders + /// calls, so the managed body itself carries checkpoint_timeout — not just the legacy v1-v16 + /// appenders EnsureConfAppended runs against a self-hosted conf. + [Fact] + public void RenderBody_ContainsCheckpointTimeout() + { + var body = ManagedConfFile.RenderBody(SampleInputs()); + + Assert.Contains("checkpoint_timeout = '15min'", body, StringComparison.Ordinal); + } + + /// + /// #4246 (claude-desktop's parity pin, 14:47Z): for EVERY marker in + /// , every key that marker's OWN legacy builder + /// emits must show up in 's output too. The expected keys are DERIVED + /// by parsing each builder's actual output () rather than + /// hand-typed here, so a future vN whose marker is added to AllManagedConfMarkers and whose builder is + /// written, but whose builder call is never added to RenderBody (exactly this PR's own bug, for v16), + /// fails this test on its own keys — with no hand roster to forget to update alongside it. + /// + /// v14 (the legacy maintenance_work_mem cap) is the one documented skip. RenderBody + /// decides v14 purely from its own inputs: it reduces the blocks above v14, + /// reads whatever maintenance_work_mem is in force from that reduction, and calls the SAME + /// predicate + /// EnsureConfAppended's own heal uses. The skip is arithmetic, not a missing input: every + /// maintenance_work_mem value this fixture's memory-sizing builders can ever produce is + /// 's own value, which is capped at + /// (2047 MB = 2,096,128 kB) for ANY RAM size — + /// the cap is a MIN in the formula, so raising RAM without bound never raises the derived value past it. That + /// cap sits 1,023 kB UNDER (2,097,151 kB) — + /// deliberately, so a fresh derivation never crosses the very limit v14 exists to cap. So no RAM figure this + /// fixture could supply (there is no upper bound to check: the derivation's cap makes every RAM equally + /// incapable) ever drives RenderBody to append v14 on its own. v14 only fires for a value ALREADY on + /// disk from before this cap existed (a pre-#3909 store) or set directly by ALTER SYSTEM — inputs + /// RenderBody's formula-only inputs cannot produce. It is exercised directly by + /// NeedsLegacyMaintenanceWorkMemCap_OnlyForAnOverLimitValueOnPostgres17OrEarlier in + /// DarlingManagedPostgresTests, not here. + /// + [Fact] + public void RenderBody_CarriesEveryManagedConfMarkersOwnedKeys() + { + var inputs = SampleInputs(); + var body = ManagedConfFile.RenderBody(inputs); + var (_, bodyValues) = ExtractValues(body); + + foreach (var marker in DarlingManagedPostgres.AllManagedConfMarkers) + { + var representativeBlock = BuildRepresentativeBlockOrNull(marker, inputs); + if (representativeBlock is null) + { + continue; // v14: documented skip above, exercised elsewhere. + } + + foreach (var (_, key, _) in DarlingManagedPostgres.ParseConfText(representativeBlock)) + { + Assert.True( + bodyValues.ContainsKey(key), + $"marker '{marker}' owns key '{key}' (from its own builder's output) but RenderBody's rendered body does not carry it."); + } + } + } + + /// Maps one entry to its OWN legacy + /// builder's output, called with inputs this fixture's can drive (RAM/disk + /// authoritative, PostgreSQL 18) — the keys asserted against RenderBody are parsed back out of this + /// text, never hand-typed. Throws for a marker with no entry here, so a marker added to + /// AllManagedConfMarkers without a corresponding line here fails loudly instead of being silently + /// skipped. + private static string? BuildRepresentativeBlockOrNull(string marker, ManagedConfFile.RenderInputs inputs) + { + if (marker == DarlingManagedPostgres.ConfMarker) + { + return DarlingManagedPostgres.BuildConfAppend(inputs.Port); + } + + if (marker == DarlingManagedPostgres.ConfMarkerV2) + { + return DarlingManagedPostgres.BuildWorkerSizingConfAppend(); + } + + if (marker == DarlingManagedPostgres.ConfMarkerV3) + { + return DarlingManagedPostgres.BuildMemorySizingConfAppend(inputs.RamBytes); + } + + if (marker == DarlingManagedPostgres.ConfMarkerV4) + { + return DarlingManagedPostgres.BuildWriteThroughputConfAppend(); + } + + if (marker == DarlingManagedPostgres.ConfMarkerV5) + { + return DarlingManagedPostgres.BuildColocatedSizingConfAppend(inputs.RamBytes); + } + + if (marker == DarlingManagedPostgres.ConfMarkerV6) + { + return DarlingManagedPostgres.BuildLogRotationConfAppend(); + } + + if (marker == DarlingManagedPostgres.ConfMarkerV7) + { + return DarlingManagedPostgres.BuildCompressionMemoryConfAppend(inputs.RamBytes); + } + + if (marker == DarlingManagedPostgres.ConfMarkerV8) + { + return DarlingManagedPostgres.BuildHardwareSizingConfAppend(inputs.RamBytes, inputs.HypertableCount); + } + + if (marker == DarlingManagedPostgres.ConfMarkerV9) + { + return DarlingManagedPostgres.BuildTimeZoneConfAppend(); + } + + if (marker == DarlingManagedPostgres.ConfMarkerV10) + { + return DarlingManagedPostgres.BuildMessageLocaleConfAppend(); + } + + if (marker == DarlingManagedPostgres.ConfMarkerV11) + { + return DarlingManagedPostgres.BuildJobExecutionLoggingConfAppend(); + } + + if (marker == DarlingManagedPostgres.ConfMarkerV12) + { + return DarlingManagedPostgres.BuildWalSizingConfAppend(inputs.DataVolumeFreeBytes, inputs.DataVolumeTotalBytes, inputs.PostgresMajor); + } + + if (marker == DarlingManagedPostgres.ConfMarkerV13) + { + return DarlingManagedPostgres.BuildStatementStatisticsConfAppend(inputs.EffectivePreloadList); + } + + if (marker == DarlingManagedPostgres.ConfMarkerV14) + { + return null; // documented skip -- no RAM ever drives RenderBody's own inputs past the cap; see the class doc comment on RenderBody_CarriesEveryManagedConfMarkersOwnedKeys + } + + if (marker == DarlingManagedPostgres.ConfMarkerV15) + { + return DarlingManagedPostgres.BuildWalVolumeConfAppend(); + } + + if (marker == DarlingManagedPostgres.ConfMarkerV16) + { + return DarlingManagedPostgres.BuildCheckpointIntervalConfAppend(); + } + + throw new InvalidOperationException( + $"marker '{marker}' is in AllManagedConfMarkers but has no representative-block mapping in this parity test -- add one."); + } + private static (List Order, Dictionary Values) ExtractValues(string body) { var order = new List(); diff --git a/Darling/Darling.Tests/ManagedConfMigrationTests.cs b/Darling/Darling.Tests/ManagedConfMigrationTests.cs index 89da4a287c..a1c42fabfc 100644 --- a/Darling/Darling.Tests/ManagedConfMigrationTests.cs +++ b/Darling/Darling.Tests/ManagedConfMigrationTests.cs @@ -46,6 +46,17 @@ public void ClassifyLines_UntouchedV15Block_IsOurs() Assert.Equal(ConfLineClassification.Ours, wal.Classification); } + [Fact] + public void ClassifyLines_UntouchedV16Block_IsOurs() + { + var conf = DarlingManagedPostgres.BuildCheckpointIntervalConfAppend(); + var lines = ClassifyLines(conf); + + var checkpoint = FindByText(lines, "checkpoint_timeout"); + Assert.Equal(ConfLineClassification.Ours, checkpoint.Classification); + Assert.Equal(DarlingManagedPostgres.ConfMarkerV16, checkpoint.BlockMarker); + } + [Fact] public void ClassifyLines_UntouchedV13PreloadLine_IsOurs() { diff --git a/Darling/Darling.Tests/ManagedConfRehearsalTests.cs b/Darling/Darling.Tests/ManagedConfRehearsalTests.cs index 81bb46204a..fb79264ec0 100644 --- a/Darling/Darling.Tests/ManagedConfRehearsalTests.cs +++ b/Darling/Darling.Tests/ManagedConfRehearsalTests.cs @@ -27,17 +27,19 @@ namespace Darling.Tests; /// stay exactly where it is); and log_timezone = 'UTC' at the end (a key no managed block owns, so it /// stays exactly where it is too). /// -/// This fixture's numbers are 69 lines removed / 24 managed keys / 1 operator line moved. +/// This fixture's numbers are 69 lines removed / 25 managed keys / 1 operator line moved. /// The "1 operator line moved" is exactly what the rewrite logic requires. The counts below explain why they -/// are 69/24 rather than some other plausible pair, given how today's builders emit their blocks: +/// are 69/25 rather than some other plausible pair, given how today's builders emit their blocks: /// -/// managed keys 24 vs 23. emits one line per key +/// managed keys 25 vs 23. emits one line per key /// reads back from calling every Build*ConfAppend in -/// order with THIS PR's inputs (RAM authoritative, disk authoritative, PostgreSQL 18) — today that is 24 keys. -/// The extra key is min_wal_size: v12's WAL-sizing block +/// order with THIS PR's inputs (RAM authoritative, disk authoritative, PostgreSQL 18) — today that is 25 keys. +/// One extra key is min_wal_size: v12's WAL-sizing block /// () writes both max_wal_size AND /// min_wal_size, and nothing later in the v1-v14 order overwrites the second one, so it survives the -/// reduction as its own key. +/// reduction as its own key. The other extra key is checkpoint_timeout: v16 (#4246) +/// () is now appended in +/// too, and nothing else in this fixture's inputs sets that key. /// lines removed 69. The initdb stock conf, v1-v14 /// each appended once except v8 four times, the three hand edits — this fixture reproduces, so the shapes /// match. The absolute REMOVAL count is a function of how many total OURS lines the classifier drops (three @@ -122,7 +124,7 @@ public void Rewrite_RehearsalFixture_LinesRemovedManagedKeysAndOperatorLinesMove var afterLineCount = result.NewConfText.Split('\n').Length; Assert.Equal(69, beforeLineCount - afterLineCount); - Assert.Equal(24, managedValues.Count); + Assert.Equal(25, managedValues.Count); Assert.Single(result.ExcludedKeys); Assert.Contains("max_connections", result.ExcludedKeys); } diff --git a/Darling/PerformanceMonitor.Darling.Service/DarlingManagedPostgres.cs b/Darling/PerformanceMonitor.Darling.Service/DarlingManagedPostgres.cs index b5e23874bb..e2450fbf9e 100644 --- a/Darling/PerformanceMonitor.Darling.Service/DarlingManagedPostgres.cs +++ b/Darling/PerformanceMonitor.Darling.Service/DarlingManagedPostgres.cs @@ -504,16 +504,16 @@ public sealed class DarlingManagedPostgres /// already proves lz4 ships in the bundled runtime; it costs less CPU than zstd for a few GB/hour of /// image data, the same trade the TOAST setting already made. /// - /// The checkpoint interval is held, not shipped. A longer checkpoint_timeout would cut - /// WAL further by re-imaging each hot page less often — #4246 found roughly two-thirds of one 5-minute - /// cycle's images repeated the previous cycle's — but it risks the store's own checkpointer self-alert: - /// (#4037) fires when a checkpoint's sync phase - /// averages more than 10 seconds, a bar that exists because sync phases of 14.0s and 25.2s killed reads on - /// a production store. #3892 found that a longer interval puts more files into each checkpoint, which - /// makes each sync phase longer, so a 15-minute interval risks trading WAL volume for killed reads and for - /// alerts firing on a healthy store. One production store measures the interval first, through ALTER - /// SYSTEM and a reload rather than this block; it ships here later, in its own marker, only if that - /// measurement stays under the sync bar. + /// The checkpoint interval ships separately, in v16. A longer checkpoint_timeout would + /// cut WAL further by re-imaging each hot page less often — #4246 found roughly two-thirds of one + /// 5-minute cycle's images repeated the previous cycle's — but it risks the store's own checkpointer + /// self-alert: (#4037) fires when a + /// checkpoint's sync phase averages more than 10 seconds, a bar that exists because sync phases of 14.0s + /// and 25.2s killed reads on a production store. #3892 found that a longer interval puts more files into + /// each checkpoint, which makes each sync phase longer, so a 15-minute interval risks trading WAL volume + /// for killed reads and for alerts firing on a healthy store. One production store measured the interval + /// first, through ALTER SYSTEM and a reload rather than this block; it now ships as + /// , its own marker, gated on that measurement staying under the sync bar. /// /// Does not touch max_wal_size. (#3802) already bounds it /// by free disk, and this block leaves that bound alone. @@ -533,6 +533,34 @@ public sealed class DarlingManagedPostgres /// public const string ConfMarkerV15 = "# Managed by PerformanceMonitor Darling (v15 WAL compression) -- do not remove this block"; + /// + /// The v16 marker (#4246): checkpoint_timeout = 15min only, up from PostgreSQL's 5-minute default. + /// The interval was held back from v15 (see ) pending a 24-hour trial on one + /// production store, applied there through ALTER SYSTEM and a reload rather than this block. That + /// trial's reading at +3 hours: WAL volume fell from 14.0 to 4.6 GB/h, the + /// full-page-image share from 8.6% to 4.2%, and the average checkpoint sync + /// phase from 1.66 s to 0.24 s. + /// + /// The risk this carries. A longer interval puts more dirty pages, and so more files, into + /// each checkpoint's sync phase, which is what makes the phase take longer. + /// (#4037) fires when a checkpoint's sync + /// phase averages more than 10 seconds — the bar #3892 traced to sync phases of 14.0s and 25.2s that + /// killed reads on a production store. This block ships only because the trial's measurement stayed + /// under that bar; a store that regresses past it needs a shorter interval, not a self-alert override. + /// + /// Does not touch max_wal_size. (#3802) already bounds it + /// by free disk, and this block leaves that bound alone. + /// + /// checkpoint_timeout is sighup-context in PostgreSQL, so a running store could take it + /// from a reload alone — but like the other sighup settings this service manages, this append runs + /// before pg_ctl start, so a service-owned start applies it on the very start that writes the + /// block, the v9-v11 story. Managed stores only; a bring-your-own store keeps whatever + /// checkpoint_timeout its owner set. A later change to this value needs a NEW marker (the + /// v11/v14/v15 precedent): this block heals by its marker's absence, so an edited value in an + /// already-marked file would never be seen. + /// + public const string ConfMarkerV16 = "# Managed by PerformanceMonitor Darling (v16 checkpoint interval) -- do not remove this block"; + /// /// Every marker this class ever appends to postgresql.conf, in append order (#4214). A generic scan that /// asks "is this line inside SOME managed block" (the host-profile check's per-setting source attribution) @@ -546,7 +574,7 @@ public sealed class DarlingManagedPostgres [ ConfMarker, ConfMarkerV2, ConfMarkerV3, ConfMarkerV4, ConfMarkerV5, ConfMarkerV6, ConfMarkerV7, ConfMarkerV8, ConfMarkerV9, ConfMarkerV10, ConfMarkerV11, ConfMarkerV12, ConfMarkerV13, ConfMarkerV14, - ConfMarkerV15, + ConfMarkerV15, ConfMarkerV16, ]; /// @@ -2245,6 +2273,22 @@ public static string BuildWalVolumeConfAppend() return builder.ToString(); } + /* ===================== v16 checkpoint interval (#4246) ===================== */ + + /// + /// The v16 block: checkpoint_timeout = 15min only. See for the trial, + /// the sync-bar risk, and why this deliberately does not touch max_wal_size. Carries no fingerprint + /// or stamp line, so the v8 and v12 staleness checks are untouched by this block. + /// + public static string BuildCheckpointIntervalConfAppend() + { + var builder = new StringBuilder(); + builder.Append('\n'); + builder.Append(ConfMarkerV16).Append('\n'); + builder.Append("checkpoint_timeout = 15min\n"); + return builder.ToString(); + } + /* ===================== v12 wal sizing (derived from data-volume headroom, #3802) ===================== */ /// 1 GB — the floor under the derived max_wal_size, and PostgreSQL's own default for it: @@ -3657,6 +3701,19 @@ so a service-owned start applies it on the very start that writes the block. */ "Appended v15 WAL compression to postgresql.conf (wal_compression = lz4): most of this store's WAL is full-page images, and compression shrinks every one of them. Effective on this start when the service owns it."); } + /* v16 (#4246): keyed on its marker's absence like v9-v11, v13 and v15, and placed last so it stays + the block this method appends LAST on any start that fires it, matching its place at the end of + AllManagedConfMarkers. Carries no fingerprint or stamp line, so v8 and v12 read exactly what they + did before this block existed. checkpoint_timeout is sighup-context, but like the other sighup + settings this service manages, this is appended before pg_ctl start, so a service-owned start + applies it on the very start that writes the block. */ + if (!conf.Contains(ConfMarkerV16, StringComparison.Ordinal)) + { + File.AppendAllText(confPath, BuildCheckpointIntervalConfAppend()); + _logger.LogInformation( + "Appended v16 checkpoint interval to postgresql.conf (checkpoint_timeout = 15min): a longer interval re-images each hot page less often, cutting write-ahead log volume. Effective on this start when the service owns it."); + } + LogStatementStatisticsPreloadCoverage(dataDirectory); } diff --git a/Darling/PerformanceMonitor.Darling.Service/ManagedConfFile.cs b/Darling/PerformanceMonitor.Darling.Service/ManagedConfFile.cs index 82f1fe45d4..d3b2af2e8f 100644 --- a/Darling/PerformanceMonitor.Darling.Service/ManagedConfFile.cs +++ b/Darling/PerformanceMonitor.Darling.Service/ManagedConfFile.cs @@ -170,9 +170,9 @@ private static long RoundToGiB(long bytes) } /// - /// The body alone: one key = 'value' line per setting the v1 through v15 blocks set today, in the + /// The body alone: one key = 'value' line per setting the v1 through v16 blocks set today, in the /// order each key is FIRST introduced (v1 before v2 before v3 ...), holding the value each key has LAST — - /// exactly what PostgreSQL itself would read from those fifteen blocks appended in order to one file. Every + /// exactly what PostgreSQL itself would read from those sixteen blocks appended in order to one file. Every /// value is single-quoted and escaped (DarlingManagedPostgres.EscapeConfValue): PostgreSQL accepts a /// quoted string for a numeric or boolean GUC too, and quoting uniformly means no value's own content (a /// path with a backslash, say) needs a second escaping rule here. @@ -227,6 +227,9 @@ from starting. NeedsLegacyMaintenanceWorkMemCap is the same pure predicate Ensur blocks.Append(DarlingManagedPostgres.BuildWalVolumeConfAppend()); + /* v16 (#4246): a fixed checkpoint_timeout, appended in the same order as the legacy blocks above. */ + blocks.Append(DarlingManagedPostgres.BuildCheckpointIntervalConfAppend()); + var (order, values) = ReduceToLastOccurrence(blocks.ToString()); var body = new StringBuilder(); foreach (var key in order) diff --git a/Darling/PerformanceMonitor.Darling.Service/ManagedConfMigration.cs b/Darling/PerformanceMonitor.Darling.Service/ManagedConfMigration.cs index 339860bd11..36925a06b9 100644 --- a/Darling/PerformanceMonitor.Darling.Service/ManagedConfMigration.cs +++ b/Darling/PerformanceMonitor.Darling.Service/ManagedConfMigration.cs @@ -131,6 +131,7 @@ internal readonly record struct ClassifiedConfLine( DarlingManagedPostgres.ConfMarkerV13, DarlingManagedPostgres.ConfMarkerV14, DarlingManagedPostgres.ConfMarkerV15, + DarlingManagedPostgres.ConfMarkerV16, ]; /// @@ -290,6 +291,8 @@ store carries this line. See ClassifyV1Line's doc comment. */ FormattableString.Invariant($"maintenance_work_mem = {DarlingManagedPostgres.MaintenanceWorkMemCapMb}MB")), _ when span.Marker == DarlingManagedPostgres.ConfMarkerV15 => ClassifyFixedLine( key, text, "wal_compression", "wal_compression = lz4"), + _ when span.Marker == DarlingManagedPostgres.ConfMarkerV16 => ClassifyFixedLine( + key, text, "checkpoint_timeout", "checkpoint_timeout = 15min"), _ => (false, HandEditReason.FormMismatch), };