moves-phase1-6 Task 3 fix round 3 (3): remove MoveRecoveryGrant.Log
Nothing calls it once the drainer delivers the console's entries: a grant is spent before its own transition's entry exists. Its test, its B8 census row and its authority census selection go with it; moveLogName moves to the writer, whose checks are the ones platform-1e set for Log.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 files changed,
+17
−174
internal/console/claim_authority_census_test.go+0 −3internal/hostops/move_log_writer.go+16 −1internal/hostops/move_log_writer_test.go+1 −1internal/hostops/move_recovery_grant.go+0 −75internal/hostops/move_recovery_grant_test.go+0 −90internal/provision/plan9_b8_census_test.go+0 −4
diff --git a/internal/console/claim_authority_census_test.go b/internal/console/claim_authority_census_test.go| index 6c5a51572..5866ca06f 100644 |
| --- a/internal/console/claim_authority_census_test.go |
| +++ b/internal/console/claim_authority_census_test.go |
| @@ -29,9 +29,6 @@ func claimAuthoritySites(t *testing.T) []string { |
| // The console's move pass (moves Plan 5 Task 7): every write of the |
| // move-recovery grant, and the lost target's stamp. |
| "ResolveStart": true, "Convert": true, "Claim": true, "Record": true, "StampLostMoveTargetParked": true, "stampLostMoveTarget": true, |
| - // The move log the pass writes through the grant (Plan 6 Task 3, fix |
| - // round 2 ruling 4): an entry of the grant's own move, create-only. |
| - "Log": true, |
| // The recovery pass's move log writer (Plan 6 Task 3, fix round 3 |
| // ruling 1): minted inside RunRecovery for the console's outbox |
| // drainer; one create-only entry per call, under the entry's own |
diff --git a/internal/hostops/move_log_writer.go b/internal/hostops/move_log_writer.go| index 7a31c0ce9..11f4753d9 100644 |
| --- a/internal/hostops/move_log_writer.go |
| +++ b/internal/hostops/move_log_writer.go |
| @@ -55,7 +55,8 @@ func runMoveLogDrain(ctx context.Context, hook func(context.Context, *MoveLogWri |
| return hook(ctx, lw) |
| } |
| -// WriteMoveLog writes one entry, with the checks MoveRecoveryGrant.Log made: |
| +// WriteMoveLog writes one entry, with the checks platform-1e set for the |
| +// grant's Log, which this writer replaced (fix round 3, ruling 3): |
| // |
| // - The key is derived, never given: e.Name under the report prefix of |
| // e's account, app, host and move. Each is a valid name; the move id and |
| @@ -113,3 +114,17 @@ func (lw *MoveLogWriter) WriteMoveLog(ctx context.Context, e MoveLogEntry) (crea |
| } |
| return false, nil |
| } |
| + |
| +// moveLogName is one path segment of letters, digits, '_' and '-', at most |
| +// 128 bytes: every identity the move log names (moves.TransitionOf) fits. |
| +func moveLogName(name string) bool { |
| + if name == "" || len(name) > 128 { |
| + return false |
| + } |
| + for _, r := range name { |
| + if !(r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '_' || r == '-') { |
| + return false |
| + } |
| + } |
| + return true |
| +} |
diff --git a/internal/hostops/move_log_writer_test.go b/internal/hostops/move_log_writer_test.go| index 26d1e49c0..a10c21058 100644 |
| --- a/internal/hostops/move_log_writer_test.go |
| +++ b/internal/hostops/move_log_writer_test.go |
| @@ -14,7 +14,7 @@ import ( |
| // Plan 6 Task 3 fix round 3, ruling 1: the recovery pass's move log writer |
| // (Recovery.MoveLogs) delivers the console's queued entries whatever their |
| -// moves now are, with Grant.Log's key derivation and checks: the key is |
| +// moves now are, with the grant Log's key derivation and checks: the key is |
| // derived from the entry's account, app, host and move under the report |
| // prefix; the name is one segment and the entry's identity; the entry is at |
| // most MoveLogEntryMax bytes and this move's and host's; the host's claim |
diff --git a/internal/hostops/move_recovery_grant.go b/internal/hostops/move_recovery_grant.go| index f1e925ba1..690ed4eb5 100644 |
| --- a/internal/hostops/move_recovery_grant.go |
| +++ b/internal/hostops/move_recovery_grant.go |
| @@ -1,7 +1,6 @@ |
| package hostops |
| import ( |
| - "bytes" |
| "context" |
| "crypto/rand" |
| "encoding/json" |
| @@ -126,80 +125,6 @@ func (g *MoveRecoveryGrant) current(ctx context.Context) (hostclaim.Observed, Re |
| return o, r, rec, err |
| } |
| -// Log writes one entry of this grant's move's log (Plan 6 Task 3; the |
| -// conditions are platform-1e's, fix round 2 ruling 4): |
| -// |
| -// - The key is the grant's alone: name under the report prefix of the |
| -// grant's move (its account, app, host and move id), whose record still |
| -// names that move at the grant's incarnation, live or closed as its |
| -// last. The caller gives only name, one segment, which must be the |
| -// entry's identity. |
| -// - body is at most MoveLogEntryMax bytes and decodes, strictly, as a |
| -// moves.LogEvent of this move and host. |
| -// - The write is create-only. A replay of the same bytes is a no-op |
| -// (created false); other bytes under the name are ErrMoveLogTaken. |
| -// - It answers only while the grant is valid, by the check every other |
| -// method makes (current): never after the grant's one mutation, after |
| -// its hook, or across a changed claim or record. A pass logs what it |
| -// owes before it acts; an entry its own write makes waits in the |
| -// caller's outbox for the next pass's grant. |
| -// |
| -// An entry records a transition and is never an input to one. |
| -func (g *MoveRecoveryGrant) Log(ctx context.Context, name string, body []byte) (created bool, err error) { |
| - if g == nil || g.state == nil { |
| - return false, ErrPending |
| - } |
| - g.state.mu.Lock() |
| - defer g.state.mu.Unlock() |
| - _, _, rec, err := g.current(ctx) |
| - if err != nil { |
| - return false, err |
| - } |
| - if !moveLogName(name) || len(body) > MoveLogEntryMax { |
| - return false, ErrIdentity |
| - } |
| - var ev moves.LogEvent |
| - if err := strictJSON(body, &ev); err != nil { |
| - return false, ErrIdentity |
| - } |
| - if ev.V != 1 || ev.Op != g.o.OpID || ev.Host != g.o.Host || ev.Identity != name { |
| - return false, ErrIdentity |
| - } |
| - live := rec.Move != nil && rec.Move.ID == g.o.OpID && rec.Move.Incarnation == g.o.Incarnation |
| - closed := rec.Move == nil && rec.LastMove != nil && rec.LastMove.ID == g.o.OpID |
| - if !live && !closed { |
| - return false, ErrIdentity |
| - } |
| - key := moves.LogKeyFor(moves.ReportPrefix(g.o.Account, g.o.App, g.o.Host, g.o.OpID), name) |
| - if _, err := g.w.raw.Put(ctx, key, body, ""); err == nil { |
| - return true, nil |
| - } else if !errors.Is(err, release.ErrCASConflict) { |
| - return false, err |
| - } |
| - have, _, err := g.w.raw.Get(ctx, key) |
| - if err != nil { |
| - return false, err |
| - } |
| - if !bytes.Equal(have, body) { |
| - return false, ErrMoveLogTaken |
| - } |
| - return false, nil |
| -} |
| - |
| -// moveLogName is one path segment of letters, digits, '_' and '-', at most |
| -// 128 bytes: every identity the move log names (moves.TransitionOf) fits. |
| -func moveLogName(name string) bool { |
| - if name == "" || len(name) > 128 { |
| - return false |
| - } |
| - for _, r := range name { |
| - if !(r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '_' || r == '-') { |
| - return false |
| - } |
| - } |
| - return true |
| -} |
| - |
| // Transition is what the grant's record write did, named as the move log |
| // names it (moves.TransitionOf over the record before and after the write), |
| // once Record has written. It is a value, never authority: the caller's log |
diff --git a/internal/hostops/move_recovery_grant_test.go b/internal/hostops/move_recovery_grant_test.go| index 827032803..470289fc4 100644 |
| --- a/internal/hostops/move_recovery_grant_test.go |
| +++ b/internal/hostops/move_recovery_grant_test.go |
| @@ -2,15 +2,12 @@ package hostops |
| import ( |
| "context" |
| - "encoding/json" |
| "errors" |
| "fmt" |
| - "strings" |
| "testing" |
| "time" |
| "github.com/carlosframework/platform/internal/hostclaim" |
| - "github.com/carlosframework/platform/internal/moves" |
| "github.com/carlosframework/platform/internal/provision" |
| "github.com/carlosframework/platform/internal/release" |
| ) |
| @@ -185,93 +182,6 @@ func TestMoveRecoveryRecordCloseThenRelease(t *testing.T) { |
| } |
| } |
| -// Plan 6 Task 3 fix round 2, ruling 4 (platform-1e's conditions): the |
| -// grant's Log writes one entry of its own move's log. The key is derived |
| -// from the grant alone (its move's report prefix); the caller names only a |
| -// single-segment entry. The entry is at most MoveLogEntryMax bytes, is this |
| -// move's and this host's, and is named by its identity. The write is |
| -// create-only: a replay of the same bytes is a no-op, other bytes under the |
| -// name are ErrMoveLogTaken. Log answers only while the grant is valid, by |
| -// the check every other grant method uses: not after its one mutation, and |
| -// not after its hook. |
| -func TestMoveRecoveryGrantLogsItsOwnMove(t *testing.T) { |
| - w := newC7World(t, func(c *hostclaim.Claim, r *provision.Record, _ *AppAdmission) { |
| - c.Op.Kind, c.Op.Phase, c.Op.Exec = hostclaim.KindLanding, hostclaim.PhaseCommitted, "target" |
| - c.Obligations = nil |
| - r.Region = "uk" |
| - r.Move.State = provision.MoveCommitted |
| - now := r.CreatedAt |
| - r.Move.CommittedAt = &now |
| - r.Move.OpenedAt = &now |
| - r.Move.SourceParkedAt = &now |
| - }) |
| - ctx := context.Background() |
| - entry := func(op, host, id string) []byte { |
| - b, err := json.Marshal(moves.LogEvent{V: 1, Op: op, Host: host, Event: "x", Identity: id, Actor: "console", At: w.now}) |
| - if err != nil { |
| - t.Fatal(err) |
| - } |
| - return b |
| - } |
| - body := entry("move", w.host, "opened") |
| - var saved *MoveRecoveryGrant |
| - got := map[string]error{} |
| - var created, again bool |
| - err := w.control.RunRecovery(ctx, func(ctx context.Context) error { |
| - return runMoveRecovery(ctx, w.claim(t), func(ctx context.Context, g *MoveRecoveryGrant) error { |
| - saved = g |
| - var err error |
| - if created, err = g.Log(ctx, "opened", body); err != nil { |
| - return err |
| - } |
| - if again, err = g.Log(ctx, "opened", body); err != nil { |
| - return err |
| - } |
| - other, _ := json.Marshal(moves.LogEvent{V: 1, Op: "move", Host: w.host, Event: "x", Identity: "opened", Actor: "target", At: w.now}) |
| - _, got["other bytes"] = g.Log(ctx, "opened", other) |
| - _, got["another move"] = g.Log(ctx, "other", entry("other", w.host, "other")) |
| - _, got["another host"] = g.Log(ctx, "elsewhere", entry("move", "elsewhere.example", "elsewhere")) |
| - _, got["a name that is not the identity"] = g.Log(ctx, "done", entry("move", w.host, "opened")) |
| - _, got["a path"] = g.Log(ctx, "../source", entry("move", w.host, "../source")) |
| - _, got["an empty name"] = g.Log(ctx, "", entry("move", w.host, "")) |
| - big := moves.LogEvent{V: 1, Op: "move", Host: w.host, Event: "x", Identity: "big", Fields: map[string]string{"pad": strings.Repeat("a", MoveLogEntryMax)}} |
| - bb, _ := json.Marshal(big) |
| - _, got["too big"] = g.Log(ctx, "big", bb) |
| - if err := g.Record(ctx, RecoveryMoveClose); err != nil { |
| - return err |
| - } |
| - _, got["after the mutation"] = g.Log(ctx, "done", entry("move", w.host, "done")) |
| - return nil |
| - }) |
| - }) |
| - if err != nil { |
| - t.Fatal(err) |
| - } |
| - if !created || again { |
| - t.Fatalf("created=%v, a replay of the same bytes created=%v", created, again) |
| - } |
| - for what, want := range map[string]error{ |
| - "other bytes": ErrMoveLogTaken, "another move": ErrIdentity, "another host": ErrIdentity, |
| - "a name that is not the identity": ErrIdentity, "a path": ErrIdentity, "an empty name": ErrIdentity, |
| - "too big": ErrIdentity, "after the mutation": ErrPending, |
| - } { |
| - if !errors.Is(got[what], want) { |
| - t.Errorf("%s: %v, want %v", what, got[what], want) |
| - } |
| - } |
| - logged, err := moves.ReadLog(ctx, w.raw, moves.ReportPrefix("bab", "app", w.host, "move")) |
| - if err != nil || len(logged) != 1 || logged[0].Identity != "opened" { |
| - t.Fatalf("log %+v %v", logged, err) |
| - } |
| - raw, _, err := w.raw.Get(ctx, moves.LogKeyFor(moves.ReportPrefix("bab", "app", w.host, "move"), "opened")) |
| - if err != nil || string(raw) != string(body) { |
| - t.Fatalf("the object is not the caller's bytes: %s %v", raw, err) |
| - } |
| - if _, err := saved.Log(ctx, "late", entry("move", w.host, "late")); !errors.Is(err, ErrPending) { |
| - t.Fatalf("a grant kept past its hook logged: %v", err) |
| - } |
| -} |
| - |
| func TestMoveRecoveryStartRequiresDurableBaseAndHonorsAppliedJournal(t *testing.T) { |
| for _, marker := range []string{"missing-base", "applied-effect", "issued"} { |
| t.Run(marker, func(t *testing.T) { |
diff --git a/internal/provision/plan9_b8_census_test.go b/internal/provision/plan9_b8_census_test.go| index 307ca32c2..b0b4e1bf0 100644 |
| --- a/internal/provision/plan9_b8_census_test.go |
| +++ b/internal/provision/plan9_b8_census_test.go |
| @@ -477,10 +477,6 @@ var plan9B8CensusInventory = []censusEntry{ |
| // log under its report prefix, for a host whose claim still names the |
| // app; never a record (Plan 6 Task 3 fix round 3, ruling 1). |
| {"internal/hostops/move_log_writer.go", "MoveLogWriter.WriteMoveLog", "nonrecord", "sel:Put", "9dffdb6f7f15f7dfffc104e0f09df4b6c6d7868a31ee1c85cfb811a04ce5c90c"}, |
| - // Writes one entry of the grant's own move's log, create-only, under its |
| - // report prefix (moves/<acct>/<app>/<host>/<op>/log/); never a record |
| - // (Plan 6 Task 3 fix round 2, ruling 4). |
| - {"internal/hostops/move_recovery_grant.go", "MoveRecoveryGrant.Log", "nonrecord", "sel:Put", "9f90ff153382fc6278ed0c17dffa5e0bfda01287716af1fa3149fef9772213c2"}, |
| {"internal/hostops/move_recovery_grant.go", "MoveRecoveryGrant.Record", "record-writer", "key:WriteRecord", "f7f04a9e2c579eaa7d254ce6310e2726d95855307d61f3f442ee9c1fa2a5d825"}, |
| // Plan 10: reviewed typed console move and loss writers. |
| {"internal/hostops/console_move_grant.go", "ControlWriter.ConsoleMove", "record-writer", "key:WriteRecord", "21e7a6ddc4dbe5b3bf06e65b39a228e2eb588af1edc43beb09d91dd72570dd44"}, |