carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package registry
2
3import (
4 "database/sql"
5 "errors"
6 "fmt"
7 "os"
8 "path/filepath"
9 "reflect"
10 "testing"
11)
12
13func openTimersReg(t *testing.T) *Registry {
14 t.Helper()
15 r, err := Open(filepath.Join(t.TempDir(), "registry.db"))
16 if err != nil {
17 t.Fatal(err)
18 }
19 t.Cleanup(func() { r.Close() })
20 return r
21}
22
23func TestTimersUpsertPreservesRunnerFields(t *testing.T) {
24 r := openTimersReg(t)
25 if err := r.UpsertTimer(Timer{Host: "h", Name: "sync", Kind: "schedule", Spec: "every 6h0m0s", Path: "/jobs/sync", NextAt: 100, CreatedAt: 1}); err != nil {
26 t.Fatal(err)
27 }
28 if _, err := r.MarkTimerFired("h", "sync", timerRegistration(t, r, "h", "sync"), 100, "204", 300, 0, 21700, 21700); err != nil {
29 t.Fatal(err)
30 }
31 // Converge sees the same declaration again: spec/path unchanged, next_at
32 // passed along as "whatever it is now" (0 here means "don't touch").
33 if err := r.UpsertTimer(Timer{Host: "h", Name: "sync", Kind: "schedule", Spec: "every 6h0m0s", Path: "/jobs/sync", NextAt: 0, CreatedAt: 99}); err != nil {
34 t.Fatal(err)
35 }
36 got, err := r.GetTimer("h", "sync")
37 if err != nil {
38 t.Fatal(err)
39 }
40 if got.LastAt != 100 || got.LastStatus != "204" || got.LastMs != 300 || got.NextAt != 21700 || got.CreatedAt != 1 {
41 t.Fatalf("runner fields clobbered: %+v", got)
42 }
43 // A changed spec DOES move next_at.
44 if err := r.UpsertTimer(Timer{Host: "h", Name: "sync", Kind: "schedule", Spec: "every 12h0m0s", Path: "/jobs/sync", NextAt: 43200}); err != nil {
45 t.Fatal(err)
46 }
47 got, _ = r.GetTimer("h", "sync")
48 if got.NextAt != 43200 || got.Spec != "every 12h0m0s" || got.LastStatus != "204" {
49 t.Fatalf("after spec change: %+v", got)
50 }
51 if err := r.UpsertTimer(Timer{Host: "h", Name: "sync", Kind: "once", Spec: "at 5", Path: "/p", NextAt: 5}); !errors.Is(err, ErrTimerKind) {
52 t.Fatalf("kind change = %v, want ErrTimerKind", err)
53 }
54}
55
56func TestTimersDueOrderingAndFireNow(t *testing.T) {
57 r := openTimersReg(t)
58 for _, tm := range []Timer{
59 {Host: "b", Name: "late", Kind: "schedule", Spec: "every 1h0m0s", Path: "/p", NextAt: 500},
60 {Host: "a", Name: "soon", Kind: "once", Spec: "at 10", Path: "/p", NextAt: 10},
61 {Host: "a", Name: "done", Kind: "once", Spec: "at 1", Path: "/p", NextAt: 0},
62 {Host: "c", Name: "future", Kind: "schedule", Spec: "every 1h0m0s", Path: "/p", NextAt: 9999},
63 } {
64 if err := r.UpsertTimer(tm); err != nil {
65 t.Fatal(err)
66 }
67 }
68 due, err := r.DueTimers(600, 10, nil)
69 if err != nil {
70 t.Fatal(err)
71 }
72 if len(due) != 2 || due[0].Name != "soon" || due[1].Name != "late" {
73 t.Fatalf("due = %+v", due)
74 }
75 if due, _ := r.DueTimers(600, 1, nil); len(due) != 1 {
76 t.Fatal("limit ignored")
77 }
78 if err := r.FireTimerNow("c", "future", 600); err != nil {
79 t.Fatal(err)
80 }
81 if due, _ := r.DueTimers(600, 10, []string{}); len(due) != 3 { // an empty exclude set is no filter
82 t.Fatalf("after FireTimerNow: %d due", len(due))
83 }
84 if err := r.FireTimerNow("nope", "x", 600); err != nil {
85 t.Fatalf("FireTimerNow on absent = %v, want nil", err)
86 }
87}
88
89func TestTimersPruneAndExcept(t *testing.T) {
90 r := openTimersReg(t)
91 for _, tm := range []Timer{
92 {Host: "alive", Name: "s1", Kind: "schedule", Spec: "every 1h0m0s", Path: "/p", NextAt: 1},
93 {Host: "alive", Name: "s2", Kind: "schedule", Spec: "every 1h0m0s", Path: "/p", NextAt: 1},
94 {Host: "alive", Name: "o1", Kind: "once", Spec: "at 1", Path: "/p", NextAt: 0, LastAt: 10},
95 {Host: "alive", Name: "o2", Kind: "once", Spec: "at 1", Path: "/p", NextAt: 0, LastAt: 500},
96 {Host: "gone", Name: "s1", Kind: "schedule", Spec: "every 1h0m0s", Path: "/p", NextAt: 1},
97 } {
98 if err := r.UpsertTimer(tm); err != nil {
99 t.Fatal(err)
100 }
101 }
102 // o1 has last_at=10 via insert — MarkTimerFired is the real path but insert suffices to seed.
103 n, err := r.PruneTimers(map[string]bool{"alive": true}, 100)
104 if err != nil || n != 2 { // gone/s1 and alive/o1
105 t.Fatalf("prune = %d %v", n, err)
106 }
107 n, err = r.DeleteTimersExcept("alive", "schedule", map[string]bool{"s2": true})
108 if err != nil || n != 1 {
109 t.Fatalf("except = %d %v", n, err)
110 }
111 left, _ := r.TimersFor("alive")
112 if len(left) != 2 || left[0].Name != "o2" || left[1].Name != "s2" {
113 t.Fatalf("left = %+v", left)
114 }
115 if c, _ := r.CountTimers("alive", "once"); c != 1 {
116 t.Fatalf("count = %d", c)
117 }
118 if _, err := r.GetTimer("gone", "s1"); !errors.Is(err, os.ErrNotExist) {
119 t.Fatalf("gone = %v", err)
120 }
121 if err := r.DeleteTimer("alive", "s2"); err != nil {
122 t.Fatal(err)
123 }
124 if err := r.DeleteTimer("alive", "s2"); err != nil {
125 t.Fatal("delete not idempotent")
126 }
127}
128
129// due_at is the occurrence a row is delivering (X-Carlos-Schedule-At). It
130// rides next_at on an upsert that moves it, follows the runner's word on
131// MarkTimerFired, and IS "now" after FireTimerNow — and a registry written
132// before the column existed gains it on open, with 0 on the old rows.
133func TestTimersDueAtFollowsTheOccurrence(t *testing.T) {
134 r := openTimersReg(t)
135 if err := r.UpsertTimer(Timer{Host: "h", Name: "s", Kind: "schedule", Spec: "every 1h0m0s", Path: "/p", NextAt: 3600, DueAt: 3600}); err != nil {
136 t.Fatal(err)
137 }
138 // A retry: next moves to the retry instant, due stays the occurrence.
139 if _, err := r.MarkTimerFired("h", "s", timerRegistration(t, r, "h", "s"), 3600, "503", 1, 1, 3660, 3600); err != nil {
140 t.Fatal(err)
141 }
142 got, _ := r.GetTimer("h", "s")
143 if got.NextAt != 3660 || got.DueAt != 3600 {
144 t.Fatalf("after retry: %+v", got)
145 }
146 // An advance: both move together.
147 if _, err := r.MarkTimerFired("h", "s", timerRegistration(t, r, "h", "s"), 3660, "204", 1, 0, 7200, 7200); err != nil {
148 t.Fatal(err)
149 }
150 if got, _ = r.GetTimer("h", "s"); got.NextAt != 7200 || got.DueAt != 7200 {
151 t.Fatalf("after advance: %+v", got)
152 }
153 // An unchanged upsert (NextAt 0) leaves both alone; a spec change moves both.
154 if err := r.UpsertTimer(Timer{Host: "h", Name: "s", Kind: "schedule", Spec: "every 1h0m0s", Path: "/p"}); err != nil {
155 t.Fatal(err)
156 }
157 if got, _ = r.GetTimer("h", "s"); got.NextAt != 7200 || got.DueAt != 7200 {
158 t.Fatalf("after unchanged upsert: %+v", got)
159 }
160 if err := r.UpsertTimer(Timer{Host: "h", Name: "s", Kind: "schedule", Spec: "every 2h0m0s", Path: "/p", NextAt: 9000, DueAt: 9000}); err != nil {
161 t.Fatal(err)
162 }
163 if got, _ = r.GetTimer("h", "s"); got.NextAt != 9000 || got.DueAt != 9000 {
164 t.Fatalf("after spec change: %+v", got)
165 }
166 // `run`: now is its own occurrence.
167 if err := r.FireTimerNow("h", "s", 8000); err != nil {
168 t.Fatal(err)
169 }
170 if got, _ = r.GetTimer("h", "s"); got.NextAt != 8000 || got.DueAt != 8000 {
171 t.Fatalf("after FireTimerNow: %+v", got)
172 }
173}
174
175func TestTimersPreDueAtRegistryGainsTheColumn(t *testing.T) {
176 path := filepath.Join(t.TempDir(), "registry.db")
177 reg, err := Open(path)
178 if err != nil {
179 t.Fatal(err)
180 }
181 if err := reg.Close(); err != nil {
182 t.Fatal(err)
183 }
184 old, err := sql.Open("sqlite", path)
185 if err != nil {
186 t.Fatal(err)
187 }
188 if _, err := old.Exec(`ALTER TABLE timers DROP COLUMN due_at`); err != nil {
189 t.Fatalf("simulate a pre-due_at registry: %v", err)
190 }
191 if _, err := old.Exec(`INSERT INTO timers (host, name, kind, spec, path, next_at, created_at) VALUES ('h', 'legacy', 'schedule', 'every 1h0m0s', '/p', 3600, 1)`); err != nil {
192 t.Fatal(err)
193 }
194 if err := old.Close(); err != nil {
195 t.Fatal(err)
196 }
197 // Open twice: the ALTER must be idempotent, not a one-shot.
198 for i := 0; i < 2; i++ {
199 reg, err = Open(path)
200 if err != nil {
201 t.Fatalf("open %d of a pre-due_at registry: %v", i+1, err)
202 }
203 got, err := reg.GetTimer("h", "legacy")
204 if err != nil {
205 t.Fatal(err)
206 }
207 if got.NextAt != 3600 || got.DueAt != 0 {
208 t.Fatalf("migrated row = %+v, want next_at kept and due_at 0", got)
209 }
210 if err := reg.Close(); err != nil {
211 t.Fatal(err)
212 }
213 }
214}
215
216// The batch is capped, and the runner skips hosts whose delivery is still
217// running: without the exclude filter a busy host with a batch's worth of due
218// rows takes every slot of every batch and another host's due timer is never
219// SEEN, let alone fired. The filter belongs in the query, before the LIMIT.
220func TestDueTimersExcludesBusyHostsBeforeTheLimit(t *testing.T) {
221 r := openTimersReg(t)
222 for i := 0; i < 8; i++ {
223 if err := r.UpsertTimer(Timer{Host: "busy", Name: "j" + string(rune('a'+i)), Kind: "schedule", Spec: "every 1h0m0s", Path: "/p", NextAt: 100}); err != nil {
224 t.Fatal(err)
225 }
226 }
227 // Due LATER than every one of the busy host's rows, so ordering alone
228 // never rescues it.
229 if err := r.UpsertTimer(Timer{Host: "quiet", Name: "j", Kind: "schedule", Spec: "every 1h0m0s", Path: "/p", NextAt: 200}); err != nil {
230 t.Fatal(err)
231 }
232 due, err := r.DueTimers(600, 4, nil)
233 if err != nil {
234 t.Fatal(err)
235 }
236 for _, tm := range due {
237 if tm.Host == "quiet" {
238 t.Fatalf("fixture: the quiet host must be crowded out without the filter, got %+v", due)
239 }
240 }
241 due, err = r.DueTimers(600, 4, []string{"busy"})
242 if err != nil {
243 t.Fatal(err)
244 }
245 if len(due) != 1 || due[0].Host != "quiet" {
246 t.Fatalf("excluding the busy host = %+v, want the quiet host's one row", due)
247 }
248 // Every host excluded is an empty batch, not a filter that silently
249 // stops applying.
250 if due, _ := r.DueTimers(600, 4, []string{"busy", "quiet"}); len(due) != 0 {
251 t.Fatalf("all hosts excluded = %+v, want none", due)
252 }
253}
254
255func TestEarliestTimersPerHost(t *testing.T) {
256 r := openTestRegistry(t)
257 rows := []Timer{
258 // Two rows for one host: the earliest wins.
259 {Host: "a.example", Name: "late", Kind: "schedule", Spec: "every 1h", Path: "/x", NextAt: 5000, DueAt: 5000},
260 {Host: "a.example", Name: "soon", Kind: "schedule", Spec: "every 5m", Path: "/y", NextAt: 1200, DueAt: 1200},
261 // A second host is reported independently.
262 {Host: "b.example", Name: "only", Kind: "schedule", Spec: "every 5m", Path: "/z", NextAt: 3000, DueAt: 3000},
263 // next_at == 0 means "not armed" and must never appear.
264 {Host: "c.example", Name: "parked", Kind: "schedule", Spec: "every 5m", Path: "/p", NextAt: 0, DueAt: 0},
265 }
266 for _, tm := range rows {
267 if err := r.UpsertTimer(tm); err != nil {
268 t.Fatalf("UpsertTimer %s/%s: %v", tm.Host, tm.Name, err)
269 }
270 }
271
272 got, err := r.EarliestTimers()
273 if err != nil {
274 t.Fatalf("EarliestTimers: %v", err)
275 }
276 want := map[string]int64{"a.example": 1200, "b.example": 3000}
277 if !reflect.DeepEqual(got, want) {
278 t.Errorf("EarliestTimers = %v, want %v", got, want)
279 }
280}
281
282func TestEarliestTimersEmpty(t *testing.T) {
283 r := openTestRegistry(t)
284 got, err := r.EarliestTimers()
285 if err != nil {
286 t.Fatalf("EarliestTimers: %v", err)
287 }
288 if len(got) != 0 {
289 t.Errorf("EarliestTimers on an empty table = %v, want empty", got)
290 }
291}
292
293func timerRegistration(t *testing.T, r *Registry, host, name string) string {
294 t.Helper()
295 row, err := r.GetTimer(host, name)
296 if err != nil {
297 t.Fatal(err)
298 }
299 return row.Registration
300}
301
302func TestTimerRegistrationProtectsMutations(t *testing.T) {
303 for _, mutation := range []string{"identical", "spec", "path", "manual", "recreate"} {
304 t.Run(mutation, func(t *testing.T) {
305 r := openTimersReg(t)
306 tm := Timer{Host: "h", Name: "s", Kind: "schedule", Spec: "every 1h0m0s", Path: "/jobs/sync", NextAt: 100, DueAt: 100, Attempts: 4, LastAt: 80, LastStatus: "503", LastMs: 1}
307 if err := r.UpsertTimer(tm); err != nil {
308 t.Fatal(err)
309 }
310 before, err := r.GetTimer(tm.Host, tm.Name)
311 if err != nil {
312 t.Fatal(err)
313 }
314 switch mutation {
315 case "spec":
316 tm.Spec, tm.NextAt, tm.DueAt = "every 2h0m0s", 0, 0
317 case "path":
318 tm.Path, tm.NextAt, tm.DueAt = "/jobs/other", 0, 0
319 case "recreate":
320 if err := r.DeleteTimer(tm.Host, tm.Name); err != nil {
321 t.Fatal(err)
322 }
323 tm.Attempts = 0
324 }
325 if mutation == "manual" {
326 err = r.FireTimerNow(tm.Host, tm.Name, 100)
327 } else {
328 err = r.UpsertTimer(tm)
329 }
330 if err != nil {
331 t.Fatal(err)
332 }
333 replacement, err := r.GetTimer(tm.Host, tm.Name)
334 if err != nil {
335 t.Fatal(err)
336 }
337 if replacement.Registration == "" || replacement.Registration == before.Registration || replacement.Attempts != 0 {
338 t.Fatalf("new registration: %+v", replacement)
339 }
340 if _, err := r.MarkTimerFired(tm.Host, tm.Name, before.Registration, 100, "204", 3, 0, 3700, 3700); err != nil {
341 t.Fatal(err)
342 }
343 if err := r.DeleteTimerRegistration(tm.Host, tm.Name, before.Registration); err != nil {
344 t.Fatal(err)
345 }
346 after, err := r.GetTimer(tm.Host, tm.Name)
347 if err != nil {
348 t.Fatal(err)
349 }
350 if *after != *replacement {
351 t.Fatalf("stale mutation changed registration: %+v -> %+v", replacement, after)
352 }
353 if err := r.DeleteTimerRegistration(tm.Host, tm.Name, after.Registration); err != nil {
354 t.Fatal(err)
355 }
356 if _, err := r.GetTimer(tm.Host, tm.Name); !errors.Is(err, os.ErrNotExist) {
357 t.Fatalf("current deletion: %v", err)
358 }
359 })
360 }
361}
362
363func TestTimerRegistrationLegacyMigration(t *testing.T) {
364 path := filepath.Join(t.TempDir(), "registry.db")
365 r, err := Open(path)
366 if err != nil {
367 t.Fatal(err)
368 }
369 if _, err := r.sql.Exec(`ALTER TABLE timers DROP COLUMN registration`); err != nil {
370 t.Fatal(err)
371 }
372 if _, err := r.sql.Exec(`INSERT INTO timers (host,name,kind,spec,path,next_at,created_at) VALUES ('h','s','once','at 100','/jobs/sync',100,1)`); err != nil {
373 t.Fatal(err)
374 }
375 if err := r.Close(); err != nil {
376 t.Fatal(err)
377 }
378 for i := 0; i < 2; i++ {
379 r, err = Open(path)
380 if err != nil {
381 t.Fatal(err)
382 }
383 row, err := r.GetTimer("h", "s")
384 if err != nil {
385 t.Fatal(err)
386 }
387 if row.Registration != "" {
388 t.Fatalf("legacy registration: %q", row.Registration)
389 }
390 if _, err := r.MarkTimerFired("h", "s", row.Registration, 100, "503", 3, 1, 160, 100); err != nil {
391 t.Fatal(err)
392 }
393 if err := r.Close(); err != nil {
394 t.Fatal(err)
395 }
396 }
397 r, err = Open(path)
398 if err != nil {
399 t.Fatal(err)
400 }
401 defer r.Close()
402 row, err := r.GetTimer("h", "s")
403 if err != nil {
404 t.Fatal(err)
405 }
406 if row.NextAt != 160 || row.DueAt != 100 || row.Attempts != 1 {
407 t.Fatalf("legacy completion: %+v", row)
408 }
409 if err := r.FireTimerNow("h", "s", 100); err != nil {
410 t.Fatal(err)
411 }
412 if _, err := r.MarkTimerFired("h", "s", "", 100, "204", 3, 0, 0, 0); err != nil {
413 t.Fatal(err)
414 }
415 row, err = r.GetTimer("h", "s")
416 if err != nil {
417 t.Fatal(err)
418 }
419 if row.Registration == "" || row.NextAt != 100 || row.Attempts != 0 {
420 t.Fatalf("rearmed legacy: %+v", row)
421 }
422}
423
424func TestUnchangedTimerUpsertConcurrentWithCompletion(t *testing.T) {
425 r := openTimersReg(t)
426 tm := Timer{Host: "h", Name: "s", Kind: "schedule", Spec: "every 1h0m0s", Path: "/jobs/sync", NextAt: 100, DueAt: 100}
427 if err := r.UpsertTimer(tm); err != nil {
428 t.Fatal(err)
429 }
430 registration := timerRegistration(t, r, tm.Host, tm.Name)
431 tm.NextAt, tm.DueAt = 0, 0
432 for i := int64(1); i <= 100; i++ {
433 start := make(chan struct{})
434 errs := make(chan error, 9)
435 for n := 0; n < 8; n++ {
436 go func() { <-start; errs <- r.UpsertTimer(tm) }()
437 }
438 go func() {
439 <-start
440 _, err := r.MarkTimerFired(tm.Host, tm.Name, registration, i, "503", 1, 1, 100+i, 100)
441 errs <- err
442 }()
443 close(start)
444 for n := 0; n < 9; n++ {
445 if err := <-errs; err != nil {
446 t.Fatal(err)
447 }
448 }
449 row, err := r.GetTimer(tm.Host, tm.Name)
450 if err != nil {
451 t.Fatal(err)
452 }
453 if row.NextAt != 100+i || row.DueAt != 100 || row.LastAt != i || row.Attempts != 1 || row.Registration != registration {
454 t.Fatalf("completion overwritten at iteration %d: %+v", i, row)
455 }
456 }
457}
458
459func TestReplaceTimersSwapsAHostsRowsExactly(t *testing.T) {
460 r := openTimersReg(t)
461 for _, tm := range []Timer{
462 {Host: "a.test", Name: "old", Kind: "schedule", Spec: "@every 1m", Path: "/t", NextAt: 100, CreatedAt: 1},
463 {Host: "b.test", Name: "other", Kind: "schedule", Spec: "@every 1m", Path: "/t", NextAt: 100, CreatedAt: 1},
464 } {
465 if err := r.UpsertTimer(tm); err != nil {
466 t.Fatal(err)
467 }
468 }
469 rows := []Timer{
470 {Host: "a.test", Name: "nightly", Kind: "schedule", Spec: "0 3 * * *", Path: "/n", NextAt: 5000, LastAt: 4000,
471 LastStatus: "ok", LastMs: 12, Attempts: 2, CreatedAt: 10, DueAt: 4900, Registration: "reg-nightly"},
472 {Host: "a.test", Name: "once-1", Kind: "once", Spec: "2026-10-06T00:00:00Z", Path: "/o", NextAt: 6000, CreatedAt: 11, DueAt: 6000, Registration: "reg-once"},
473 }
474 if err := r.ReplaceTimers("a.test", rows); err != nil {
475 t.Fatal(err)
476 }
477 got, err := r.TimersFor("a.test")
478 if err != nil {
479 t.Fatal(err)
480 }
481 if len(got) != 2 {
482 t.Fatalf("a.test has %d timers, want 2: %+v", len(got), got)
483 }
484 n := got[0] // ordered by name: nightly, once-1
485 if n.Name != "nightly" || n.NextAt != 5000 || n.LastAt != 4000 || n.LastStatus != "ok" || n.LastMs != 12 ||
486 n.Attempts != 2 || n.CreatedAt != 10 || n.DueAt != 4900 || n.Registration != "reg-nightly" {
487 t.Fatalf("bookkeeping not preserved: %+v", n)
488 }
489 if other, _ := r.TimersFor("b.test"); len(other) != 1 {
490 t.Fatalf("b.test has %d timers, want 1: another host's rows were touched", len(other))
491 }
492}
493
494func TestReplaceTimersIsAllOrNothing(t *testing.T) {
495 r := openTimersReg(t)
496 if err := r.UpsertTimer(Timer{Host: "a.test", Name: "keep", Kind: "schedule", Spec: "@every 1m", Path: "/t", NextAt: 100, CreatedAt: 1}); err != nil {
497 t.Fatal(err)
498 }
499 // Two rows with one name: the delete and the first insert succeed, the
500 // second insert breaks the primary key. Without a transaction the host
501 // would be left with "dup" and without "keep".
502 dup := []Timer{
503 {Host: "a.test", Name: "dup", Kind: "schedule", Spec: "@every 1m", Path: "/t", NextAt: 1, CreatedAt: 1},
504 {Host: "a.test", Name: "dup", Kind: "schedule", Spec: "@every 2m", Path: "/t", NextAt: 1, CreatedAt: 1},
505 }
506 if err := r.ReplaceTimers("a.test", dup); err == nil {
507 t.Fatal("two rows with one name were accepted")
508 }
509 got, _ := r.TimersFor("a.test")
510 if len(got) != 1 || got[0].Name != "keep" {
511 t.Fatalf("a failed replace changed the rows: %+v", got)
512 }
513}
514
515func TestReplaceTimersRefusesARowForAnotherHost(t *testing.T) {
516 r := openTimersReg(t)
517 rows := []Timer{{Host: "elsewhere.test", Name: "y", Kind: "schedule", Spec: "@every 1m", Path: "/t", NextAt: 1, CreatedAt: 1}}
518 if err := r.ReplaceTimers("a.test", rows); err == nil {
519 t.Fatal("a row for another host was accepted")
520 }
521}
522
523func TestReplaceTimersWithNoRowsClearsTheHost(t *testing.T) {
524 r := openTimersReg(t)
525 if err := r.UpsertTimer(Timer{Host: "a.test", Name: "gone", Kind: "schedule", Spec: "@every 1m", Path: "/t", NextAt: 100, CreatedAt: 1}); err != nil {
526 t.Fatal(err)
527 }
528 if err := r.ReplaceTimers("a.test", nil); err != nil {
529 t.Fatal(err)
530 }
531 if got, _ := r.TimersFor("a.test"); len(got) != 0 {
532 t.Fatalf("rows left: %+v", got)
533 }
534}
535
536// Moves Task 6 fix round 1: DueUnheldTimers leaves out every row whose
537// route is held (moves spec §5.2) BEFORE the limit, so a held host's rows
538// cannot fill the batch and hide another host's due timer. DueTimers itself
539// is unchanged: with the hold wiring off the runner still asks it.
540func TestDueUnheldTimersExcludesHeldHostsBeforeTheLimit(t *testing.T) {
541 r := openTimersReg(t)
542 for _, h := range []string{"held.test", "free.test"} {
543 if err := r.Upsert(Route{Host: h, Kind: "instance", Socket: "/s"}); err != nil {
544 t.Fatal(err)
545 }
546 }
547 if err := r.SetHold("held.test", "moving", holdOpA); err != nil {
548 t.Fatal(err)
549 }
550 for i, name := range []string{"t0", "t1", "t2"} {
551 if err := r.UpsertTimer(Timer{Host: "held.test", Name: name, Kind: "schedule", Spec: "every 1h0m0s", Path: "/j", NextAt: int64(10 + i)}); err != nil {
552 t.Fatal(err)
553 }
554 }
555 // Due after every held row; and a row whose host has no route at all,
556 // which neither query drops (the converger prunes those).
557 if err := r.UpsertTimer(Timer{Host: "free.test", Name: "t", Kind: "schedule", Spec: "every 1h0m0s", Path: "/j", NextAt: 50}); err != nil {
558 t.Fatal(err)
559 }
560 if err := r.UpsertTimer(Timer{Host: "gone.test", Name: "t", Kind: "schedule", Spec: "every 1h0m0s", Path: "/j", NextAt: 60}); err != nil {
561 t.Fatal(err)
562 }
563 due, err := r.DueTimers(100, 2, nil)
564 if err != nil {
565 t.Fatal(err)
566 }
567 if len(due) != 2 || due[0].Host != "held.test" || due[1].Host != "held.test" {
568 t.Fatalf("DueTimers = %+v, want the two earliest held rows (main's answer)", due)
569 }
570 un, err := r.DueUnheldTimers(100, 2, nil)
571 if err != nil {
572 t.Fatal(err)
573 }
574 if len(un) != 2 || un[0].Host != "free.test" || un[1].Host != "gone.test" {
575 t.Fatalf("DueUnheldTimers = %+v, want free.test then gone.test", un)
576 }
577 // The busy-host exclusion still applies on top.
578 un, err = r.DueUnheldTimers(100, 2, []string{"free.test"})
579 if err != nil {
580 t.Fatal(err)
581 }
582 if len(un) != 1 || un[0].Host != "gone.test" {
583 t.Fatalf("DueUnheldTimers excluding free.test = %+v", un)
584 }
585 // Once the hold lifts the rows are due again, in order.
586 if ok, _ := r.ReleaseHold("held.test", holdOpA); !ok {
587 t.Fatal("release")
588 }
589 un, _ = r.DueUnheldTimers(100, 2, nil)
590 if len(un) != 2 || un[0].Host != "held.test" {
591 t.Fatalf("after the release DueUnheldTimers = %+v", un)
592 }
593}
594
595// Final review F5: both due queries leave out every row of a gated route
596// (an uncommitted import's) before the limit. The runner skips such a row
597// without advancing it, so 64 of them, due first, would otherwise fill the
598// runner's whole batch on every tick and hide another host's due timer for
599// ever. A row whose host has no route is still returned.
600func TestDueTimersExcludeGatedRoutesBeforeTheLimit(t *testing.T) {
601 r := openTimersReg(t)
602 const batch = 64
603 for i := range 4 {
604 host := fmt.Sprintf("gated%d.test", i)
605 if err := r.Upsert(Route{Host: host, Kind: "instance", Socket: "/s", ImportGate: ImportGateClosed}); err != nil {
606 t.Fatal(err)
607 }
608 for n := range batch / 4 {
609 if err := r.UpsertTimer(Timer{Host: host, Name: fmt.Sprintf("t%d", n), Kind: "schedule", Spec: "every 1h0m0s", Path: "/j",
610 NextAt: int64(10 + n)}); err != nil {
611 t.Fatal(err)
612 }
613 }
614 }
615 if err := r.Upsert(Route{Host: "free.test", Kind: "instance", Socket: "/s"}); err != nil {
616 t.Fatal(err)
617 }
618 if err := r.UpsertTimer(Timer{Host: "free.test", Name: "t", Kind: "schedule", Spec: "every 1h0m0s", Path: "/j", NextAt: 90}); err != nil {
619 t.Fatal(err)
620 }
621 if err := r.UpsertTimer(Timer{Host: "gone.test", Name: "t", Kind: "schedule", Spec: "every 1h0m0s", Path: "/j", NextAt: 95}); err != nil {
622 t.Fatal(err)
623 }
624 for name, due := range map[string]func(int64, int, []string) ([]Timer, error){
625 "DueTimers": r.DueTimers, "DueUnheldTimers": r.DueUnheldTimers,
626 } {
627 got, err := due(100, batch, nil)
628 if err != nil {
629 t.Fatal(err)
630 }
631 var hosts []string
632 for _, tm := range got {
633 hosts = append(hosts, tm.Host)
634 }
635 if len(hosts) != 2 || hosts[0] != "free.test" || hosts[1] != "gone.test" {
636 t.Fatalf("%s hosts = %q, want free.test then gone.test", name, hosts)
637 }
638 }
639}
640