|
5 | 5 | "context" |
6 | 6 | "errors" |
7 | 7 | "fmt" |
| 8 | + "slices" |
8 | 9 | "strings" |
9 | 10 | "testing" |
10 | 11 | "time" |
@@ -2207,7 +2208,7 @@ func TestEnsureCNPGBackup_UsesPluginMethod(t *testing.T) { |
2207 | 2208 | c := newCNPGStrategyTestClient(t, j) |
2208 | 2209 | r := &BackupJobReconciler{Client: c} |
2209 | 2210 |
|
2210 | | - bk, err := r.ensureCNPGBackup(context.Background(), j, "postgres-app", cnpgDestination{DestinationPath: "s3://bucket/"}) |
| 2211 | + bk, err := r.ensureCNPGBackup(context.Background(), j, "postgres-app", nil, cnpgDestination{DestinationPath: "s3://bucket/"}) |
2211 | 2212 | if err != nil { |
2212 | 2213 | t.Fatalf("ensureCNPGBackup: %v", err) |
2213 | 2214 | } |
@@ -2567,8 +2568,8 @@ func (f *cnpgRolloutFixture) seedOtherBackupJob(name string, phase backupsv1alph |
2567 | 2568 | func TestReconcileCNPG_RunningBackupDoesNotReapplyObjectStore(t *testing.T) { |
2568 | 2569 | f := newAttachedObjectStoreFixture(t, time.Now(), "s3://other/") |
2569 | 2570 | f.seedCNPGBackup("bj-cnpg", "bj", rolloutCluster, "running") |
2570 | | - // Two jobs can pass the queue together, since it reads the cache; once |
2571 | | - // both have a Backup neither may wait on the other. |
| 2571 | + // Two jobs can still pass the queue together, across a leader handover; |
| 2572 | + // once both have a Backup neither may wait on the other. |
2572 | 2573 | f.seedOtherBackupJob("bj-other", backupsv1alpha1.BackupJobPhaseRunning) |
2573 | 2574 | f.seedCNPGBackup("bj-other-cnpg", "bj-other", rolloutCluster, "running") |
2574 | 2575 | writes := 0 |
@@ -5655,3 +5656,126 @@ func TestReconcileCNPG_ArtifactOfABackupWithoutARecord(t *testing.T) { |
5655 | 5656 | t.Fatalf("artifact = %+v, want one built from the strategy", artifact) |
5656 | 5657 | } |
5657 | 5658 | } |
| 5659 | + |
| 5660 | +// cacheLaggingBehind makes the reconciler's cached client miss the named |
| 5661 | +// cnpg.io/Backups, as an informer does right after they are created, while |
| 5662 | +// its uncached reader sees them. |
| 5663 | +func (f *cnpgRolloutFixture) cacheLaggingBehind(hidden ...string) { |
| 5664 | + f.r.APIReader = f.c |
| 5665 | + f.r.Client = interceptor.NewClient(f.c.(client.WithWatch), interceptor.Funcs{ |
| 5666 | + List: func(ctx context.Context, cl client.WithWatch, list client.ObjectList, opts ...client.ListOption) error { |
| 5667 | + if err := cl.List(ctx, list, opts...); err != nil { |
| 5668 | + return err |
| 5669 | + } |
| 5670 | + if backups, ok := list.(*cnpgtypes.BackupList); ok { |
| 5671 | + backups.Items = slices.DeleteFunc(backups.Items, func(b cnpgtypes.Backup) bool { |
| 5672 | + return slices.Contains(hidden, b.Name) |
| 5673 | + }) |
| 5674 | + } |
| 5675 | + return nil |
| 5676 | + }, |
| 5677 | + }) |
| 5678 | +} |
| 5679 | + |
| 5680 | +// Reconciles run one at a time, so a Backup created by one pass exists by |
| 5681 | +// the next; the queue has to see it even when the cache does not yet, or |
| 5682 | +// the next job rewrites the destination under it and the record on that |
| 5683 | +// Backup no longer names where its WAL goes. |
| 5684 | +func TestReconcileCNPG_QueueSeesABackupTheCacheHasNotCaught(t *testing.T) { |
| 5685 | + f := newAttachedObjectStoreFixture(t, time.Now(), "s3://other/") |
| 5686 | + f.seedOtherBackupJob("bj-other", backupsv1alpha1.BackupJobPhaseRunning) |
| 5687 | + f.seedCNPGBackup("bj-other-cnpg", "bj-other", rolloutCluster, "running") |
| 5688 | + f.cacheLaggingBehind("bj-other-cnpg") |
| 5689 | + |
| 5690 | + j, backups := f.pass() |
| 5691 | + if ready := apimeta.FindStatusCondition(j.Status.Conditions, "Ready"); ready == nil || ready.Reason != "WaitingForOtherBackup" { |
| 5692 | + t.Fatalf("Ready = %+v, want WaitingForOtherBackup", ready) |
| 5693 | + } |
| 5694 | + if got := f.liveDestination(); got != "s3://other/" { |
| 5695 | + t.Fatalf("destination = %q, rewritten under the other job's Backup", got) |
| 5696 | + } |
| 5697 | + if len(backups) != 1 { |
| 5698 | + t.Fatalf("got %d cnpg.io/Backups, want only the other job's", len(backups)) |
| 5699 | + } |
| 5700 | +} |
| 5701 | + |
| 5702 | +// The job's own Backup, missing from the cache right after it was created, |
| 5703 | +// is neither created again nor written under. |
| 5704 | +func TestReconcileCNPG_OwnBackupTheCacheHasNotCaught(t *testing.T) { |
| 5705 | + f := newAttachedObjectStoreFixture(t, time.Now(), "s3://other/") |
| 5706 | + f.seedCNPGBackup("bj-cnpg", "bj", rolloutCluster, "running") |
| 5707 | + f.cacheLaggingBehind("bj-cnpg") |
| 5708 | + |
| 5709 | + _, backups := f.pass() |
| 5710 | + if len(backups) != 1 { |
| 5711 | + t.Fatalf("got %d cnpg.io/Backups, want the job's one", len(backups)) |
| 5712 | + } |
| 5713 | + if got := f.liveDestination(); got != "s3://other/" { |
| 5714 | + t.Fatalf("destination = %q, rewritten under the job's own Backup", got) |
| 5715 | + } |
| 5716 | +} |
| 5717 | + |
| 5718 | +// failingBackupLists makes the uncached reader fail the cnpg.io/Backup lists |
| 5719 | +// for which fail, given whether the list selects the job's own Backup by |
| 5720 | +// label and how many such lists came before, returns true. |
| 5721 | +func (f *cnpgRolloutFixture) failingBackupLists(fail func(own bool, n int) bool) { |
| 5722 | + counts := map[bool]int{} |
| 5723 | + f.r.APIReader = interceptor.NewClient(f.c.(client.WithWatch), interceptor.Funcs{ |
| 5724 | + List: func(ctx context.Context, cl client.WithWatch, list client.ObjectList, opts ...client.ListOption) error { |
| 5725 | + if _, ok := list.(*cnpgtypes.BackupList); ok { |
| 5726 | + o := &client.ListOptions{} |
| 5727 | + o.ApplyOptions(opts) |
| 5728 | + own := o.LabelSelector != nil && !o.LabelSelector.Empty() |
| 5729 | + counts[own]++ |
| 5730 | + if fail(own, counts[own]) { |
| 5731 | + return apierrors.NewServiceUnavailable("apiserver unavailable") |
| 5732 | + } |
| 5733 | + } |
| 5734 | + return cl.List(ctx, list, opts...) |
| 5735 | + }, |
| 5736 | + }) |
| 5737 | +} |
| 5738 | + |
| 5739 | +// A failed apiserver read of the Backups is retried, not taken as the job |
| 5740 | +// failing: nothing has been written yet that the retry would not redo. |
| 5741 | +func TestReconcileCNPG_BackupListErrorIsReturned(t *testing.T) { |
| 5742 | + for name, own := range map[string]bool{"own Backup": true, "queue": false} { |
| 5743 | + t.Run(name, func(t *testing.T) { |
| 5744 | + f := newAttachedObjectStoreFixture(t, time.Now(), "s3://other/") |
| 5745 | + f.failingBackupLists(func(o bool, _ int) bool { return o == own }) |
| 5746 | + j := &backupsv1alpha1.BackupJob{} |
| 5747 | + if err := f.c.Get(context.Background(), client.ObjectKey{Namespace: rolloutNS, Name: "bj"}, j); err != nil { |
| 5748 | + t.Fatalf("get BackupJob: %v", err) |
| 5749 | + } |
| 5750 | + if _, err := f.r.reconcileCNPG(context.Background(), j, f.resolved); err == nil { |
| 5751 | + t.Fatalf("reconcileCNPG swallowed the Backup list error") |
| 5752 | + } |
| 5753 | + got := &backupsv1alpha1.BackupJob{} |
| 5754 | + if err := f.c.Get(context.Background(), client.ObjectKey{Namespace: rolloutNS, Name: "bj"}, got); err != nil { |
| 5755 | + t.Fatalf("get BackupJob: %v", err) |
| 5756 | + } |
| 5757 | + if got.Status.Phase == backupsv1alpha1.BackupJobPhaseFailed { |
| 5758 | + t.Fatalf("BackupJob failed on a Backup list error: %s", got.Status.Message) |
| 5759 | + } |
| 5760 | + if dest := f.liveDestination(); dest != "s3://other/" { |
| 5761 | + t.Fatalf("destination = %q, written although the Backups could not be read", dest) |
| 5762 | + } |
| 5763 | + }) |
| 5764 | + } |
| 5765 | +} |
| 5766 | + |
| 5767 | +// The pass that writes the destination looks its own Backup up once, at the |
| 5768 | +// start: a second lookup before the create could only fail, after the write, |
| 5769 | +// and fail the job with it. |
| 5770 | +func TestReconcileCNPG_LooksUpItsOwnBackupOncePerPass(t *testing.T) { |
| 5771 | + f := newAttachedObjectStoreFixture(t, time.Now(), "s3://other/") |
| 5772 | + f.failingBackupLists(func(own bool, n int) bool { return own && n > 1 }) |
| 5773 | + |
| 5774 | + j, backups := f.pass() |
| 5775 | + if j.Status.Phase == backupsv1alpha1.BackupJobPhaseFailed { |
| 5776 | + t.Fatalf("BackupJob failed: %s", j.Status.Message) |
| 5777 | + } |
| 5778 | + if len(backups) != 1 { |
| 5779 | + t.Fatalf("got %d cnpg.io/Backups, want 1", len(backups)) |
| 5780 | + } |
| 5781 | +} |
0 commit comments