carlos / platform Public

registry: HandHold, the move's hold handed to its rollback in one statement (ruling J3)

One UPDATE ... WHERE hold_op = fromOp: the row is never unheld between
the move id and the rollback id. An unheld row, a row held by another
op and an absent row match nothing. Callers come in later tasks, under
WithHostHoldLock, and touch the stage-requested marker after a write
that landed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 49ada90d0250ebecc8c0c7d2bb65526a0600c66f parent 9315357
2 files changed, +79 −0
  • internal/registry/hold_test.go +56 −0
  • internal/registry/registry.go +23 −0
diff --git a/internal/registry/hold_test.go b/internal/registry/hold_test.go
index 34d016f56..6a4422504 100644
--- a/internal/registry/hold_test.go
+++ b/internal/registry/hold_test.go
@@ -468,3 +468,59 @@ func TestSetHoldIfIsConditionalOnTheCurrentOp(t *testing.T) {
t.Fatalf("hold after release: %v %v", ok, err)
}
}
+
+// Ruling J3: a rollback takes over the move's hold in one statement, so the
+// row is never unheld between the two ops.
+func TestHandHold(t *testing.T) {
+ const rb = "rb_abcdefghijklmnopqrstuvwxyz"
+ r := holdFixture(t, "h")
+ if err := r.SetHold("h", "landing", holdOpA); err != nil {
+ t.Fatal(err)
+ }
+ // Held by another op: nothing moves.
+ if ok, err := r.HandHold("h", "releasing", rb, holdOpB); err != nil || ok {
+ t.Fatalf("hand-over from an op that does not hold the row: %v %v", ok, err)
+ }
+ if rt, _ := r.Get("h"); rt.Status != "landing" || rt.HoldOp != holdOpA {
+ t.Fatalf("row changed by the refusal: %+v", rt)
+ }
+ // The hand-over: one write, the new op and status.
+ if ok, err := r.HandHold("h", "releasing", rb, holdOpA); err != nil || !ok {
+ t.Fatalf("hand-over: %v %v", ok, err)
+ }
+ if rt, _ := r.Get("h"); rt.Status != "releasing" || rt.HoldOp != rb {
+ t.Fatalf("after the hand-over: %+v", rt)
+ }
+ // A replay finds the row already handed over and matches nothing.
+ if ok, err := r.HandHold("h", "releasing", rb, holdOpA); err != nil || ok {
+ t.Fatalf("replay: %v %v", ok, err)
+ }
+ // An unheld row is not taken: that is SetHoldIf's, with expectOp "".
+ if err := r.Upsert(Route{Host: "plain", Kind: "instance", Socket: "/s"}); err != nil {
+ t.Fatal(err)
+ }
+ if ok, err := r.HandHold("plain", "restoring", rb, holdOpA); err != nil || ok {
+ t.Fatalf("unheld row: %v %v", ok, err)
+ }
+ if rt, _ := r.Get("plain"); rt.HoldOp != "" || IsHold(rt.Status) {
+ t.Fatalf("unheld row taken: %+v", rt)
+ }
+ // An absent row matches nothing.
+ if ok, err := r.HandHold("absent", "restoring", rb, holdOpA); err != nil || ok {
+ t.Fatalf("absent row: %v %v", ok, err)
+ }
+ // Malformed: not a hold status, an empty op on either side, the same op.
+ for name, c := range map[string][3]string{
+ "ready": {"ready", rb, rb},
+ "no to": {"restoring", "", holdOpA},
+ "no from": {"restoring", rb, ""},
+ "same op": {"restoring", rb, rb},
+ } {
+ if ok, err := r.HandHold("h", c[0], c[1], c[2]); err == nil || ok {
+ t.Fatalf("%s: %v %v", name, ok, err)
+ }
+ }
+ if rt, _ := r.Get("h"); rt.Status != "releasing" || rt.HoldOp != rb {
+ t.Fatalf("row changed by a malformed hand-over: %+v", rt)
+ }
+}
diff --git a/internal/registry/registry.go b/internal/registry/registry.go
index f8642d645..ad3a2d270 100644
--- a/internal/registry/registry.go
+++ b/internal/registry/registry.go
@@ -1118,6 +1118,29 @@ func (r *Registry) SetHoldIf(host, status, op, expectOp string) (bool, error) {
return n > 0, err
}
+// HandHold hands a held row from one op to another in one statement: the
+// row is held status under toOp only while its hold_op is still fromOp
+// (moves Plan 4, ruling J3). It is the one takeover there is, between the
+// two ops of one move: the move id hands its landing or moving hold to the
+// rollback id. Releasing first and holding after would leave the row
+// unheld between the two writes. An unheld row is never taken (SetHoldIf
+// with expectOp "" is that), and a row held by any other op is left alone.
+// false with no error means nothing matched: the row's hold changed since
+// the caller read it (a replay finds it already handed over), or there is
+// no row. Callers hold WithHostHoldLock over a fresh record read and, as
+// with SetHold, touch the stage-requested marker after a write that landed.
+func (r *Registry) HandHold(host, status, toOp, fromOp string) (bool, error) {
+ if !IsHold(status) || toOp == "" || fromOp == "" || toOp == fromOp {
+ return false, fmt.Errorf("registry: hand %s from %q to %q as %q is not a hand-over between two ops' holds", host, fromOp, toOp, status)
+ }
+ res, err := r.sql.Exec(`UPDATE routes SET status = ?, hold_op = ? WHERE host = ? AND hold_op = ?`, status, toOp, host, fromOp)
+ if err != nil {
+ return false, err
+ }
+ n, err := res.RowsAffected()
+ return n > 0, err
+}
+
// ReleaseHold is §5.2's one conditional statement: a row leaves a hold only
// under the op that holds it. false means nothing matched (a late write from a
// finished operation, an unheld row, or an empty op).