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),
};