Commit history detached from cancellation on every path, not just failure (#73)
Build image / build-and-push (push) Successful in 7s
Build image / build-and-push (push) Successful in 7s
Co-authored-by: Steve Dudenhoeffer <[email protected]>
This commit was merged in pull request #73.
This commit is contained in:
@@ -805,3 +805,91 @@ func countsTotal(counts []domain.ChangeCount) int {
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// TestSucceededTurnRecordsEvenIfTheCallerWentAway is the production bug.
|
||||
//
|
||||
// The failure path was detached from cancellation; the SUCCESS path was not. An
|
||||
// agent turn whose client disconnects can still COMPLETE — and then the success
|
||||
// path committed with a dead context, the write failed, and real changes were
|
||||
// left with no change set and no way to undo them. Found live with 18 plantings
|
||||
// behind no history at all.
|
||||
//
|
||||
// Nothing about a commit needs the caller to still be there: by the time it
|
||||
// runs, the data it describes has already been written.
|
||||
func TestSucceededTurnRecordsEvenIfTheCallerWentAway(t *testing.T) {
|
||||
s := newTestService(t, openConfig())
|
||||
owner := seedUser(t, s, "[email protected]")
|
||||
g := seedGarden(t, s, owner)
|
||||
bed := seedBed(t, s, owner, g.ID)
|
||||
plant := seedOwnPlant(t, s, owner, 15)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
before := len(history(t, s, owner, g.ID))
|
||||
|
||||
// fn does its work and SUCCEEDS, but the caller goes away before it returns.
|
||||
cs, err := s.WithChangeSet(ctx, owner, g.ID, ChangeSetOptions{
|
||||
Source: domain.SourceAgent, Summary: "plant beans in the second bed",
|
||||
}, func(ctx context.Context) error {
|
||||
if _, err := s.FillNamedRegion(ctx, owner, bed.ID, "all", plant.ID, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
cancel() // the client disconnects, mid-turn, after the work landed
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("WithChangeSet: %v", err)
|
||||
}
|
||||
if cs == nil {
|
||||
t.Fatal("the turn changed things but produced no change set")
|
||||
}
|
||||
|
||||
after := history(t, s, owner, g.ID)
|
||||
if len(after) != before+1 {
|
||||
t.Fatalf("recorded %d change sets, want 1", len(after)-before)
|
||||
}
|
||||
if after[0].Summary != "plant beans in the second bed" {
|
||||
t.Errorf("summary = %q; a completed turn shouldn't be marked partial", after[0].Summary)
|
||||
}
|
||||
// And it's undoable, which is the entire point.
|
||||
if _, conflicts, err := s.RevertChangeSet(context.Background(), owner, cs.ID, domain.SourceUI); err != nil || len(conflicts) != 0 {
|
||||
t.Fatalf("the recorded turn should be undoable: err=%v conflicts=%+v", err, conflicts)
|
||||
}
|
||||
active, _ := s.store.ListActivePlantingsForObject(context.Background(), bed.ID)
|
||||
if len(active) != 0 {
|
||||
t.Errorf("%d plantings survived the undo", len(active))
|
||||
}
|
||||
}
|
||||
|
||||
// TestAutoScopedMutationRecordsEvenIfTheCallerWentAway — the same rule for the
|
||||
// path virtually every mutation takes.
|
||||
//
|
||||
// A plain REST PATCH auto-scopes into its own change set. If the client hangs up
|
||||
// between the row landing and the change set being written, that change is
|
||||
// orphaned exactly as an agent turn's was — and this path is used far more.
|
||||
func TestAutoScopedMutationRecordsEvenIfTheCallerWentAway(t *testing.T) {
|
||||
s := newTestService(t, openConfig())
|
||||
owner := seedUser(t, s, "[email protected]")
|
||||
g := seedGarden(t, s, owner)
|
||||
bed := seedBed(t, s, owner, g.ID)
|
||||
before := len(history(t, s, owner, g.ID))
|
||||
|
||||
// The row write and the history write share this context; cancelling after
|
||||
// the mutation returns is the client hanging up mid-request.
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
if _, err := s.UpdateObject(ctx, owner, bed.ID, ObjectPatch{XCM: f64Ptr(300)}, bed.Version); err != nil {
|
||||
t.Fatalf("UpdateObject: %v", err)
|
||||
}
|
||||
cancel()
|
||||
|
||||
after := history(t, s, owner, g.ID)
|
||||
if len(after) != before+1 {
|
||||
t.Fatalf("recorded %d change sets, want 1 — the move is otherwise un-undoable", len(after)-before)
|
||||
}
|
||||
if _, conflicts, err := s.RevertChangeSet(context.Background(), owner, after[0].ID, domain.SourceUI); err != nil || len(conflicts) != 0 {
|
||||
t.Fatalf("the recorded move should be undoable: err=%v conflicts=%+v", err, conflicts)
|
||||
}
|
||||
back, _ := s.store.GetObject(context.Background(), bed.ID)
|
||||
if back.XCM != bed.XCM {
|
||||
t.Errorf("undo left x at %v, want %v", back.XCM, bed.XCM)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user