| index c06250467..923e302bc 100644 |
| --- a/internal/hostclaim/generation_test.go |
| +++ b/internal/hostclaim/generation_test.go |
| @@ -12,7 +12,7 @@ import ( |
| "github.com/carlosframework/platform/internal/release" |
| ) |
| |
| -func TestAdmitWriterWarmColdJournal(t *testing.T) { |
| +func TestAdmitWriterWarmColdAdmissions(t *testing.T) { |
| ctx := context.Background() |
| s := release.NewDirStore(t.TempDir()) |
| o := servingClaim(t, ctx, s) |
| @@ -28,7 +28,7 @@ func TestAdmitWriterWarmColdJournal(t *testing.T) { |
| if err := json.Unmarshal(raw, &fields); err != nil { |
| t.Fatal(err) |
| } |
| - for _, name := range []string{"session", "writer_box"} { |
| + for _, name := range []string{"session", "writer_box", "admissions"} { |
| if _, ok := fields[name]; ok { |
| t.Fatalf("zero %s must be omitted", name) |
| } |
| @@ -41,8 +41,8 @@ func TestAdmitWriterWarmColdJournal(t *testing.T) { |
| if o.Claim.Holder.WriterBox != "adi-knitting" || o.Claim.Holder.Generation != "gen_a" || o.Claim.Holder.Stamp != cold || o.Claim.Holder.Session != cold || cold <= stamp { |
| t.Fatalf("cold holder = %+v, gen = %d", o.Claim.Holder, cold) |
| } |
| - first := fmt.Sprintf(`{"session":%d,"box":"adi-knitting","generation":"gen_a","stamp":%d,"mode":"cold","predecessor":{"session":0,"box":"","generation":"","stamp":%d}}`, cold, cold, stamp) |
| - assertWriterJournal(t, o.Claim, "["+first+"]") |
| + first := AdmissionRecord{Session: cold, Box: "adi-knitting", Generation: "gen_a", Mode: "cold"} |
| + assertAdmissions(t, o.Claim, []AdmissionRecord{first}) |
| o, err = Update(ctx, s, o, func(c *Claim) error { |
| c.Op.Exec = "boxT" |
| return nil |
| @@ -58,8 +58,8 @@ func TestAdmitWriterWarmColdJournal(t *testing.T) { |
| if o.Claim.Holder.WriterBox != "boxT" || o.Claim.Holder.Session != warm || warm <= cold || o.Claim.Holder.Generation != "gen_a" || o.Claim.Holder.Stamp != cold { |
| t.Fatalf("warm holder = %+v, gen = %d", o.Claim.Holder, warm) |
| } |
| - second := fmt.Sprintf(`{"session":%d,"box":"boxT","generation":"gen_a","stamp":%d,"mode":"warm","predecessor":{"session":%d,"box":"adi-knitting","generation":"gen_a","stamp":%d}}`, warm, cold, cold, cold) |
| - assertWriterJournal(t, o.Claim, "["+first+","+second+"]") |
| + second := AdmissionRecord{Session: warm, Box: "boxT", Generation: "gen_a", Mode: "warm", Predecessor: "gen_a"} |
| + assertAdmissions(t, o.Claim, []AdmissionRecord{first, second}) |
| o, err = Update(ctx, s, o, AdmitWriter(Admission{ObservedSession: warm, ObservedGeneration: "gen_a", Box: "boxT", Mode: "cold", NewGeneration: "gen_b", PredecessorCertified: true})) |
| if err != nil { |
| t.Fatal(err) |
| @@ -67,8 +67,8 @@ func TestAdmitWriterWarmColdJournal(t *testing.T) { |
| if o.Claim.Holder.Generation != "gen_b" || o.Claim.Holder.Stamp != o.Claim.Gen || o.Claim.Holder.Session != o.Claim.Gen { |
| t.Fatalf("second cold holder = %+v, gen = %d", o.Claim.Holder, o.Claim.Gen) |
| } |
| - third := fmt.Sprintf(`{"session":%d,"box":"boxT","generation":"gen_b","stamp":%d,"mode":"cold","predecessor":{"session":%d,"box":"boxT","generation":"gen_a","stamp":%d}}`, o.Claim.Gen, o.Claim.Gen, warm, cold) |
| - assertWriterJournal(t, o.Claim, "["+first+","+second+","+third+"]") |
| + third := AdmissionRecord{Session: o.Claim.Gen, Box: "boxT", Generation: "gen_b", Mode: "cold", Predecessor: "gen_a"} |
| + assertAdmissions(t, o.Claim, []AdmissionRecord{first, second, third}) |
| stored, err := Read(ctx, s, testHost) |
| if err != nil { |
| t.Fatal(err) |
| @@ -78,17 +78,13 @@ func TestAdmitWriterWarmColdJournal(t *testing.T) { |
| } |
| } |
| |
| -func assertWriterJournal(t *testing.T, c *Claim, want string) { |
| +func assertAdmissions(t *testing.T, c *Claim, want []AdmissionRecord) { |
| t.Helper() |
| - var gotJSON, wantJSON any |
| - if err := json.Unmarshal([]byte(c.Op.Journal["writers"]), &gotJSON); err != nil { |
| - t.Fatal(err) |
| - } |
| - if err := json.Unmarshal([]byte(want), &wantJSON); err != nil { |
| - t.Fatal(err) |
| + if !reflect.DeepEqual(c.Holder.Admissions, want) { |
| + t.Fatalf("admissions = %+v, want %+v", c.Holder.Admissions, want) |
| } |
| - if !reflect.DeepEqual(gotJSON, wantJSON) { |
| - t.Fatalf("writers = %s, want %s", c.Op.Journal["writers"], want) |
| + if _, ok := c.Op.Journal["writers"]; ok { |
| + t.Fatal("admission wrote an operation writers journal") |
| } |
| } |
| |
| @@ -127,9 +123,9 @@ func TestAdmitWriterPreservesOperation(t *testing.T) { |
| want.Holder.Generation = generation |
| want.Holder.Stamp = want.Gen |
| } |
| - want.Op.Journal = map[string]string{"source": "unchanged", "writers": next.Claim.Op.Journal["writers"]} |
| - if want.Op.Journal["writers"] == "" || !reflect.DeepEqual(next.Claim, &want) { |
| - t.Fatalf("admission changed fields outside holder session/generation/stamp and writers journal: got %+v, want %+v", next.Claim, &want) |
| + want.Holder.Admissions = []AdmissionRecord{{Session: want.Gen, Box: "boxT", Generation: generation, Mode: mode}} |
| + if !reflect.DeepEqual(next.Claim, &want) { |
| + t.Fatalf("admission changed fields outside holder writer fields and admissions: got %+v, want %+v", next.Claim, &want) |
| } |
| }) |
| } |
| @@ -137,7 +133,7 @@ func TestAdmitWriterPreservesOperation(t *testing.T) { |
| } |
| |
| func TestAdmitWriterRefusals(t *testing.T) { |
| - for _, name := range []string{"no holder", "no executor", "non-executor", "stale session", "stale generation", "warm with generation", "warm with current generation", "cold without generation", "cold same generation", "unknown mode", "empty mode", "malformed journal", "warm without certificate", "stale op", "stale phase", "creating", "deleting", "parking", "purging", "placing"} { |
| + for _, name := range []string{"no holder", "no executor", "non-executor", "stale session", "stale generation", "warm with generation", "warm with current generation", "cold without generation", "cold same generation", "unknown mode", "empty mode", "unknown coverage", "warm without certificate", "stale op", "stale phase", "creating", "deleting", "parking", "purging", "placing"} { |
| t.Run(name, func(t *testing.T) { |
| ctx := context.Background() |
| s := release.NewDirStore(t.TempDir()) |
| @@ -153,8 +149,6 @@ func TestAdmitWriterRefusals(t *testing.T) { |
| c.Op.Exec = "" |
| case "creating", "deleting", "parking", "purging", "placing": |
| c.Op.Kind, c.Op.ID = name, "op1" |
| - case "malformed journal": |
| - c.Op.Journal = map[string]string{"writers": "not-json"} |
| } |
| return nil |
| }) |
| @@ -188,6 +182,8 @@ func TestAdmitWriterRefusals(t *testing.T) { |
| switch name { |
| case "warm without certificate": |
| in.Mode, in.NewGeneration, in.PredecessorCertified = "warm", "", false |
| + case "unknown coverage": |
| + in.Coverage = "remote" |
| case "stale op": |
| in.ObservedOpID = "old" |
| case "stale phase": |
| @@ -327,8 +323,11 @@ func TestAdmitWriterSurvivesOperationReplacement(t *testing.T) { |
| if err != nil { |
| t.Fatal(err) |
| } |
| - want := fmt.Sprintf(`[{"session":%d,"box":"boxT","generation":"gen_b","stamp":%d,"mode":"cold","predecessor":{"session":%d,"box":"adi-knitting","generation":"gen_a","stamp":%d},"writes_lost":{"box":"adi-knitting","generation":"gen_a","session":%d,"remote_max_txid":42}}]`, o.Claim.Gen, o.Claim.Gen, writer.Session, writer.Stamp, writer.Session) |
| - assertWriterJournal(t, o.Claim, want) |
| + want := append(append([]AdmissionRecord(nil), writer.Admissions...), AdmissionRecord{ |
| + Session: o.Claim.Gen, Box: "boxT", Generation: "gen_b", Mode: "cold", Predecessor: "gen_a", |
| + WritesLost: &WritesLost{Box: "adi-knitting", Generation: "gen_a", Session: writer.Session, RemoteMaxTXID: 42}, |
| + }) |
| + assertAdmissions(t, o.Claim, want) |
| if o.Claim.Holder.WriterBox != "boxT" { |
| t.Fatalf("new writer = %q", o.Claim.Holder.WriterBox) |
| } |
| @@ -390,8 +389,10 @@ func TestAdmitWriterKeepStamp(t *testing.T) { |
| if next.Claim.Holder.Stamp != before.Stamp || next.Claim.Holder.Session != next.Claim.Gen || next.Claim.Holder.Generation != "gen_b" { |
| t.Fatalf("landing admission holder = %+v, prior stamp = %d", next.Claim.Holder, before.Stamp) |
| } |
| - want := fmt.Sprintf(`[{"session":%d,"box":"boxT","generation":"gen_b","stamp":%d,"mode":"cold","predecessor":{"session":%d,"box":"adi-knitting","generation":"gen_a","stamp":%d}}]`, next.Claim.Gen, before.Stamp, before.Session, before.Stamp) |
| - assertWriterJournal(t, next.Claim, want) |
| + want := append(append([]AdmissionRecord(nil), before.Admissions...), AdmissionRecord{ |
| + Session: next.Claim.Gen, Box: "boxT", Generation: "gen_b", Mode: "cold", Predecessor: "gen_a", |
| + }) |
| + assertAdmissions(t, next.Claim, want) |
| assertAdmissionStored(t, ctx, s, next) |
| in.ObservedSession, in.ObservedGeneration, in.NewGeneration = next.Claim.Holder.Session, "gen_b", "gen_c" |
| if _, err := Update(ctx, s, next, AdmitWriter(in)); !errors.Is(err, ErrWrongState) { |
| @@ -402,45 +403,30 @@ func TestAdmitWriterKeepStamp(t *testing.T) { |
| } |
| } |
| |
| -func TestAdmitWriterBaselineCoversPredecessor(t *testing.T) { |
| +func TestAdmitWriterCoverage(t *testing.T) { |
| for _, mode := range []string{"cold", "warm"} { |
| for _, certified := range []bool{false, true} { |
| - for _, covered := range []bool{false, true} { |
| + for _, coverage := range []string{"", "sealed", "clean-return"} { |
| if mode == "warm" && !certified { |
| continue |
| } |
| - t.Run(fmt.Sprintf("%s/certified=%t/covered=%t", mode, certified, covered), func(t *testing.T) { |
| + t.Run(fmt.Sprintf("%s/certified=%t/coverage=%s", mode, certified, coverage), func(t *testing.T) { |
| ctx := context.Background() |
| s := release.NewDirStore(t.TempDir()) |
| o := servingClaim(t, ctx, s) |
| - o, err := Update(ctx, s, o, AdmitWriter(Admission{Box: "adi-knitting", Mode: "cold", NewGeneration: "gen_a", PredecessorCertified: true})) |
| - if err != nil { |
| - t.Fatal(err) |
| - } |
| + o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{Box: "adi-knitting", Mode: "cold", NewGeneration: "gen_a", PredecessorCertified: true})) |
| before := *o.Claim.Holder |
| - var oldEntries []json.RawMessage |
| - if err := json.Unmarshal([]byte(o.Claim.Op.Journal["writers"]), &oldEntries); err != nil { |
| - t.Fatal(err) |
| - } |
| - in := Admission{ObservedSession: before.Session, ObservedGeneration: before.Generation, Box: "adi-knitting", Mode: mode, PredecessorCertified: certified, BaselineCoversPredecessor: covered, RemoteMaxTXID: 42} |
| - generation, stamp := before.Generation, before.Stamp |
| + in := Admission{ObservedSession: before.Session, ObservedGeneration: before.Generation, Box: "adi-knitting", Mode: mode, PredecessorCertified: certified, Coverage: coverage, RemoteMaxTXID: 42} |
| + generation := before.Generation |
| if mode == "cold" { |
| - in.NewGeneration, generation, stamp = "gen_b", "gen_b", o.Claim.Gen+1 |
| + in.NewGeneration, generation = "gen_b", "gen_b" |
| } |
| - o, err = Update(ctx, s, o, AdmitWriter(in)) |
| - if err != nil { |
| - t.Fatal(err) |
| + o = updateGenerationTest(t, ctx, s, o, AdmitWriter(in)) |
| + last := AdmissionRecord{Session: o.Claim.Gen, Box: "adi-knitting", Generation: generation, Predecessor: "gen_a", Mode: mode, Coverage: coverage} |
| + if mode == "cold" && !certified && coverage == "" { |
| + last.WritesLost = &WritesLost{Box: "adi-knitting", Generation: "gen_a", Session: before.Session, RemoteMaxTXID: 42} |
| } |
| - extra := "" |
| - if mode == "cold" && !certified { |
| - if covered { |
| - extra = `,"baseline":"sealed"` |
| - } else { |
| - extra = fmt.Sprintf(`,"writes_lost":{"box":"adi-knitting","generation":"gen_a","session":%d,"remote_max_txid":42}`, before.Session) |
| - } |
| - } |
| - last := fmt.Sprintf(`{"session":%d,"box":"adi-knitting","generation":%q,"stamp":%d,"mode":%q,"predecessor":{"session":%d,"box":"adi-knitting","generation":"gen_a","stamp":%d}%s}`, o.Claim.Gen, generation, stamp, mode, before.Session, before.Stamp, extra) |
| - assertWriterJournal(t, o.Claim, "["+string(oldEntries[0])+","+last+"]") |
| + assertAdmissions(t, o.Claim, append(append([]AdmissionRecord(nil), before.Admissions...), last)) |
| assertAdmissionStored(t, ctx, s, o) |
| }) |
| } |
| @@ -448,135 +434,321 @@ func TestAdmitWriterBaselineCoversPredecessor(t *testing.T) { |
| } |
| } |
| |
| -func TestQueueGenerationRetire(t *testing.T) { |
| - ctx := context.Background() |
| - s := release.NewDirStore(t.TempDir()) |
| - o := servingClaim(t, ctx, s) |
| - o, err := Update(ctx, s, o, AdmitWriter(Admission{Box: "adi-knitting", Mode: "cold", NewGeneration: "gen_a", PredecessorCertified: true})) |
| +func updateGenerationTest(t *testing.T, ctx context.Context, s release.Store, o Observed, tr func(*Claim) error) Observed { |
| + t.Helper() |
| + next, err := Update(ctx, s, o, tr) |
| if err != nil { |
| t.Fatal(err) |
| } |
| - session := o.Claim.Holder.Session |
| - calls := 0 |
| - newID := func() string { calls++; return fmt.Sprintf("id%d", calls) } |
| - for _, target := range []struct{ region, generation string }{{"ie", "old"}, {"uk", "old"}, {"ie", "older"}} { |
| - before := o |
| - o, err = Update(ctx, s, o, QueueGenerationRetire(session, "gen_a", target.region, target.generation, newID)) |
| - if err != nil { |
| - t.Fatal(err) |
| - } |
| - want := *before.Claim |
| - want.Gen++ |
| - want.Obligations = append(append([]Obligation(nil), want.Obligations...), Obligation{ |
| - ID: fmt.Sprintf("id%d", calls-1), Op: fmt.Sprintf("session:%d", session), Kind: "park-generation", |
| - Region: target.region, Generation: target.generation, Retirement: fmt.Sprintf("id%d", calls), Owner: "region:" + target.region, |
| + return assertAdmissionStored(t, ctx, s, next) |
| +} |
| + |
| +func TestAdmitWriterCleanReturnPredecessor(t *testing.T) { |
| + for _, coverage := range []string{"", "clean-return"} { |
| + t.Run(coverage, func(t *testing.T) { |
| + ctx := context.Background() |
| + s := release.NewDirStore(t.TempDir()) |
| + o := servingClaim(t, ctx, s) |
| + o = updateGenerationTest(t, ctx, s, o, func(c *Claim) error { |
| + c.Op = Op{Kind: KindLanding, ID: "move", Phase: "probe", Exec: "boxT"} |
| + return nil |
| + }) |
| + o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{ObservedOpID: "move", ObservedPhase: "probe", Box: "boxT", Mode: "cold", NewGeneration: "GT"})) |
| + target := o.Claim.Holder.Admissions[0] |
| + o = updateGenerationTest(t, ctx, s, o, func(c *Claim) error { |
| + c.Holder.Generation = "GS" |
| + c.Op = Op{Kind: KindServing, Exec: "boxS"} |
| + return nil |
| + }) |
| + o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{ObservedSession: target.Session, ObservedGeneration: "GS", Box: "boxS", Mode: "cold", NewGeneration: "GS2", Coverage: coverage, RemoteMaxTXID: 42})) |
| + last := AdmissionRecord{Session: o.Claim.Gen, Box: "boxS", Generation: "GS2", Predecessor: "GT", Mode: "cold", Coverage: coverage} |
| + if coverage == "" { |
| + last.WritesLost = &WritesLost{Box: "boxT", Generation: "GT", Session: target.Session, RemoteMaxTXID: 42} |
| + } |
| + assertAdmissions(t, o.Claim, []AdmissionRecord{target, last}) |
| }) |
| - if !reflect.DeepEqual(o.Claim, &want) { |
| - t.Fatalf("queued claim = %+v, want %+v", o.Claim, &want) |
| - } |
| - assertAdmissionStored(t, ctx, s, o) |
| - } |
| - if calls != 6 { |
| - t.Fatalf("newID calls = %d, want 6", calls) |
| } |
| - if _, err := Update(ctx, s, o, QueueGenerationRetire(session, "gen_a", "ie", "old", newID)); !errors.Is(err, ErrWrongState) { |
| - t.Fatalf("duplicate retirement = %v, want ErrWrongState", err) |
| +} |
| + |
| +func TestAdoptAdmitPublishRootGeneration(t *testing.T) { |
| + for _, replaceOp := range []bool{false, true} { |
| + t.Run(fmt.Sprintf("replaceOp=%t", replaceOp), func(t *testing.T) { |
| + ctx := context.Background() |
| + s := release.NewDirStore(t.TempDir()) |
| + o, err := Read(ctx, s, testHost) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + o = updateGenerationTest(t, ctx, s, o, Adopt(newHolder(), "boxS", nil, nil, "adopt", counter())) |
| + if o.Claim.Holder.WriterBox != "boxS" || o.Claim.Holder.Generation != "" || RootGeneration != "@root" { |
| + t.Fatalf("adopted holder = %+v", o.Claim.Holder) |
| + } |
| + for _, generation := range []string{"", RootGeneration} { |
| + newID := func() string { t.Fatal("unadmitted publication allocated an ID"); return "" } |
| + if _, err := Update(ctx, s, o, PublishGeneration("", "", 0, generation, "ie", newID)); !errors.Is(err, ErrWrongState) { |
| + t.Fatalf("publication before admission = %v", err) |
| + } |
| + assertAdmissionStored(t, ctx, s, o) |
| + } |
| + if replaceOp { |
| + o = updateGenerationTest(t, ctx, s, o, func(c *Claim) error { |
| + c.Op = Op{Kind: KindLanding, ID: "move", Phase: "probe", Exec: "boxT"} |
| + return nil |
| + }) |
| + o = updateGenerationTest(t, ctx, s, o, func(c *Claim) error { |
| + c.Op = Op{Kind: KindServing, Exec: "boxT"} |
| + return nil |
| + }) |
| + } |
| + o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{Box: o.Claim.Op.Exec, Mode: "cold", NewGeneration: "G1", RemoteMaxTXID: 42})) |
| + assertAdmissions(t, o.Claim, []AdmissionRecord{{ |
| + Session: o.Claim.Gen, Box: o.Claim.Op.Exec, Generation: "G1", Mode: "cold", |
| + WritesLost: &WritesLost{Box: "boxS", Session: 0, Generation: "", RemoteMaxTXID: 42}, |
| + }}) |
| + o = updateGenerationTest(t, ctx, s, o, PublishGeneration("", "", o.Claim.Holder.Session, "G1", "ie", counter())) |
| + if !o.Claim.Holder.Admissions[0].Covered || len(o.Claim.Obligations) != 1 { |
| + t.Fatalf("published claim = %+v", o.Claim) |
| + } |
| + ob := o.Claim.Obligations[0] |
| + if ob.Generation != RootGeneration || ob.Kind != "park-generation" || ob.Region != "ie" || ob.Owner != "region:ie" || ob.Op != fmt.Sprintf("session:%d", o.Claim.Holder.Session) { |
| + t.Fatalf("root obligation = %+v", ob) |
| + } |
| + holder := *o.Claim.Holder |
| + o, err = Update(ctx, s, o, CompleteObligation(ob.Op, ob.ID)) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + stored, err := Read(ctx, s, testHost) |
| + if err != nil || stored.ETag != o.ETag || len(stored.Claim.Obligations) != 0 || !reflect.DeepEqual(*stored.Claim.Holder, holder) || stored.Claim.Op.Kind != KindServing { |
| + t.Fatalf("completed root retirement = %+v, %v", stored, err) |
| + } |
| + }) |
| } |
| - if calls != 6 { |
| - t.Fatalf("duplicate consumed IDs: %d", calls) |
| +} |
| + |
| +func TestPublishGenerationChain(t *testing.T) { |
| + for _, kind := range []string{KindServing, KindMoving, KindLanding, KindReverting, KindRestoring, KindPushing} { |
| + t.Run(kind, func(t *testing.T) { |
| + ctx := context.Background() |
| + s := release.NewDirStore(t.TempDir()) |
| + o := servingClaim(t, ctx, s) |
| + if kind != KindServing { |
| + o = updateGenerationTest(t, ctx, s, o, func(c *Claim) error { |
| + c.Op = Op{Kind: kind, ID: "move", Phase: "probe", Exec: "boxT", Journal: map[string]string{"source": "unchanged"}} |
| + return nil |
| + }) |
| + } |
| + for _, generation := range []string{"G1", "G2"} { |
| + o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{ObservedOpID: o.Claim.Op.ID, ObservedPhase: o.Claim.Op.Phase, |
| + ObservedSession: o.Claim.Holder.Session, ObservedGeneration: o.Claim.Holder.Generation, |
| + Box: o.Claim.Op.Exec, Mode: "cold", NewGeneration: generation})) |
| + } |
| + before := o |
| + calls := 0 |
| + newID := func() string { calls++; return fmt.Sprintf("id%d", calls) } |
| + publish := PublishGeneration(o.Claim.Op.ID, o.Claim.Op.Phase, o.Claim.Holder.Session, "G2", "ie", newID) |
| + o = updateGenerationTest(t, ctx, s, o, publish) |
| + op := before.Claim.Op.ID |
| + if op == "" { |
| + op = fmt.Sprintf("session:%d", before.Claim.Holder.Session) |
| + } |
| + want := *before.Claim |
| + holder := *want.Holder |
| + want.Holder = &holder |
| + want.Gen++ |
| + holder.Admissions = append([]AdmissionRecord(nil), holder.Admissions...) |
| + for i := range holder.Admissions { |
| + holder.Admissions[i].Covered = true |
| + } |
| + want.Obligations = []Obligation{ |
| + {ID: "id1", Op: op, Kind: "park-generation", Region: "ie", Generation: RootGeneration, Retirement: "id2", Owner: "region:ie"}, |
| + {ID: "id3", Op: op, Kind: "park-generation", Region: "ie", Generation: "G1", Retirement: "id4", Owner: "region:ie"}, |
| + } |
| + if calls != 4 || !reflect.DeepEqual(o.Claim, &want) { |
| + t.Fatalf("publication = %+v, want %+v; ID calls %d", o.Claim, &want, calls) |
| + } |
| + o = updateGenerationTest(t, ctx, s, o, publish) |
| + want.Gen++ |
| + if calls != 4 || !reflect.DeepEqual(o.Claim, &want) { |
| + t.Fatalf("republication changed coverage or chores: %+v; calls %d", o.Claim, calls) |
| + } |
| + }) |
| } |
| - assertAdmissionStored(t, ctx, s, o) |
| } |
| |
| -func TestQueueGenerationRetireRefusals(t *testing.T) { |
| - for _, name := range []string{"no holder", "creating", "deleting", "parking", "purging", "placing", "moving", "landing", "reverting", "restoring", "pushing", "stale session", "unpublished", "current generation", "empty region"} { |
| +func TestPublishGenerationRefusals(t *testing.T) { |
| + for _, name := range []string{"no holder", "creating", "deleting", "parking", "purging", "placing", "stale op", "stale phase", "stale session", "wrong generation", "empty region", "no matching admission"} { |
| t.Run(name, func(t *testing.T) { |
| ctx := context.Background() |
| s := release.NewDirStore(t.TempDir()) |
| o := servingClaim(t, ctx, s) |
| - o, err := Update(ctx, s, o, func(c *Claim) error { |
| - c.Holder.Session, c.Holder.Generation = c.Gen+1, "gen_a" |
| + o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{Box: "adi-knitting", Mode: "cold", NewGeneration: "G1"})) |
| + o = updateGenerationTest(t, ctx, s, o, func(c *Claim) error { |
| + c.Op = Op{Kind: KindLanding, ID: "move", Phase: "probe", Exec: "boxT"} |
| if name == "no holder" { |
| c.Holder, c.Op = nil, Op{} |
| } else if knownKinds[name] { |
| - c.Op.Kind, c.Op.ID = name, "op1" |
| + c.Op.Kind = name |
| + } else if name == "no matching admission" { |
| + c.Holder.Generation = "unrecorded" |
| } |
| return nil |
| }) |
| - if err != nil { |
| - t.Fatal(err) |
| + op, phase, session, generation, region := o.Claim.Op.ID, o.Claim.Op.Phase, uint64(0), "G1", "ie" |
| + if o.Claim.Holder != nil { |
| + session, generation = o.Claim.Holder.Session, o.Claim.Holder.Generation |
| } |
| - session, published, region, retire := o.Claim.Gen, "gen_a", "ie", "old" |
| switch name { |
| + case "stale op": |
| + op = "" |
| + case "stale phase": |
| + phase = "" |
| case "stale session": |
| session-- |
| - case "unpublished": |
| - published = "old" |
| - case "current generation": |
| - retire = "gen_a" |
| + case "wrong generation": |
| + generation = "old" |
| case "empty region": |
| region = "" |
| } |
| newID := func() string { t.Fatal("refusal allocated an ID"); return "" } |
| - if _, err := Update(ctx, s, o, QueueGenerationRetire(session, published, region, retire, newID)); !errors.Is(err, ErrWrongState) { |
| - t.Fatalf("retirement = %v, want ErrWrongState", err) |
| + if _, err := Update(ctx, s, o, PublishGeneration(op, phase, session, generation, region, newID)); !errors.Is(err, ErrWrongState) { |
| + t.Fatalf("publication = %v, want ErrWrongState", err) |
| } |
| assertAdmissionStored(t, ctx, s, o) |
| }) |
| } |
| } |
| |
| -func TestAdoptAdmitRetireRootGeneration(t *testing.T) { |
| +func TestPublishGenerationCoverageBoundary(t *testing.T) { |
| ctx := context.Background() |
| s := release.NewDirStore(t.TempDir()) |
| - o, err := Read(ctx, s, testHost) |
| - if err != nil { |
| - t.Fatal(err) |
| - } |
| - o, err = Update(ctx, s, o, Adopt(newHolder(), "adi-knitting", nil, nil, "adopt1", counter())) |
| - if err != nil { |
| - t.Fatal(err) |
| + o := servingClaim(t, ctx, s) |
| + o = updateGenerationTest(t, ctx, s, o, func(c *Claim) error { |
| + c.Holder.Generation = "G2" |
| + c.Holder.Admissions = []AdmissionRecord{ |
| + {Generation: "old", Predecessor: "already-covered", Covered: true}, |
| + {Generation: "G1", Predecessor: ""}, |
| + {Generation: "G1", Predecessor: RootGeneration}, |
| + {Generation: "G2", Predecessor: "G1"}, |
| + {Generation: "G2", Predecessor: "G2", Mode: "warm"}, |
| + {Generation: "later", Predecessor: "G2"}, |
| + } |
| + c.Obligations = []Obligation{ |
| + {ID: "existing", Op: "old-op", Kind: "park-generation", Region: "ie", Generation: "G1", Retirement: "ret1", Owner: "region:ie"}, |
| + {ID: "other-region", Op: "old-op", Kind: "park-generation", Region: "uk", Generation: RootGeneration, Retirement: "ret2", Owner: "region:uk"}, |
| + } |
| + return nil |
| + }) |
| + before := o |
| + calls := 0 |
| + newID := func() string { calls++; return fmt.Sprintf("id%d", calls) } |
| + o = updateGenerationTest(t, ctx, s, o, PublishGeneration("", "", 0, "G2", "ie", newID)) |
| + want := append(append([]Obligation(nil), before.Claim.Obligations...), Obligation{ |
| + ID: "id1", Op: "session:0", Kind: "park-generation", Region: "ie", Generation: RootGeneration, Retirement: "id2", Owner: "region:ie", |
| + }) |
| + if calls != 2 || !reflect.DeepEqual(o.Claim.Obligations, want) { |
| + t.Fatalf("deduplicated chores = %+v, want %+v; calls %d", o.Claim.Obligations, want, calls) |
| } |
| - if o.Claim.Holder.Generation != "" || RootGeneration != "@root" { |
| - t.Fatalf("legacy generation = %q, root sentinel = %q", o.Claim.Holder.Generation, RootGeneration) |
| + for i, record := range o.Claim.Holder.Admissions { |
| + if record.Covered != (i < 5) { |
| + t.Fatalf("record %d covered = %t", i, record.Covered) |
| + } |
| } |
| - before := *o.Claim.Holder |
| - o = assertAdmissionStored(t, ctx, s, o) |
| - o, err = Update(ctx, s, o, AdmitWriter(Admission{ObservedSession: before.Session, ObservedGeneration: "", Box: "adi-knitting", Mode: "cold", NewGeneration: "G1", PredecessorCertified: true})) |
| - if err != nil { |
| - t.Fatal(err) |
| +} |
| + |
| +func TestPublishGenerationActiveRoot(t *testing.T) { |
| + ctx := context.Background() |
| + s := release.NewDirStore(t.TempDir()) |
| + o := servingClaim(t, ctx, s) |
| + o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{Box: "adi-knitting", Mode: "warm", PredecessorCertified: true})) |
| + o = updateGenerationTest(t, ctx, s, o, PublishGeneration("", "", o.Claim.Holder.Session, "", "ie", func() string { |
| + t.Fatal("active root must not be retired") |
| + return "" |
| + })) |
| + if !o.Claim.Holder.Admissions[0].Covered || len(o.Claim.Obligations) != 0 { |
| + t.Fatalf("active root publication = %+v", o.Claim) |
| } |
| - want := fmt.Sprintf(`[{"session":%d,"box":"adi-knitting","generation":"G1","stamp":%d,"mode":"cold","predecessor":{"session":%d,"box":"","generation":"","stamp":%d}}]`, o.Claim.Gen, o.Claim.Gen, before.Session, before.Stamp) |
| - assertWriterJournal(t, o.Claim, want) |
| - if _, err := Update(ctx, s, o, QueueGenerationRetire(o.Claim.Holder.Session, "", "ie", RootGeneration, counter())); !errors.Is(err, ErrWrongState) { |
| - t.Fatalf("retiring unfinished admission baseline = %v, want ErrWrongState", err) |
| +} |
| + |
| +func TestAdmitWriterTrimsCoveredPrefix(t *testing.T) { |
| + for _, name := range []string{"all covered", "uncovered first", "uncovered middle"} { |
| + t.Run(name, func(t *testing.T) { |
| + ctx := context.Background() |
| + s := release.NewDirStore(t.TempDir()) |
| + o := servingClaim(t, ctx, s) |
| + for i := 1; i <= 10; i++ { |
| + o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{ObservedSession: o.Claim.Holder.Session, ObservedGeneration: o.Claim.Holder.Generation, |
| + Box: o.Claim.Op.Exec, Mode: "cold", NewGeneration: fmt.Sprintf("G%d", i)})) |
| + } |
| + if len(o.Claim.Holder.Admissions) != 10 { |
| + t.Fatalf("uncovered records were trimmed: %+v", o.Claim.Holder.Admissions) |
| + } |
| + o = updateGenerationTest(t, ctx, s, o, PublishGeneration("", "", o.Claim.Holder.Session, "G10", "ie", counter())) |
| + if name != "all covered" { |
| + o = updateGenerationTest(t, ctx, s, o, func(c *Claim) error { |
| + i := 0 |
| + if name == "uncovered middle" { |
| + i = 1 |
| + } |
| + c.Holder.Admissions[i].Covered = false |
| + return nil |
| + }) |
| + } |
| + before := o.Claim.Holder.Admissions |
| + o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{ObservedSession: o.Claim.Holder.Session, ObservedGeneration: "G10", |
| + Box: o.Claim.Op.Exec, Mode: "cold", NewGeneration: "G11", PredecessorCertified: true})) |
| + drop := 3 |
| + if name == "uncovered first" { |
| + drop = 0 |
| + } else if name == "uncovered middle" { |
| + drop = 1 |
| + } |
| + want := append(append([]AdmissionRecord(nil), before[drop:]...), AdmissionRecord{Session: o.Claim.Gen, Box: o.Claim.Op.Exec, Generation: "G11", Predecessor: "G10", Mode: "cold"}) |
| + assertAdmissions(t, o.Claim, want) |
| + }) |
| } |
| - assertAdmissionStored(t, ctx, s, o) |
| - o, err = Update(ctx, s, o, QueueGenerationRetire(o.Claim.Holder.Session, "G1", "ie", RootGeneration, counter())) |
| +} |
| + |
| +func TestAdmissionRecordJSON(t *testing.T) { |
| + ctx := context.Background() |
| + s := release.NewDirStore(t.TempDir()) |
| + o := servingClaim(t, ctx, s) |
| + o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{Box: "adi-knitting", Mode: "cold", NewGeneration: "G1", RemoteMaxTXID: 42})) |
| + o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{ObservedSession: o.Claim.Holder.Session, ObservedGeneration: "G1", Box: "adi-knitting", Mode: "cold", NewGeneration: "G2", Coverage: "sealed", PredecessorCertified: true})) |
| + o = updateGenerationTest(t, ctx, s, o, PublishGeneration("", "", o.Claim.Holder.Session, "G2", "ie", counter())) |
| + raw, err := json.Marshal(o.Claim.Holder) |
| if err != nil { |
| t.Fatal(err) |
| } |
| - o = assertAdmissionStored(t, ctx, s, o) |
| - if len(o.Claim.Obligations) != 1 { |
| - t.Fatalf("obligations = %+v", o.Claim.Obligations) |
| - } |
| - ob := o.Claim.Obligations[0] |
| - if ob.Generation != "@root" || ob.Kind != "park-generation" || ob.Region != "ie" || ob.Owner != "region:ie" || ob.Op != fmt.Sprintf("session:%d", o.Claim.Holder.Session) { |
| - t.Fatalf("root obligation = %+v", ob) |
| + var holder map[string]json.RawMessage |
| + if err := json.Unmarshal(raw, &holder); err != nil { |
| + t.Fatal(err) |
| } |
| - holder := *o.Claim.Holder |
| - o, err = Update(ctx, s, o, CompleteObligation(ob.Op, ob.ID)) |
| - if err != nil { |
| + var got []map[string]any |
| + if err := json.Unmarshal(holder["admissions"], &got); err != nil { |
| t.Fatal(err) |
| } |
| - if len(o.Claim.Obligations) != 0 || !reflect.DeepEqual(*o.Claim.Holder, holder) || o.Claim.Op.Kind != KindServing { |
| - t.Fatalf("completed root retirement = %+v", o.Claim) |
| + want := []map[string]any{ |
| + {"session": float64(o.Claim.Holder.Admissions[0].Session), "box": "adi-knitting", "generation": "G1", "mode": "cold", "covered": true, |
| + "writes_lost": map[string]any{"box": "", "generation": "", "session": float64(0), "remote_max_txid": float64(42)}}, |
| + {"session": float64(o.Claim.Holder.Session), "box": "adi-knitting", "generation": "G2", "predecessor": "G1", "mode": "cold", "coverage": "sealed", "covered": true}, |
| } |
| - stored, err := Read(ctx, s, testHost) |
| - if err != nil { |
| - t.Fatal(err) |
| + if !reflect.DeepEqual(got, want) { |
| + t.Fatalf("admissions JSON = %s, want %+v", holder["admissions"], want) |
| } |
| - if stored.ETag != o.ETag || len(stored.Claim.Obligations) != 0 || !reflect.DeepEqual(*stored.Claim.Holder, holder) || stored.Claim.Op.Kind != KindServing { |
| - t.Fatalf("stored root retirement = %+v", stored) |
| +} |
| + |
| +func TestAdmitWriterIgnoresLegacyWriterJournal(t *testing.T) { |
| + ctx := context.Background() |
| + s := release.NewDirStore(t.TempDir()) |
| + o := servingClaim(t, ctx, s) |
| + o = updateGenerationTest(t, ctx, s, o, func(c *Claim) error { |
| + c.Op.Journal = map[string]string{"writers": "not-json", "source": "unchanged"} |
| + return nil |
| + }) |
| + before := o.Claim.Op |
| + o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{Box: "adi-knitting", Mode: "cold", NewGeneration: "G1"})) |
| + if !reflect.DeepEqual(o.Claim.Op, before) || len(o.Claim.Holder.Admissions) != 1 { |
| + t.Fatalf("legacy journal affected admission: %+v", o.Claim) |
| } |
| } |