[Bug]: Empty WAL archive check is silently skipped when enabling the plugin as WAL archiver on a previously plugin-less cluster · Issue #11126 · cloudnative-pg/cloudnative-pg · GitHub
You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Bug]: Empty WAL archive check is silently skipped when enabling the plugin as WAL archiver on a previously plugin-less cluster #11126
I have searched for an existing issue, and could not find anything. I believe this is a new bug.
I have read the troubleshooting guide
I have read the troubleshooting guide and I think this is a new bug.
I am running a supported version of CloudNativePG
I have read the troubleshooting guide and I think this is a new bug.
Version
1.30.0
What version of Kubernetes are you using?
1.36
What is your Kubernetes environment?
Cloud: Azure AKS
How did you install the operator?
Helm
What happened?
If a Cluster is first created without the barman-cloud plugin and is later modified to add the plugin as WAL archiver, the empty-WAL-archive safety check (CheckBackupDestination / barman-cloud-check-wal-archive) is never executed. The cluster will happily begin archiving into a non-empty destination without the "Expected empty archive" protection ever running. This happens regardless of the cnpg.io/skipEmptyWalArchiveCheck annotation.
This is the inverse of issue 350 in plugin-barman-cloud: there the check fires when it arguably shouldn't; here it is silently skipped when it arguably should run.
Why it happens
The check is gated on the .check-empty-wal-archive flag file in PGDATA (plugin internal/cnpgi/common/wal.go, IsEmptyWalArchiveCheckEnabled(...) && checkFileExisting). That flag is created at bootstrap but removed by the operator as soon as the cluster reports ContinuousArchiving=true (reconcileCheckWalArchiveFile, internal/management/controller/instance_controller.go).
On a plugin-less cluster this condition still becomes true: CloudNativePG keeps archive_mode=on and its wal-archive command returns success as a no-op when no archiver is configured (pkg/management/postgres/archiver/archiver.go → "Backup not configured, skipping WAL" → internal/cmd/manager/walarchive/cmd.go sets the condition). So within a few minutes of bootstrap the flag is gone. Adding the plugin later never re-creates it, so CheckBackupDestination is never invoked.
Steps to reproduce
Create a Cluster with bootstrap.initdb and no.spec.plugins. Let it run (so ContinuousArchiving becomes true and .check-empty-wal-archive is removed).
Ensure the target object store already contains data for serverName = <cluster-name> (e.g. reuse a bucket/serverName from a prior cluster, or a recovery source).
Contrast: create the same final config with the plugin present at bootstrap and the check fires as designed (fails with "Expected empty archive"). Same end state, opposite outcome depending only on whether the plugin was present at creation.
Expected
The empty-archive check should run on the first real WAL archive after a WAL-archiver plugin is enabled (unless cnpg.io/skipEmptyWalArchiveCheck: enabled), so enabling archiving into a non-empty destination is caught rather than silently co-mingling timelines.
Actual
The check is skipped entirely; archiving proceeds into the non-empty destination with no error or event.
Thanks for the detailed write-up, @magnus-longva-bouvet. Your analysis is accurate — this is a real gap in the current implementation.
To confirm your findings: reconcileCheckWalArchiveFile only removes the .check-empty-wal-archive flag when ContinuousArchiving becomes True[1], and the flag is only ever created during bootstrap operations (initdb, restore, pg_basebackup) [2][3]. There is no mechanism to re-create it when a WAL-archiver plugin is added to a running cluster.
On a plugin-less cluster, the built-in archiver returns success as a no-op when no backup is configured [4], which causes the ContinuousArchiving condition to become True and removes the flag file within minutes of bootstrap. By the time the plugin is added, the sentinel is long gone, and the plugin's CheckBackupDestination gate (IsEmptyWalArchiveCheckEnabled && checkFileExisting) [5] is never satisfied.
The related thread plugin-barman-cloud#350 describes the inverse scenario (check fires when it shouldn't), and your comment there correctly identifies that the fix in #457 addressed the annotation behavior but not this path. PR #11032 (merged 2026-06-26) fixes the sidecar injection deadlock when enabling the plugin on an existing cluster, but does not address re-creating the sentinel file for the empty-archive check.
A potential fix would likely involve either:
Re-creating .check-empty-wal-archive when a WAL-archiver plugin is added (e.g., in the operator's plugin reconciliation path), or
Having the plugin itself detect that it's being invoked for the first time (no prior successful archive) and run the check regardless of the flag file.
05:45:38Z: Reaches ContinuousArchiving=True with reason ContinuousArchivingSuccess via the no-op path. This prematurely deletes the .check-empty-wal-archive sentinel file.
After Patch: The plugin sidecar is added, but the sentinel is already gone, leaving no trace for the empty check to fire:
ls: cannot access '/var/lib/postgresql/data/pgdata/.check-empty-wal-archive': No such file or directory
cluster-control
05:49:43Z: Bootstrap completes.
05:50:04Z: Correctly runs the empty archive check, detects existing data, and blocks archiving by transitioning to ContinuousArchiving=False (ContinuousArchivingFailing). The sentinel file remains intact:
cluster-control-1 (Executes check and catches the collision)
{"level":"info","ts":"2026-07-06T05:50:04.434819892Z","logger":"barman-cloud-check-wal-archive","msg":"2026-07-06 05:50:04,434 [40] ERROR: WAL archive check failed for server cluster-control: Expected empty archive","pipe":"stderr","logging_pod":"cluster-control-1"}
{"level":"error","ts":"2026-07-06T05:50:04.463555309Z","msg":"Error invoking barman-cloud-check-wal-archive","logging_pod":"cluster-control-1","error":"exit status 1"}
MinIO Bucket Listing
The destination bucket already contained a pre-existing object (...0001). cluster-buggy silently co-mingled its own new WAL segments (...0004, ...0005) right next to it without warning:
[2026-07-06 05:45:38 UTC] 16MiB STANDARD wals/0000000100000000/000000010000000000000004 # Overwritten/Added silently by buggy cluster
[2026-07-06 05:46:38 UTC] 16MiB STANDARD wals/0000000100000000/000000010000000000000005 # Overwritten/Added silently by buggy cluster
Root cause, as I traced it: the check is gated on the .check-empty-wal-archive sentinel, which is removed by reconcileCheckWalArchiveFile once ContinuousArchiving=True. But archiver-less clusters also reach ContinuousArchiving=True (same Success reason) via the no-op wal-archive path, so the sentinel is consumed within minutes of bootstrap — long before any real archiver exists. When a WAL-archiver plugin is enabled later, nothing re-creates the sentinel, so the plugin-side check (IsEmptyWalArchiveCheckEnabled && sentinel exists) can never fire.
Proposed approach (two changes that need to land together):
Re-create/keep the sentinel from the instance reconciler whenever the cluster has no WAL archiver configured. Detection would be conservative — treat any enabled plugin as a potential archiver, since archiveWALViaPlugins also invokes legacy plugins that don't declare isWALArchiver. A false "has archiver" only means the check runs once more; a false "no archiver" could clobber a working setup.
Distinguish no-op success in the condition reason: the no-op path would report ContinuousArchiving=True with a new reason (e.g. ContinuousArchivingNotConfigured) instead of ContinuousArchivingSuccess, and sentinel removal would be gated on True && reason==Success.
Change (1) alone is insufficient: the stale Success condition left over from the no-op era deletes the re-created sentinel on the next reconcile, before a newly enabled archiver runs its first check — I could observe this ordering in the repro. (2) closes that race. Since the condition is overwritten on every reconcile, existing archiver-less clusters self-heal to the new reason after upgrade with no migration needed.
Compatibility: Status stays True for archiver-less clusters, so kubectl wait --for=condition=ContinuousArchiving, alerting rules, and the e2e helpers are unaffected; only reason/message change. The estimated non-test diff is ~40 lines across 4 files, plus new unit tests for reconcileCheckWalArchiveFile (currently uncovered), including a regression test for the race above.
Two questions before I open a PR:
Are you comfortable with the reason change for archiver-less clusters, or would you prefer keeping Success and gating sentinel removal some other way (e.g. persisting archiver-configured state elsewhere)?
Should removing an archiver deliberately re-arm the empty-archive check for a later re-add? The proposed reconciler logic would do this as a side effect; cnpg.io/skipEmptyWalArchiveCheck remains the escape hatch for intentional re-use. Happy to make it either way — just want to confirm which behavior is intended.
As far as I can tell, the refactoring centralizes where the decision is computed, but doesn't change what it evaluates: shouldCheckEmptyWalArchive is still annotation && marker-file-present, and the marker's lifecycle (reconcileCheckWalArchiveFile) is untouched. So for the scenario in this issue — the marker was already consumed via the no-op archiving path before the plugin existed — the operator will compute false and the plugin will now obey it as-is (per #1009, the plugin no longer re-inspects the marker). The bug remains, just with a single owner for the decision.
That single ownership is actually good news for fixing this: once #11216 lands, the fix for this issue becomes operator-only (no plugin change needed). The proposal in my earlier comment — (1) re-create/keep the marker while no WAL archiver is configured, and (2) report the no-op archiving success with a distinct reason (e.g. ContinuousArchivingNotConfigured) so that marker removal is gated on a real archiving success — layers directly on top of the refactoring, feeding the corrected marker state into shouldCheckEmptyWalArchive.
@mnencia — if you're already planning to address this issue as a follow-up to #11216, happy to defer or help review (I have a kind+MinIO reproduction with a control cluster I can re-run against any candidate fix). Otherwise I'd like to take it, building on your refactoring. I have a working implementation of the earlier proposal, verified against the reproduction: with the fix, the marker survives on archiver-less clusters (condition reports True with a distinct NotConfigured reason), enabling the plugin later now triggers "Expected empty archive" as designed, and the bootstrap-with-plugin control behaves identically to today. Notably it works with the unmodified released plugin (v0.13.0), and I've confirmed it merges cleanly with #11216 (build and unit tests pass on the combined tree). Before opening a PR, the two open questions from my earlier comment (whether the reason change for archiver-less clusters is acceptable, and whether removing an archiver should re-arm the check) are the main design points I'd like a nod on.
On my side the scenario still reproduces: a cluster created without an archiver reports ContinuousArchiving=True(ContinuousArchivingSuccess) from the no-op archiving path, and the .check-empty-wal-archive marker was already gone 23 seconds after the cluster became Ready. Adding the barman-cloud plugin afterwards, with a destination that already
contained an object, the sidecar log shows no check at all and two 16 MiB WAL segments went straight into that destination. The control cluster — same final spec, plugin present at bootstrap — failed with Expected empty archive as designed. That matches what I'd expect from reading the refactoring: shouldCheckEmptyWalArchive centralises the decision, but it still evaluates the annotation and the marker, and the marker's lifecycle in reconcileCheckWalArchiveFile is unchanged.
I've opened PR #11258 with the approach from my earlier comments, rebased on top of #11216. It's operator-only — verified against the released plugin v0.13.0 with no plugin changes — and in the same reproduction the check now fires 12 seconds after the sidecar is injected, with nothing written into the non-empty destination. I also added a control for the case I was most unsure about: with an empty destination the check still runs exactly once, passes, and the marker is consumed as before, so the check doesn't start repeating.
The two design questions from my earlier comment are still the parts I'd most like your read on: whether the distinct condition reason for archiver-less clusters is acceptable, and whether removing an archiver should re-arm the check. I answered both in the PR description so there's something concrete to react to, but I'm happy to go the other way on either — and if you'd rather fold this into your own follow-up to #11216, that's fine too, just tell me which is easier to review.
Create a Cluster without spec.backup and let it write WAL. ContinuousArchiving=True (ContinuousArchivingSuccess) is reported, as described above.
Add spec.backup.barmanObjectStore and take an immediate Backup.
The Backup reports completed, and its backup_label starts in segment N. But segments up to N are absent from the bucket, even though pg_stat_archiver counts them as archived: they were handled by the no-op path before the store was configured.
Recovery from that backup fails with WAL file …000N for server <name> does not exist.
So beyond the skipped empty-archive check, the no-op path can leave a backup that is reported complete but unrestorable, with the cluster reporting healthy archiving throughout. We reproduced it repeatedly across our acceptance runs (listing the bucket with the source's own credentials). Our workaround is to create the Cluster only after its object store is ready, so it has spec.backup from bootstrap and never takes the no-op path. We haven't yet checked whether the barman-cloud plugin path (added later) produces the same unrestorable backup, but the marker analysis above suggests the window exists there too. A fix that keeps ContinuousArchiving from reporting True while no archiver is configured would close both.
Activity
dosubot commentedon Jul 3, 2026
Thanks for the detailed write-up, @magnus-longva-bouvet. Your analysis is accurate — this is a real gap in the current implementation.
To confirm your findings:
reconcileCheckWalArchiveFileonly removes the.check-empty-wal-archiveflag whenContinuousArchivingbecomesTrue[1], and the flag is only ever created during bootstrap operations (initdb, restore, pg_basebackup) [2] [3]. There is no mechanism to re-create it when a WAL-archiver plugin is added to a running cluster.On a plugin-less cluster, the built-in archiver returns success as a no-op when no backup is configured [4], which causes the
ContinuousArchivingcondition to becomeTrueand removes the flag file within minutes of bootstrap. By the time the plugin is added, the sentinel is long gone, and the plugin'sCheckBackupDestinationgate (IsEmptyWalArchiveCheckEnabled && checkFileExisting) [5] is never satisfied.The related thread plugin-barman-cloud#350 describes the inverse scenario (check fires when it shouldn't), and your comment there correctly identifies that the fix in #457 addressed the annotation behavior but not this path. PR #11032 (merged 2026-06-26) fixes the sidecar injection deadlock when enabling the plugin on an existing cluster, but does not address re-creating the sentinel file for the empty-archive check.
A potential fix would likely involve either:
.check-empty-wal-archivewhen a WAL-archiver plugin is added (e.g., in the operator's plugin reconciliation path), orTo reply, just mention @dosu.
Share context across your team and agents. Try Dosu.
ichibahn commentedon Jul 6, 2026
I can confirm this reproduces on current
main(025a9be53) withplugin-barman-cloudv0.13.0 (kind + MinIO), and I'd like to work on it.Reproduction summary — two clusters with the identical final spec, differing only in ordering:
barman-cloud-check-wal-archiveruns, fails withExpected empty archive, archiving blockedbarman-cloud-wal-archiveContinuousArchiving=TruethroughoutRepro steps and logs
1. Cluster Manifests
cluster-buggy(Bootstrapped without plugins, patched later):Patch applied to cluster-buggy later (11-add-plugin-patch.yaml):
cluster-control (Control group - bootstrapped with the plugin from the start):
2. Timeline & Conditions Transition
cluster-buggy05:35:25Z: Initial bootstrap completes (Initialized=True).
05:45:38Z: Reaches ContinuousArchiving=True with reason ContinuousArchivingSuccess via the no-op path. This prematurely deletes the .check-empty-wal-archive sentinel file.
After Patch: The plugin sidecar is added, but the sentinel is already gone, leaving no trace for the empty check to fire:
cluster-control05:49:43Z: Bootstrap completes.
05:50:04Z: Correctly runs the empty archive check, detects existing data, and blocks archiving by transitioning to ContinuousArchiving=False (ContinuousArchivingFailing). The sentinel file remains intact:
3. Sidecar Log Snippets
cluster-buggy-1(Skips validation, archives directly)cluster-control-1(Executes check and catches the collision)The destination bucket already contained a pre-existing object (...0001). cluster-buggy silently co-mingled its own new WAL segments (...0004, ...0005) right next to it without warning:
Root cause, as I traced it: the check is gated on the
.check-empty-wal-archivesentinel, which is removed byreconcileCheckWalArchiveFileonceContinuousArchiving=True. But archiver-less clusters also reachContinuousArchiving=True(sameSuccessreason) via the no-opwal-archivepath, so the sentinel is consumed within minutes of bootstrap — long before any real archiver exists. When a WAL-archiver plugin is enabled later, nothing re-creates the sentinel, so the plugin-side check (IsEmptyWalArchiveCheckEnabled&& sentinel exists) can never fire.Proposed approach (two changes that need to land together):
Re-create/keep the sentinel from the instance reconciler whenever the cluster has no WAL archiver configured. Detection would be conservative — treat any enabled plugin as a potential archiver, since
archiveWALViaPluginsalso invokes legacy plugins that don't declareisWALArchiver. A false "has archiver" only means the check runs once more; a false "no archiver" could clobber a working setup.Distinguish no-op success in the condition reason: the no-op path would report
ContinuousArchiving=Truewith a new reason (e.g.ContinuousArchivingNotConfigured) instead ofContinuousArchivingSuccess, and sentinel removal would be gated onTrue && reason==Success.Change (1) alone is insufficient: the stale
Successcondition left over from the no-op era deletes the re-created sentinel on the next reconcile, before a newly enabled archiver runs its first check — I could observe this ordering in the repro. (2) closes that race. Since the condition is overwritten on every reconcile, existing archiver-less clusters self-heal to the new reason after upgrade with no migration needed.Compatibility:
StatusstaysTruefor archiver-less clusters, sokubectl wait --for=condition=ContinuousArchiving, alerting rules, and the e2e helpers are unaffected; onlyreason/messagechange. The estimated non-test diff is ~40 lines across 4 files, plus new unit tests forreconcileCheckWalArchiveFile(currently uncovered), including a regression test for the race above.Two questions before I open a PR:
reasonchange for archiver-less clusters, or would you prefer keepingSuccessand gating sentinel removal some other way (e.g. persisting archiver-configured state elsewhere)?cnpg.io/skipEmptyWalArchiveCheckremains the escape hatch for intentional re-use. Happy to make it either way — just want to confirm which behavior is intended.Thanks !
ichibahn commentedon Jul 21, 2026
Following up after reading #11216 / #11217 and its plugin-side counterpart cloudnative-pg/plugin-barman-cloud#1009 — since they touch the same decision path, I checked how they interact with this issue.
As far as I can tell, the refactoring centralizes where the decision is computed, but doesn't change what it evaluates:
shouldCheckEmptyWalArchiveis stillannotation && marker-file-present, and the marker's lifecycle (reconcileCheckWalArchiveFile) is untouched. So for the scenario in this issue — the marker was already consumed via the no-op archiving path before the plugin existed — the operator will computefalseand the plugin will now obey it as-is (per #1009, the plugin no longer re-inspects the marker). The bug remains, just with a single owner for the decision.That single ownership is actually good news for fixing this: once #11216 lands, the fix for this issue becomes operator-only (no plugin change needed). The proposal in my earlier comment — (1) re-create/keep the marker while no WAL archiver is configured, and (2) report the no-op archiving success with a distinct reason (e.g.
ContinuousArchivingNotConfigured) so that marker removal is gated on a real archiving success — layers directly on top of the refactoring, feeding the corrected marker state intoshouldCheckEmptyWalArchive.@mnencia — if you're already planning to address this issue as a follow-up to #11216, happy to defer or help review (I have a kind+MinIO reproduction with a control cluster I can re-run against any candidate fix). Otherwise I'd like to take it, building on your refactoring. I have a working implementation of the earlier proposal, verified against the reproduction: with the fix, the marker survives on archiver-less clusters (condition reports True with a distinct NotConfigured reason), enabling the plugin later now triggers "Expected empty archive" as designed, and the bootstrap-with-plugin control behaves identically to today. Notably it works with the unmodified released plugin (v0.13.0), and I've confirmed it merges cleanly with #11216 (build and unit tests pass on the combined tree). Before opening a PR, the two open questions from my earlier comment (whether the reason change for archiver-less clusters is acceptable, and whether removing an archiver should re-arm the check) are the main design points I'd like a nod on.
ichibahn commentedon Jul 28, 2026
#11216, cloudnative-pg/plugin-barman-cloud#1009 and cloudnative-pg/cnpg-i#353 have all landed since my last comment, so I re-ran my reproduction against
mainat8046d243d(which includes #11216) to see where that leaves this issue.On my side the scenario still reproduces: a cluster created without an archiver reports
ContinuousArchiving=True(ContinuousArchivingSuccess)from the no-op archiving path, and the.check-empty-wal-archivemarker was already gone 23 seconds after the cluster became Ready. Adding the barman-cloud plugin afterwards, with a destination that alreadycontained an object, the sidecar log shows no check at all and two 16 MiB WAL segments went straight into that destination. The control cluster — same final spec, plugin present at bootstrap — failed with
Expected empty archiveas designed. That matches what I'd expect from reading the refactoring:shouldCheckEmptyWalArchivecentralises the decision, but it still evaluates the annotation and the marker, and the marker's lifecycle inreconcileCheckWalArchiveFileis unchanged.I've opened PR #11258 with the approach from my earlier comments, rebased on top of #11216. It's operator-only — verified against the released plugin v0.13.0 with no plugin changes — and in the same reproduction the check now fires 12 seconds after the sidecar is injected, with nothing written into the non-empty destination. I also added a control for the case I was most unsure about: with an empty destination the check still runs exactly once, passes, and the marker is consumed as before, so the check doesn't start repeating.
The two design questions from my earlier comment are still the parts I'd most like your read on: whether the distinct condition reason for archiver-less clusters is acceptable, and whether removing an archiver should re-arm the check. I answered both in the PR description so there's something concrete to react to, but I'm happy to go the other way on either — and if you'd rather fold this into your own follow-up to #11216, that's fine too, just tell me which is easier to review.
khrisrichardson commentedon Oct 10, 2026
Another consequence of the same no-op archiving path: a "completed" base backup that cannot be restored.
Versions: CloudNativePG 1.30.1 (operator and instance manager),
ghcr.io/cloudnative-pg/postgresql:18.6, in-treebarmanObjectStore; kind.spec.backupand let it write WAL.ContinuousArchiving=True (ContinuousArchivingSuccess)is reported, as described above.spec.backup.barmanObjectStoreand take an immediateBackup.completed, and itsbackup_labelstarts in segment N. But segments up to N are absent from the bucket, even thoughpg_stat_archivercounts them as archived: they were handled by the no-op path before the store was configured.WAL file …000N for server <name> does not exist.So beyond the skipped empty-archive check, the no-op path can leave a backup that is reported complete but unrestorable, with the cluster reporting healthy archiving throughout. We reproduced it repeatedly across our acceptance runs (listing the bucket with the source's own credentials). Our workaround is to create the Cluster only after its object store is ready, so it has
spec.backupfrom bootstrap and never takes the no-op path. We haven't yet checked whether the barman-cloud plugin path (added later) produces the same unrestorable backup, but the marker analysis above suggests the window exists there too. A fix that keepsContinuousArchivingfrom reporting True while no archiver is configured would close both.