Fix #75: atomic-or-recoverable org creation #84

Merged
crueber merged 2 commits from fix/issue-75 into main 2026-09-05 01:02:23 +00:00
Owner

Fixes #75 (codex major): a failed org creation left an irrecoverable ownerless namespace (org.json reserved, members.json write failed, retries 409 forever).

Atomic-or-recoverable CreateOrg (internal/identity/orgs.go):

  • members-seed failure rolls the org.json reservation back (version-guarded Delete, best-effort) and returns the original error;
  • retry/resume on an existing org.json heals via CAS: missing-or-ownerless roster binds the caller as owner (same path heals the crash-between-writes residue), re-create by the bound owner is idempotent success, rival creates still 409 and write nothing;
  • seed-412 path re-reads and succeeds only when the caller is the bound owner (reverse-race arbitration: exactly one winner).
    Concurrency note (hazard + avoidance) on CreateOrg; store CAS is the only lock, no new mutexes.

Tests: new internal/identity/orgs_recover_test.go — fault-injected members write (fails pre-fix: reservation left behind, retry 409s; passes post-fix: retry binds owner), failed-rollback heals via resume, crash residue, ownerless roster, rival-409-keeps-roster, idempotent owner, stale-members both ways, corrupt roster, 16-way concurrent-create one-winner. http_test.go dup-org updated to the idempotent contract. internal/identity: gofmt/vet clean, go test -race green (incl. race test x10), coverage 97.3% (gate >=95%). Doc 01 Decisions entry appended (law 12).

Fixes #75 (codex major): a failed org creation left an irrecoverable ownerless namespace (org.json reserved, members.json write failed, retries 409 forever). Atomic-or-recoverable CreateOrg (internal/identity/orgs.go): - members-seed failure rolls the org.json reservation back (version-guarded Delete, best-effort) and returns the original error; - retry/resume on an existing org.json heals via CAS: missing-or-ownerless roster binds the caller as owner (same path heals the crash-between-writes residue), re-create by the bound owner is idempotent success, rival creates still 409 and write nothing; - seed-412 path re-reads and succeeds only when the caller is the bound owner (reverse-race arbitration: exactly one winner). Concurrency note (hazard + avoidance) on CreateOrg; store CAS is the only lock, no new mutexes. Tests: new internal/identity/orgs_recover_test.go — fault-injected members write (fails pre-fix: reservation left behind, retry 409s; passes post-fix: retry binds owner), failed-rollback heals via resume, crash residue, ownerless roster, rival-409-keeps-roster, idempotent owner, stale-members both ways, corrupt roster, 16-way concurrent-create one-winner. http_test.go dup-org updated to the idempotent contract. internal/identity: gofmt/vet clean, go test -race green (incl. race test x10), coverage 97.3% (gate >=95%). Doc 01 Decisions entry appended (law 12).
CreateOrg rolls the org.json reservation back (version-guarded,
best-effort) when the members.json seed fails, and resumes an ownerless
reservation via CAS on retry (heals crash-between-writes residue too);
re-create by the bound owner is idempotent, rival creates still 409 and
write nothing. Regression tests fault-inject the members write plus a
16-way concurrent-create arbitration test. Doc 01 decisions entry.
docs/features/01 §3: the seed-failure rollback now checks members.json
first — a present roster means a concurrent create healed under this
reservation and won the namespace, so arbitrate read-only via
confirmOrgOwner instead of deleting org.json from under the winner.
Adds TestCreateOrgRollbackSkipsDeleteUnderWinner (fails pre-fix).
Sign in to join this conversation.
No description provided.