| index 34d016f56..688f87076 100644 |
| --- a/internal/registry/hold_test.go |
| +++ b/internal/registry/hold_test.go |
| @@ -185,6 +185,70 @@ func TestDeleteIfHold(t *testing.T) { |
| } |
| } |
| |
| +// The source's finalization (moves spec §7.9 step 3, ruling H5(d)): the row |
| +// and the host's timer rows go in one transaction, and only while the op |
| +// holds the row. Any refusal leaves both, and the neighbour's timers stay. |
| +func TestDeleteIfHoldWithTimers(t *testing.T) { |
| + r := holdFixture(t, "h") |
| + if err := r.Upsert(Route{Host: "n", Kind: "instance", Socket: "/n", App: "a", Hibernate: true}); err != nil { |
| + t.Fatal(err) |
| + } |
| + for _, tm := range []Timer{{Host: "h", Name: "nightly", Kind: "cron", Spec: "0 3 * * *", NextAt: 1000}, |
| + {Host: "h", Name: "hourly", Kind: "cron", Spec: "0 * * * *", NextAt: 2000}, |
| + {Host: "n", Name: "nightly", Kind: "cron", Spec: "0 3 * * *", NextAt: 1000}} { |
| + if err := r.UpsertTimer(tm); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + timers := func(host string) int { |
| + t.Helper() |
| + got, err := r.TimersFor(host) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + return len(got) |
| + } |
| + // Not held, held by another op, an empty op: nothing goes. |
| + for _, c := range []struct{ name, hold, op string }{{"unheld", "", holdOpA}, {"another op", holdOpB, holdOpA}, {"empty op", holdOpA, ""}} { |
| + if c.hold != "" { |
| + if err := r.SetHold("h", "moving", c.hold); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + if ok, err := r.DeleteIfHoldWithTimers("h", c.op); err != nil || ok { |
| + t.Fatalf("%s: %v %v", c.name, ok, err) |
| + } |
| + if rt, _ := r.Get("h"); rt == nil || timers("h") != 2 { |
| + t.Fatalf("%s: the row or its timers changed: %+v, %d timers", c.name, rt, timers("h")) |
| + } |
| + if c.hold != "" { |
| + if ok, err := r.ReleaseHold("h", c.hold); err != nil || !ok { |
| + t.Fatal(ok, err) |
| + } |
| + } |
| + } |
| + if err := r.SetHold("h", "moving", holdOpA); err != nil { |
| + t.Fatal(err) |
| + } |
| + if ok, err := r.DeleteIfHoldWithTimers("h", holdOpA); err != nil || !ok { |
| + t.Fatalf("delete under the holding op: %v %v", ok, err) |
| + } |
| + if rt, _ := r.Get("h"); rt != nil || timers("h") != 0 { |
| + t.Fatalf("after the delete: %+v, %d timers", rt, timers("h")) |
| + } |
| + if rt, _ := r.Get("n"); rt == nil || timers("n") != 1 { |
| + t.Fatalf("the neighbour changed: %+v, %d timers", rt, timers("n")) |
| + } |
| + // A replay finds no row held by the op and deletes nothing, timers |
| + // written since included. |
| + if err := r.UpsertTimer(Timer{Host: "h", Name: "late", Kind: "cron", Spec: "0 3 * * *", NextAt: 1000}); err != nil { |
| + t.Fatal(err) |
| + } |
| + if ok, err := r.DeleteIfHoldWithTimers("h", holdOpA); err != nil || ok || timers("h") != 1 { |
| + t.Fatalf("replay: %v %v, %d timers", ok, err, timers("h")) |
| + } |
| +} |
| + |
| func TestReleaseHoldEmptyOpMatchesNothing(t *testing.T) { |
| r := holdFixture(t, "h") |
| if ok, err := r.ReleaseHold("h", ""); err != nil || ok { |