carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "context"
5 "errors"
6 "strings"
7 "sync/atomic"
8 "testing"
9 "time"
10
11 "github.com/carlosframework/platform/internal/objstore"
12 "github.com/carlosframework/platform/internal/release"
13)
14
15var errTransientList = errors.New("transient listing failure")
16
17// flakyListStore fails listings under one prefix while failing is set. It
18// embeds the concrete *release.DirStore so PageLister and ModLister stay
19// promoted (see drainFixture).
20type flakyListStore struct {
21 *release.DirStore
22 failing atomic.Bool
23 // dirsPrefix fails ListDirs(prefix) for exactly this prefix.
24 dirsPrefix string
25 // listPrefix fails List/ListPage for keys under this prefix.
26 listPrefix string
27}
28
29func (f *flakyListStore) ListDirs(ctx context.Context, prefix string) ([]string, error) {
30 if f.failing.Load() && f.dirsPrefix != "" && prefix == f.dirsPrefix {
31 return nil, errTransientList
32 }
33 return f.DirStore.ListDirs(ctx, prefix)
34}
35
36func (f *flakyListStore) List(ctx context.Context, prefix string) ([]string, error) {
37 if f.failing.Load() && f.listPrefix != "" && strings.HasPrefix(prefix, f.listPrefix) {
38 return nil, errTransientList
39 }
40 return f.DirStore.List(ctx, prefix)
41}
42
43func (f *flakyListStore) ListPage(ctx context.Context, prefix, startAfter string, limit int) ([]string, error) {
44 if f.failing.Load() && f.listPrefix != "" && strings.HasPrefix(prefix, f.listPrefix) {
45 return nil, errTransientList
46 }
47 return f.DirStore.ListPage(ctx, prefix, startAfter, limit)
48}
49
50func indexed(fx *consoleFixture, acct, app string) bool {
51 for _, ref := range fx.srv.scanQueue.Apps() {
52 if ref.Account == acct && ref.App == app {
53 return true
54 }
55 }
56 return false
57}
58
59// TestRebuildRetriesAfterPartialFailure: a nested listing failure must make
60// rebuildScanQueue return an error (so StartScanDrain retries) while the
61// healthy app is still found, and the retry must find the skipped app.
62func TestRebuildRetriesAfterPartialFailure(t *testing.T) {
63 const badAcct, goodAcct = "acct-bad", "acct-good"
64 cases := []struct {
65 name string
66 setup func(f *flakyListStore)
67 // acct/app of the skipped app, and a healthy app that must still be found
68 skipAcct, skipApp string
69 goodAcct, goodApp string
70 }{
71 {
72 name: "account app-list failure",
73 setup: func(f *flakyListStore) {
74 f.dirsPrefix = "apps/" + badAcct + "/"
75 },
76 skipAcct: badAcct, skipApp: "shop",
77 goodAcct: goodAcct, goodApp: "shop",
78 },
79 {
80 name: "per-app queue-list failure",
81 setup: func(f *flakyListStore) {
82 f.listPrefix = objstore.QueuePrefix(badAcct, "shop")
83 },
84 skipAcct: badAcct, skipApp: "shop",
85 goodAcct: badAcct, goodApp: "blog",
86 },
87 }
88 for _, tc := range cases {
89 t.Run(tc.name, func(t *testing.T) {
90 ctx := context.Background()
91 fx, _, _ := drainFixture(t)
92 flaky := &flakyListStore{DirStore: fx.rel.(*release.DirStore)}
93 tc.setup(flaky)
94 fx.srv.cfg.Releases = flaky
95
96 // Names sort so the failing account/app comes first.
97 enableStore(t, fx, tc.skipAcct, tc.skipApp, true)
98 enableStore(t, fx, tc.goodAcct, tc.goodApp, true)
99 at := time.Now().Add(-time.Minute)
100 enqueueStranded(t, fx, tc.skipAcct, tc.skipApp, "uploads/skipped.pdf", at)
101 enqueueStranded(t, fx, tc.goodAcct, tc.goodApp, "uploads/healthy.pdf", at)
102
103 flaky.failing.Store(true)
104 err := fx.srv.rebuildScanQueue(ctx)
105 if !errors.Is(err, errTransientList) {
106 t.Fatalf("first rebuild err = %v, want the transient listing failure so the drain retries", err)
107 }
108 if !indexed(fx, tc.goodAcct, tc.goodApp) {
109 t.Errorf("healthy app %s/%s not indexed: one failure cost the others their queue", tc.goodAcct, tc.goodApp)
110 }
111 if indexed(fx, tc.skipAcct, tc.skipApp) {
112 t.Errorf("skipped app %s/%s indexed despite its listing failing", tc.skipAcct, tc.skipApp)
113 }
114 // Nothing was deleted or lost by the failed pass.
115 if n := queueLen(t, fx, tc.skipAcct, tc.skipApp); n != 1 {
116 t.Errorf("skipped app has %d queue item(s) after the failed pass, want 1", n)
117 }
118 if n := queueLen(t, fx, tc.goodAcct, tc.goodApp); n != 1 {
119 t.Errorf("healthy app has %d queue item(s) after the pass, want 1", n)
120 }
121
122 flaky.failing.Store(false)
123 if err := fx.srv.rebuildScanQueue(ctx); err != nil {
124 t.Fatalf("second rebuild after the failure cleared: %v", err)
125 }
126 if !indexed(fx, tc.skipAcct, tc.skipApp) {
127 t.Errorf("retry did not find the previously skipped app %s/%s", tc.skipAcct, tc.skipApp)
128 }
129 if n := queueLen(t, fx, tc.skipAcct, tc.skipApp); n != 1 {
130 t.Errorf("skipped app has %d queue item(s) after the retry, want 1", n)
131 }
132 if d := fx.srv.ScanQueueDepth(); d != 2 {
133 t.Errorf("index depth %d after the retry, want 2", d)
134 }
135 })
136 }
137}
138