| 1 | package dbreads |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "net" |
| 7 | "strings" |
| 8 | "sync/atomic" |
| 9 | "testing" |
| 10 | "time" |
| 11 | |
| 12 | "github.com/benbjohnson/litestream" |
| 13 | |
| 14 | "github.com/carlosframework/platform/internal/release" |
| 15 | ) |
| 16 | |
| 17 | // countingStore counts reads of request objects and lists. |
| 18 | type countingStore struct { |
| 19 | release.Store |
| 20 | requestGets, lists atomic.Int64 |
| 21 | } |
| 22 | |
| 23 | func (s *countingStore) Get(ctx context.Context, key string) ([]byte, string, error) { |
| 24 | if strings.HasSuffix(key, "/request.json") { |
| 25 | s.requestGets.Add(1) |
| 26 | } |
| 27 | return s.Store.Get(ctx, key) |
| 28 | } |
| 29 | |
| 30 | func (s *countingStore) List(ctx context.Context, prefix string) ([]string, error) { |
| 31 | s.lists.Add(1) |
| 32 | return s.Store.List(ctx, prefix) |
| 33 | } |
| 34 | |
| 35 | // Review Focus 2: a request issued at 10:59:50 is listed at 11:00:20 under |
| 36 | // the previous hour's prefix, and picked. |
| 37 | func TestPassFindsAnEntryFromThePreviousHour(t *testing.T) { |
| 38 | t.Parallel() |
| 39 | fx := newFixture(t, 1) |
| 40 | fx.now = time.Date(2026, 10, 22, 10, 59, 50, 0, time.UTC) |
| 41 | a, _ := fx.agent("uk-1", func(c *AgentConfig, _ *inProcWorkers) { |
| 42 | c.Now = func() time.Time { return time.Date(2026, 10, 22, 11, 0, 20, 0, time.UTC) } |
| 43 | }) |
| 44 | req := fx.request("SELECT count(*) FROM t", "") |
| 45 | fx.submit(req) |
| 46 | a.Pass(context.Background()) |
| 47 | a.Wait() |
| 48 | if _, _, r := fx.answer(req.ID); r.State != StateDone { |
| 49 | t.Fatalf("%+v", r) |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | func TestPassSkipsAnOutOfWindowEntryWithoutReadingIt(t *testing.T) { |
| 54 | t.Parallel() |
| 55 | fx := newFixture(t, 0) |
| 56 | cs := &countingStore{Store: fx.store} |
| 57 | fx.now = time.Now().UTC().Add(-3 * time.Minute).Truncate(time.Second) |
| 58 | fx.submit(fx.request("SELECT 1", "")) |
| 59 | a, _ := fx.agent("uk-1", func(c *AgentConfig, _ *inProcWorkers) { c.Store = cs }) |
| 60 | a.Pass(context.Background()) |
| 61 | if cs.requestGets.Load() != 0 { |
| 62 | t.Fatal("an out-of-window request was read") |
| 63 | } |
| 64 | } |
| 65 | |
| 66 | // T4, the box's half: a forged queue entry and an unsigned request are |
| 67 | // refused, decided, and logged once. |
| 68 | func TestPassRefusesForgedEntriesAndUnsignedRequests(t *testing.T) { |
| 69 | t.Parallel() |
| 70 | fx := newFixture(t, 1) |
| 71 | var logged atomic.Int64 |
| 72 | a, w := fx.agent("uk-1", func(c *AgentConfig, _ *inProcWorkers) { |
| 73 | c.Logf = func(format string, args ...any) { |
| 74 | if strings.Contains(format, "refused the request") { |
| 75 | logged.Add(1) |
| 76 | } |
| 77 | } |
| 78 | }) |
| 79 | forged := fx.request("SELECT 1", "") |
| 80 | wire, _ := Sign(release.RootSigner(fx.rootPriv), forged) |
| 81 | fx.store.Put(context.Background(), RequestKey(forged.ID), wire, "") |
| 82 | issued, _ := forged.Issued() |
| 83 | fx.store.Put(context.Background(), QueueKey("uk", issued.Add(-time.Second), forged.ID), nil, "") |
| 84 | unsigned := fx.request("SELECT 1", "") |
| 85 | fx.store.Put(context.Background(), RequestKey(unsigned.ID), []byte(`{"payload":"e30=","sig":""}`), "") |
| 86 | fx.store.Put(context.Background(), QueueKey("uk", issued, unsigned.ID), nil, "") |
| 87 | a.Pass(context.Background()) |
| 88 | a.Pass(context.Background()) |
| 89 | a.Wait() |
| 90 | for _, id := range []string{forged.ID, unsigned.ID} { |
| 91 | if _, _, err := fx.store.Get(context.Background(), PickedKey(id)); err == nil { |
| 92 | t.Errorf("%s was picked", id) |
| 93 | } |
| 94 | } |
| 95 | if logged.Load() != 2 || w.dials.Load() != 0 { |
| 96 | t.Fatalf("logged %d refusals, dialed %d workers", logged.Load(), w.dials.Load()) |
| 97 | } |
| 98 | } |
| 99 | |
| 100 | // T4 and codex 5: the same envelope offered again, before and after the |
| 101 | // agent restarts, is never run twice: picked holds. |
| 102 | func TestReplayIsRefusedByPickedAcrossARestart(t *testing.T) { |
| 103 | t.Parallel() |
| 104 | fx := newFixture(t, 1) |
| 105 | a, _ := fx.agent("uk-1") |
| 106 | req := fx.request("SELECT count(*) FROM t", "") |
| 107 | fx.submit(req) |
| 108 | a.Pass(context.Background()) |
| 109 | a.Wait() |
| 110 | a.Pass(context.Background()) // the same agent again |
| 111 | restarted, w := fx.agent("uk-1") |
| 112 | restarted.Pass(context.Background()) |
| 113 | restarted.Wait() |
| 114 | if w.dials.Load() != 0 { |
| 115 | t.Fatal("a restarted agent ran a read it had already picked") |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | func TestTwoAgentsServeEachReadOnce(t *testing.T) { |
| 120 | t.Parallel() |
| 121 | fx := newFixture(t, 1) |
| 122 | a, wa := fx.agent("uk-1") |
| 123 | b, wb := fx.agent("uk-2") |
| 124 | var ids []string |
| 125 | for i := 0; i < 10; i++ { |
| 126 | req := fx.request("SELECT count(*) FROM t", "") |
| 127 | fx.submit(req) |
| 128 | ids = append(ids, req.ID) |
| 129 | } |
| 130 | for round := 0; round < 20; round++ { |
| 131 | done := make(chan struct{}, 2) |
| 132 | go func() { a.Pass(context.Background()); done <- struct{}{} }() |
| 133 | go func() { b.Pass(context.Background()); done <- struct{}{} }() |
| 134 | <-done |
| 135 | <-done |
| 136 | a.Wait() |
| 137 | b.Wait() |
| 138 | } |
| 139 | for _, id := range ids { |
| 140 | if _, _, r := fx.answer(id); r.State != StateDone { |
| 141 | t.Fatalf("%s: %+v", id, r) |
| 142 | } |
| 143 | } |
| 144 | if got := wa.dials.Load() + wb.dials.Load(); got != int64(len(ids)) { |
| 145 | t.Fatalf("%d worker connections for %d reads", got, len(ids)) |
| 146 | } |
| 147 | } |
| 148 | |
| 149 | // Plan review finding 14 (spec §7.1, D2): a draining box beside a healthy |
| 150 | // one never wins a read, including one that starts draining during its |
| 151 | // jitter. Mutation: drop the Draining check after the jitter; the draining |
| 152 | // agent picks every read and this test is red. (The check before admission |
| 153 | // only spares a reservation; the one after the jitter is the gate.) |
| 154 | func TestADrainingBoxDoesNotPick(t *testing.T) { |
| 155 | t.Parallel() |
| 156 | fx := newFixture(t, 1) |
| 157 | var flip atomic.Bool |
| 158 | drain, dw := fx.agent("uk-drain", func(c *AgentConfig, _ *inProcWorkers) { |
| 159 | c.Draining = flip.Load |
| 160 | c.Jitter = func() time.Duration { flip.Store(true); return time.Millisecond } |
| 161 | }) |
| 162 | healthy, _ := fx.agent("uk-1") |
| 163 | for i := 0; i < 5; i++ { |
| 164 | req := fx.request("SELECT 1", "") |
| 165 | fx.submit(req) |
| 166 | drain.Pass(context.Background()) // first: it passes the first check, then drains in its jitter |
| 167 | drain.Wait() |
| 168 | healthy.Pass(context.Background()) |
| 169 | healthy.Wait() |
| 170 | if _, _, r := fx.answer(req.ID); r.Box != "uk-1" { |
| 171 | t.Fatalf("read %d served by %s", i, r.Box) |
| 172 | } |
| 173 | flip.Store(false) |
| 174 | } |
| 175 | if dw.dials.Load() != 0 { |
| 176 | t.Fatal("the draining box started a worker") |
| 177 | } |
| 178 | } |
| 179 | |
| 180 | // Finding 11's box half: a request already cancelled is never picked. |
| 181 | // Mutation: drop the cancel read in consider; the agent picks it. |
| 182 | func TestPassSkipsACancelledRequest(t *testing.T) { |
| 183 | t.Parallel() |
| 184 | fx := newFixture(t, 1) |
| 185 | a, w := fx.agent("uk-1") |
| 186 | req := fx.request("SELECT 1", "") |
| 187 | fx.submit(req) |
| 188 | fx.store.Put(context.Background(), CancelKey(req.ID), nil, "") |
| 189 | a.Pass(context.Background()) |
| 190 | a.Wait() |
| 191 | if _, _, err := fx.store.Get(context.Background(), PickedKey(req.ID)); err == nil || w.dials.Load() != 0 { |
| 192 | t.Fatal("a cancelled request was picked") |
| 193 | } |
| 194 | } |
| 195 | |
| 196 | func TestPassNeedsTheCapability(t *testing.T) { |
| 197 | t.Parallel() |
| 198 | fx := newFixture(t, 0) |
| 199 | cs := &countingStore{Store: fx.store} |
| 200 | a, _ := fx.agent("uk-1", func(c *AgentConfig, _ *inProcWorkers) { c.Store = cs }) |
| 201 | a.probeOK.Store(false) |
| 202 | a.Pass(context.Background()) |
| 203 | if cs.lists.Load() != 0 { |
| 204 | t.Fatal("an incapable box listed the queue") |
| 205 | } |
| 206 | } |
| 207 | |
| 208 | func TestProbe(t *testing.T) { |
| 209 | t.Parallel() |
| 210 | for name, c := range map[string]struct { |
| 211 | self func() HelloReply |
| 212 | dial error |
| 213 | want bool |
| 214 | }{ |
| 215 | "a sandboxed worker": {func() HelloReply { return HelloReply{V: ProtocolVersion, Interfaces: []string{"lo"}} }, nil, true}, |
| 216 | "another version": {func() HelloReply { return HelloReply{V: 2, Interfaces: []string{"lo"}} }, nil, false}, |
| 217 | "a network": {func() HelloReply { return HelloReply{V: 1, Interfaces: []string{"lo", "eth0"}} }, nil, false}, |
| 218 | "a visible /data": {func() HelloReply { return HelloReply{V: 1, Interfaces: []string{"lo"}, DataEntries: 3} }, nil, false}, |
| 219 | "a readable host.env": {func() HelloReply { return HelloReply{V: 1, Interfaces: []string{"lo"}, EtcReadable: true} }, nil, false}, |
| 220 | "an observation that failed": {func() HelloReply { |
| 221 | return HelloReply{V: 1, Errors: []string{"interfaces: permission denied"}} |
| 222 | }, nil, false}, |
| 223 | "no service": {nil, errors.New("connection refused"), false}, |
| 224 | } { |
| 225 | t.Run(name, func(t *testing.T) { |
| 226 | t.Parallel() |
| 227 | fx := newFixture(t, 0) |
| 228 | a, _ := fx.agent("uk-1", func(cfg *AgentConfig, w *inProcWorkers) { w.opt.Self = c.self }) |
| 229 | if c.dial != nil { |
| 230 | a.cfg.DialWorker = func(context.Context) (net.Conn, error) { return nil, c.dial } |
| 231 | } |
| 232 | a.probeOK.Store(!c.want) |
| 233 | a.Probe(context.Background()) |
| 234 | if a.Capable() != c.want { |
| 235 | t.Fatalf("Capable = %v", a.Capable()) |
| 236 | } |
| 237 | }) |
| 238 | } |
| 239 | } |
| 240 | |
| 241 | // Review Focus 5 and spec §7.1: a probe beside an orphan, or with no unit |
| 242 | // listing, is skipped and the last result stands. |
| 243 | func TestProbeIsSkippedWhileAdmissionIsBlocked(t *testing.T) { |
| 244 | t.Parallel() |
| 245 | for _, last := range []bool{true, false} { |
| 246 | fx := newFixture(t, 0) |
| 247 | a, _ := fx.agent("uk-1", func(c *AgentConfig, _ *inProcWorkers) { |
| 248 | c.Units = func(context.Context) (int, error) { return 1, nil } // an orphan |
| 249 | }) |
| 250 | a.probeOK.Store(last) |
| 251 | a.Probe(context.Background()) |
| 252 | if a.Capable() != last { |
| 253 | t.Fatalf("a skipped probe changed the capability from %v", last) |
| 254 | } |
| 255 | } |
| 256 | } |
| 257 | |
| 258 | // T13, the unit half: a box running two reads keeps the capability; its |
| 259 | // probe gets the third connection. |
| 260 | func TestProbeOnTheThirdConnection(t *testing.T) { |
| 261 | t.Parallel() |
| 262 | fx := newFixture(t, 1) |
| 263 | hang := make(chan struct{}) |
| 264 | a, w := fx.agent("uk-1", func(c *AgentConfig, w *inProcWorkers) { |
| 265 | w.opt.Restore = func(ctx context.Context, _ litestream.ReplicaClient, _ string, _ uint64) error { |
| 266 | <-hang |
| 267 | return errors.New("released") |
| 268 | } |
| 269 | }) |
| 270 | for i := 0; i < 2; i++ { |
| 271 | fx.submit(fx.request("SELECT 1", "")) |
| 272 | } |
| 273 | a.Pass(context.Background()) |
| 274 | deadline := time.Now().Add(5 * time.Second) |
| 275 | for w.live.Load() < 2 && time.Now().Before(deadline) { |
| 276 | time.Sleep(10 * time.Millisecond) |
| 277 | } |
| 278 | a.Probe(context.Background()) |
| 279 | if !a.Capable() { |
| 280 | t.Fatal("a busy box lost the capability") |
| 281 | } |
| 282 | close(hang) |
| 283 | a.Wait() |
| 284 | } |
| 285 | |
| 286 | func TestRunPassesOnANudge(t *testing.T) { |
| 287 | t.Parallel() |
| 288 | fx := newFixture(t, 1) |
| 289 | // The start probe's worker unit lingers as systemd's deactivation (and a |
| 290 | // loaded CI box) makes it: main's CI caught the nudge's pass meeting it |
| 291 | // as an orphan, refusing admission, and nothing passing again within |
| 292 | // the test's bound (ListEvery is 30 s). |
| 293 | a, w := fx.agent("uk-1", func(_ *AgentConfig, w *inProcWorkers) { w.linger = 300 * time.Millisecond }) |
| 294 | ctx, cancel := context.WithCancel(context.Background()) |
| 295 | defer cancel() |
| 296 | nudge := make(chan struct{}, 1) |
| 297 | go a.Run(ctx, nudge) |
| 298 | // A pass that meets that unit refuses admission by design (spec §5.7) |
| 299 | // and the read waits for a later pass. This test is about the nudge, so |
| 300 | // it nudges once the start probe has run and its unit is gone. |
| 301 | ready := time.Now().Add(10 * time.Second) |
| 302 | for !(w.dials.Load() >= 1 && w.live.Load() == 0 && a.Capable()) { |
| 303 | if time.Now().After(ready) { |
| 304 | t.Fatal("the start probe never finished") |
| 305 | } |
| 306 | time.Sleep(10 * time.Millisecond) |
| 307 | } |
| 308 | req := fx.request("SELECT count(*) FROM t", "") |
| 309 | fx.submit(req) |
| 310 | nudge <- struct{}{} |
| 311 | deadline := time.Now().Add(10 * time.Second) |
| 312 | for time.Now().Before(deadline) { |
| 313 | if _, _, err := fx.store.Get(context.Background(), ResultKey(req.ID)); err == nil { |
| 314 | return |
| 315 | } |
| 316 | time.Sleep(20 * time.Millisecond) |
| 317 | } |
| 318 | t.Fatal("a nudge did not serve the read") |
| 319 | } |
| 320 | |
| 321 | // Plan review finding 29, through the real loop: a read whose worker never |
| 322 | // starts asks for a probe while that worker's unit is still stopping, so |
| 323 | // admission skips it; the probe stays owed and runs once the unit is gone, |
| 324 | // and the capability is withdrawn well inside ProbeEvery. Mutation: Run |
| 325 | // calls Probe once on probeAfter and drops it when skipped (no retry): |
| 326 | // Capable stays true until ProbeEvery and this test is red. |
| 327 | func TestAProbeSkippedWhileTheUnitLingersRunsOnceItIsGone(t *testing.T) { |
| 328 | t.Parallel() |
| 329 | fx := newFixture(t, 1) |
| 330 | a, w := fx.agent("uk-1", func(_ *AgentConfig, w *inProcWorkers) { w.linger = 700 * time.Millisecond }) |
| 331 | ctx, cancel := context.WithCancel(context.Background()) |
| 332 | nudge := make(chan struct{}, 1) |
| 333 | done := make(chan struct{}) |
| 334 | go func() { a.Run(ctx, nudge); close(done) }() |
| 335 | defer func() { cancel(); <-done }() |
| 336 | until := func(what string, ok func() bool) { |
| 337 | t.Helper() |
| 338 | deadline := time.Now().Add(10 * time.Second) |
| 339 | for !ok() { |
| 340 | if time.Now().After(deadline) { |
| 341 | t.Fatalf("never: %s", what) |
| 342 | } |
| 343 | time.Sleep(20 * time.Millisecond) |
| 344 | } |
| 345 | } |
| 346 | // The start probe has run and its unit is gone. |
| 347 | until("the start probe", func() bool { return w.dials.Load() >= 1 && w.live.Load() == 0 && a.Capable() }) |
| 348 | w.failStart.Store(true) |
| 349 | req := fx.request("SELECT 1", "") |
| 350 | fx.submit(req) |
| 351 | nudge <- struct{}{} |
| 352 | until("the read's result", func() bool { |
| 353 | _, _, err := fx.store.Get(context.Background(), ResultKey(req.ID)) |
| 354 | return err == nil |
| 355 | }) |
| 356 | until("the owed probe withdrawing the capability", func() bool { return !a.Capable() }) |
| 357 | } |
| 358 | |