package inventory import ( "context" "errors" "testing" "time" ) // Order and one writer, as the store keeps them (novox/hq to-be 45 ยง6). // **A plan is written by compare-and-set on its revision, carrying the epoch**: a write against a plan // another writer moved since is refused, and nothing is written by a process that may not act. func TestAPlanIsWrittenByCompareAndSetUnderTheLease(t *testing.T) { inv := ForTest(t) ctx := context.Background() epoch := uint64(57) inv.ActsUnder(func(context.Context) (uint64, error) { return epoch, nil }) p := Plan{ID: "plan-cas", Repository: "novox/app", Commit: "c0ffee00", Created: time.Now(), State: PlanBuilding, Tiers: [][]string{{"app"}}, Modules: map[string]*PlanModule{"app": {}}} if err := inv.SavePlan(ctx, &p); err != nil { t.Fatal(err) } if p.Revision != 1 || p.Epoch != 57 { t.Fatalf("a new plan was written at revision %d, epoch %d", p.Revision, p.Epoch) } // Two readers of revision 1: the first write wins, the second is refused and writes nothing. first, err := inv.PlanByID(ctx, "plan-cas") if err != nil { t.Fatal(err) } second := first first.Note = "the newer word" if err := inv.SavePlan(ctx, &first); err != nil { t.Fatal(err) } second.State = PlanFailed if err := inv.SavePlan(ctx, &second); !errors.Is(err, ErrPlanMoved) { t.Fatalf("a write against a plan moved since it was read was not refused: %v", err) } kept, _ := inv.PlanByID(ctx, "plan-cas") if kept.State != PlanBuilding || kept.Note != "the newer word" || kept.Revision != 2 { t.Fatalf("the refused write reached the plan: %+v", kept) } // The writer saves its own plan again without reading it: its revision moved with its write. first.Tier = 0 if err := inv.SavePlan(ctx, &first); err != nil { t.Fatalf("a writer could not save its own plan twice: %v", err) } // A new plan under an id already written is refused, not laid over it. again := Plan{ID: "plan-cas", Repository: "novox/app", Commit: "deadbeef", Created: time.Now(), State: PlanBuilding} if err := inv.SavePlan(ctx, &again); !errors.Is(err, ErrPlanMoved) { t.Fatalf("a second plan under one id was written: %v", err) } // And a process that does not hold the lease writes nothing. inv.ActsUnder(func(context.Context) (uint64, error) { return 0, errors.New("this controller lost the lease") }) first.Note = "from a controller that lost the lease" if err := inv.SavePlan(ctx, &first); err == nil { t.Fatal("a plan was written by a controller that does not hold the lease") } if kept, _ := inv.PlanByID(ctx, "plan-cas"); kept.Note != "the newer word" || kept.Epoch != 57 { t.Fatalf("a controller without the lease wrote the plan: %+v", kept) } } // The epochs the mesh issued: the highest is the floor; taking one ends every earlier one nobody gave // back as found expired, and says which; one given back says so and is not said again. func TestTheEpochsIssuedAreKept(t *testing.T) { inv := ForTest(t) ctx := context.Background() if highest, err := inv.HighestEpoch(ctx); err != nil || highest != 0 { t.Fatalf("a mesh that issued none has %d (%v)", highest, err) } if _, err := inv.TookEpoch(ctx, Epoch{Epoch: 41, Instance: "a", Taken: time.Now()}); err != nil { t.Fatal(err) } // a stopped renewing and said nothing; b takes the lease. expired, err := inv.TookEpoch(ctx, Epoch{Epoch: 57, Instance: "b", Taken: time.Now()}) if err != nil || len(expired) != 1 || expired[0].Epoch != 41 || expired[0].How != EpochExpired { t.Fatalf("taking 57 ended %+v (%v), want 41 found expired", expired, err) } if err := inv.EndEpoch(ctx, 57, EpochReleased); err != nil { t.Fatal(err) } expired, err = inv.TookEpoch(ctx, Epoch{Epoch: 60, Instance: "c", Taken: time.Now()}) if err != nil || len(expired) != 0 { t.Fatalf("a lease given back was found expired: %+v (%v)", expired, err) } if highest, _ := inv.HighestEpoch(ctx); highest != 60 { t.Fatalf("the highest epoch issued is %d, want 60", highest) } e, found, err := inv.EpochOf(ctx, 57) if err != nil || !found || e.Instance != "b" || e.How != EpochReleased || e.Ended == nil { t.Fatalf("epoch 57 reads %+v (%v, %v)", e, found, err) } recent, err := inv.EpochsSince(ctx, time.Now().Add(-time.Hour)) if err != nil || len(recent) != 3 || recent[0].Epoch != 60 || recent[0].Ended != nil { t.Fatalf("the epochs of the last hour read %+v (%v)", recent, err) } } // What a machine was sent under, and whether it reads an epoch. func TestTheEpochAMachineWasSentIsKept(t *testing.T) { inv := ForTest(t) ctx := context.Background() node, err := inv.AddNode(ctx, "anchor") if err != nil { t.Fatal(err) } if err := inv.RecordSentUnder(ctx, node.ID, "d1", nil, 57); err != nil { t.Fatal(err) } if e, err := inv.SentEpoch(ctx, node.ID); err != nil || e != 57 { t.Fatalf("sent under %d (%v)", e, err) } if err := inv.RecordSent(ctx, node.ID, "d2", nil); err != nil { t.Fatal(err) } if e, _ := inv.SentEpoch(ctx, node.ID); e != 0 { t.Fatalf("a declaration sent without an epoch left %d kept", e) } if reads, _ := inv.ReadsEpoch(ctx, node.ID); reads { t.Fatal("a machine that never said so reads an epoch") } }