Report
In operator 3.0.0, every backup schedule defined in spec.backups.pgbackrest.repos[].schedules is executed by two independent mechanisms simultaneously:
- The upstream (CrunchyData) reconciler creates a Kubernetes CronJob (
<cluster>-repo1-<type>) whose pod runs pgbackrest backup directly (internal/controller/postgrescluster/pgbackrest.go, ~L3184).
- The Percona layer registers an in-process cron for the same schedule (
percona/controller/pgcluster/schedule.go — reconcileScheduledBackup → createScheduledBackup) which creates a PerconaPGBackup CR (GenerateName: <cronjob-name>-, no annotations) ~30s later.
The PerconaPGBackup controller never "adopts" the CronJob's job: findBackupJob() (percona/controller/pgbackup/controller.go, ~L702-739) only matches manual-type jobs annotated with the CR's name. So reconciling the scheduler-created CR calls startBackup() and launches a second, manual-type backup job (<cluster>-backup-xxxx) for the same slot. Both pgbackrest processes race for the stanza's backup lock on the repo host:
ERROR: [050]: unable to acquire lock on file '/tmp/pgbackrest/db-backup-1.lock': Resource temporarily unavailable
HINT: is another pgBackRest process running?
Additionally, reconcileBackupJob() (percona/controller/pgcluster/backup.go, ~L187) creates a third object — a tracking PerconaPGBackup CR for the CronJob's job (annotated with pgv2.percona.com/pgbackrest-backup-job-name) — so two CRs accumulate per schedule slot.
More about the problem
The duplicate execution cascades into a self-perpetuating failure loop:
- Whichever process loses the lock race fails with exit 50; scheduled CronJob jobs intermittently end
BackoffLimitExceeded (slots missing their backup, retries succeeding at odd times).
- When the duplicate manual job's pods exhaust retries and the job is removed, its CR remains in
Starting forever — the BackupStarting branch requeues "Waiting for backup to start" every 5s with no timeout (percona/controller/pgbackup/controller.go, ~L240).
- The stuck CR holds the controller's lease (
handleLease), freezing reconciliation of all pg-backup CRs for the cluster. In our production cluster, 936 unreconciled CRs accumulated over ~6 weeks behind a CR stuck in Starting.
- Deleting the stuck CR unfreezes the queue; the backlog then fires manual backups at arbitrary times, colliding with the scheduled CronJobs again and producing a new stuck CR. We reproduced and recorded this live.
Operator log while stuck (repeats every 5s, indefinitely):
INFO Waiting for backup to start {"controller": "perconapgbackup", "controllerKind": "PerconaPGBackup", "name": "postgres-ash-vega-prod-repo1-incr-7nskz", ...}
ps on the repo host during a slot — the CR-driven backup (note the --annotation) holding the lock while the scheduled CronJob's pods crash-loop with exit 50:
pgbackrest backup --stanza=db --repo=1 --type=incr --annotation=percona.com/backup-name=postgres-ash-vega-prod-repo1-incr-9qg9g
Also: stuck manual jobs remain Terminating indefinitely holding the internal.percona.com/keep-job finalizer after their CR is deleted.
Steps to reproduce
- Deploy a PerconaPGCluster with operator 3.0.0 and
backups.pgbackrest.repos[0].schedules set (e.g. incremental 20 0,3,9,12,15,18,21 * * *, S3 repo with dedicated repo host).
- Watch a schedule slot: a CronJob job runs pgbackrest directly AND ~30s later an unannotated PerconaPGBackup CR is created; its reconcile spawns a
<cluster>-backup-* job → lock collision on the repo host.
- Let the manual job fail past its backoff limit (easy when the backup is long enough to overlap): its CR stays in
Starting forever and all subsequent pg-backup CRs stop being reconciled (empty state).
Versions
- Operator: docker.io/percona/percona-postgresql-operator:3.0.0
- Database: PostgreSQL 18, percona/percona-pgbackrest:2.58.0-2 (pgBackRest 2.58.0)
- Kubernetes: v1.36.1+rke2r2 (RKE2, bare metal)
- Backup storage: S3-compatible object storage (path-style), dedicated repo host, archive-async=y
Anything else?
Production workaround we deployed: a cron "janitor" that (a) deletes scheduler-created CRs — no pgv2.percona.com/pgbackrest-backup-job-name annotation and name matching <cluster>-repo1-(incr|full)-[a-z0-9]{5} — before they spawn duplicate jobs, and (b) strips the orphaned keep-job finalizer from Terminating jobs. With the upstream CronJob as the single executor, every slot completes cleanly.
Suggested fixes:
- Gate the upstream CronJob creation when the in-process scheduler is active (or vice versa), or make the scheduler-created CR adopt the CronJob's running job in
findBackupJob();
- Add a timeout/failure transition to the
BackupStarting wait so a CR whose job disappeared cannot hold the lease forever.
Report
In operator 3.0.0, every backup schedule defined in
spec.backups.pgbackrest.repos[].schedulesis executed by two independent mechanisms simultaneously:<cluster>-repo1-<type>) whose pod runspgbackrest backupdirectly (internal/controller/postgrescluster/pgbackrest.go, ~L3184).percona/controller/pgcluster/schedule.go—reconcileScheduledBackup→createScheduledBackup) which creates aPerconaPGBackupCR (GenerateName: <cronjob-name>-, no annotations) ~30s later.The PerconaPGBackup controller never "adopts" the CronJob's job:
findBackupJob()(percona/controller/pgbackup/controller.go, ~L702-739) only matches manual-type jobs annotated with the CR's name. So reconciling the scheduler-created CR callsstartBackup()and launches a second, manual-type backup job (<cluster>-backup-xxxx) for the same slot. Both pgbackrest processes race for the stanza's backup lock on the repo host:Additionally,
reconcileBackupJob()(percona/controller/pgcluster/backup.go, ~L187) creates a third object — a tracking PerconaPGBackup CR for the CronJob's job (annotated withpgv2.percona.com/pgbackrest-backup-job-name) — so two CRs accumulate per schedule slot.More about the problem
The duplicate execution cascades into a self-perpetuating failure loop:
BackoffLimitExceeded(slots missing their backup, retries succeeding at odd times).Startingforever — theBackupStartingbranch requeues"Waiting for backup to start"every 5s with no timeout (percona/controller/pgbackup/controller.go, ~L240).handleLease), freezing reconciliation of all pg-backup CRs for the cluster. In our production cluster, 936 unreconciled CRs accumulated over ~6 weeks behind a CR stuck inStarting.Operator log while stuck (repeats every 5s, indefinitely):
pson the repo host during a slot — the CR-driven backup (note the--annotation) holding the lock while the scheduled CronJob's pods crash-loop with exit 50:Also: stuck manual jobs remain
Terminatingindefinitely holding theinternal.percona.com/keep-jobfinalizer after their CR is deleted.Steps to reproduce
backups.pgbackrest.repos[0].schedulesset (e.g. incremental20 0,3,9,12,15,18,21 * * *, S3 repo with dedicated repo host).<cluster>-backup-*job → lock collision on the repo host.Startingforever and all subsequent pg-backup CRs stop being reconciled (empty state).Versions
Anything else?
Production workaround we deployed: a cron "janitor" that (a) deletes scheduler-created CRs — no
pgv2.percona.com/pgbackrest-backup-job-nameannotation and name matching<cluster>-repo1-(incr|full)-[a-z0-9]{5}— before they spawn duplicate jobs, and (b) strips the orphanedkeep-jobfinalizer from Terminating jobs. With the upstream CronJob as the single executor, every slot completes cleanly.Suggested fixes:
findBackupJob();BackupStartingwait so a CR whose job disappeared cannot hold the lease forever.