Reconcile docs with reality; fix module graph, token compare, health #1

Open
kami wants to merge 216 commits from webui-and-audit-reconciliation into master
7 changed files with 120 additions and 5 deletions
Showing only changes of commit 6565b9fce2 - Show all commits
+6 -3
View File
@@ -119,6 +119,7 @@ type lease struct {
}
type releaseTransaction struct {
ID string `json:"id"`
LeaseEpoch string `json:"lease_epoch,omitempty"`
LeaseVersion int `json:"lease_version"`
Ref string `json:"handoff_ref,omitempty"`
AnchorSHA string `json:"anchor_sha,omitempty"`
@@ -836,7 +837,7 @@ func (w *worker) advanceRelease(ctx context.Context, id string, s herdr.Session)
w.recordError(fmt.Errorf("release %s: lease missing", id))
return
}
tx = releaseTransaction{ID: domain.NewID(), LeaseVersion: l.Version, Phase: "prepared", UpdatedAt: time.Now().UTC()}
tx = releaseTransaction{ID: domain.NewID(), LeaseEpoch: l.Epoch, LeaseVersion: l.Version, Phase: "prepared", UpdatedAt: time.Now().UTC()}
w.releases[id] = tx
_ = w.save()
}
@@ -864,8 +865,10 @@ func (w *worker) advanceRelease(ctx context.Context, id string, s herdr.Session)
_ = w.save()
}
if tx.Phase == "anchor_pushed" {
l := w.leases[id]
if err := w.api.Release(ctx, id, tx.Ref, tx.AnchorSHA, tx.ID, l.Epoch, tx.LeaseVersion, w.sessionEvidence(ctx, id, s)); err != nil {
// The epoch comes from the transaction, not from w.leases: an expiry
// replay deletes the lease, and the coordinator needs the epoch of the
// lease this anchor was pushed under to accept the late commit.
if err := w.api.Release(ctx, id, tx.Ref, tx.AnchorSHA, tx.ID, tx.LeaseEpoch, tx.LeaseVersion, w.sessionEvidence(ctx, id, s)); err != nil {
tx.LastError, tx.UpdatedAt = err.Error(), time.Now().UTC()
w.releases[id] = tx
_ = w.save()
+37
View File
@@ -1123,3 +1123,40 @@ func TestHoldsLeaseGatesWorkOnATaskTheWorkerLost(t *testing.T) {
t.Error("a task this worker never leased is held")
}
}
// Run 10 lost a finished task here: the anchor was pushed, the lease expired,
// and every retry sent the epoch from w.leases — which the expiry replay had
// already deleted. An empty epoch can never be accepted, so the work sat in
// the worktree until retry_limit. The epoch belongs to the transaction.
func TestExpiredReleaseStillCommitsWithTheTransactionEpoch(t *testing.T) {
var sent struct {
LeaseEpoch string `json:"lease_epoch"`
}
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/handoff") {
_ = json.NewDecoder(r.Body).Decode(&sent)
w.WriteHeader(http.StatusNoContent)
return
}
w.WriteHeader(http.StatusNoContent)
}))
defer s.Close()
w := &worker{
api: federation.Client{BaseURL: s.URL, WorkerID: "h", Token: "t"},
harnessID: "h",
tasks: map[string]domain.Task{"t": {ID: "t"}},
sessions: map[string]herdr.Session{"t": {PaneID: "pane"}},
leases: map[string]lease{}, // expiry deleted it
releases: map[string]releaseTransaction{"t": {ID: "tx", Phase: "anchor_pushed", Ref: "sha256:abc", AnchorSHA: "abc", LeaseEpoch: "e1"}},
quarantined: map[string]bool{},
statePath: t.TempDir() + "/state.json",
hard: .75,
}
w.advanceRelease(context.Background(), "t", w.sessions["t"])
if sent.LeaseEpoch != "e1" {
t.Fatalf("release sent epoch %q, want the transaction's e1", sent.LeaseEpoch)
}
if got := w.releases["t"].Phase; got != "event_committed" {
t.Fatalf("phase %q, want event_committed", got)
}
}
+18 -2
View File
@@ -162,6 +162,19 @@ func validateLocalMachine(rr registry.Registry, localMachine string) error {
return nil
}
// lateHandoffAccepted reports whether a release may still commit after its
// lease expired. A worker pushes the anchor first and commits second; when the
// lease dies in between, the finished work is durable in git and unreachable
// forever (run 10 lost a whole task that way). The expired owner is the only
// caller that can present the ended lease's epoch, and this fires only while
// the task is unleased and its reclaim recorded no handoff, so it can never
// overwrite a successor's work. Store.Append's version fence settles a race
// with a concurrent re-lease.
func lateHandoffAccepted(t domain.Task, leaseEpoch string) bool {
return t.State == domain.StateQueued && t.Lease == nil && t.HandoffRef == "" &&
leaseEpoch != "" && leaseEpoch == t.LastLeaseEpoch
}
func main() {
dir := os.Getenv("ORCHESTRA_DATA")
if dir == "" {
@@ -1428,7 +1441,8 @@ func main() {
w.WriteHeader(http.StatusNoContent)
return
}
if !ownedLease {
lateHandoff := !ownedLease && strings.HasSuffix(r.URL.Path, "/handoff") && lateHandoffAccepted(t, b.LeaseEpoch)
if !ownedLease && !lateHandoff {
http.Error(w, "lease not owned", 409)
return
}
@@ -1656,7 +1670,9 @@ func main() {
http.Error(w, "anchor_sha required", 400)
return
}
if b.TransactionID == "" || b.ExpectedVersion != t.Version {
// A late handoff cannot know the version: expiry bumped it after the
// worker read it. Its fence is the epoch plus the append.
if b.TransactionID == "" || (!lateHandoff && b.ExpectedVersion != t.Version) {
http.Error(w, "release transaction and current lease version required", http.StatusConflict)
return
}
+20
View File
@@ -6,6 +6,7 @@ import (
"testing"
"time"
"orchestra/internal/domain"
"orchestra/internal/registry"
)
@@ -89,3 +90,22 @@ func TestTmuxRegistryRequiresMachineIdentityEvenOnOneMachine(t *testing.T) {
t.Fatal(err)
}
}
func TestLateHandoffAcceptedOnlyFromTheExpiredOwner(t *testing.T) {
expired := domain.Task{State: domain.StateQueued, LastLeaseEpoch: "ep1"}
if !lateHandoffAccepted(expired, "ep1") {
t.Fatal("the expired owner must still commit its pushed anchor")
}
for name, tk := range map[string]domain.Task{
"re-leased": {State: domain.StateLeased, Lease: &domain.Lease{Epoch: "ep2"}, LastLeaseEpoch: "ep1"},
"already handed off": {State: domain.StateQueued, LastLeaseEpoch: "ep1", HandoffRef: "sha256:x"},
"blocked": {State: domain.StateBlocked, LastLeaseEpoch: "ep1"},
} {
if lateHandoffAccepted(tk, "ep1") {
t.Fatalf("%s must refuse a late handoff", name)
}
}
if lateHandoffAccepted(expired, "ep2") || lateHandoffAccepted(domain.Task{State: domain.StateQueued}, "") {
t.Fatal("a foreign or empty epoch must refuse")
}
}
+6
View File
@@ -173,6 +173,12 @@ type Task struct {
BlockedAt time.Time `json:"blocked_at,omitempty"`
LastPaneID string `json:"last_pane_id,omitempty"`
LastHarness string `json:"last_harness_id,omitempty"`
// LastLeaseEpoch is the fencing token of the lease that most recently
// ended. A worker can push its release anchor and only then discover the
// lease expired; the finished work is durable in git but the commit can
// never land. Retaining the epoch lets exactly that owner still commit
// while the task sits unleased.
LastLeaseEpoch string `json:"last_lease_epoch,omitempty"`
PaneState string `json:"pane_state,omitempty"` // open, closed, unreachable, unknown
LastSession SessionEvidence `json:"last_session,omitempty"`
// Recovery state is part of the durable projection, never process-local
+3
View File
@@ -387,6 +387,9 @@ func (s *Store) apply(e domain.Event) error {
case "TaskLaunchAcknowledged":
t.LifecyclePhase = "started"
case "TaskReleased":
if t.Lease != nil {
t.LastLeaseEpoch = t.Lease.Epoch
}
t.State = domain.StateQueued
t.LifecyclePhase = "reclaimed"
t.Lease = nil
+30
View File
@@ -663,3 +663,33 @@ func TestQuotaSinceReportsEmptyWindowAsKnownZero(t *testing.T) {
}
const fiveHours = 5 * time.Hour
// TestExpiryRetainsLeaseEpoch covers the late-handoff fence: a worker that
// pushed its release anchor and then lost the lease can only commit if the
// projection still knows which epoch just ended.
func TestExpiryRetainsLeaseEpoch(t *testing.T) {
s, err := Open(t.TempDir())
if err != nil {
t.Fatal(err)
}
if err := s.Append(created("e1")); err != nil {
t.Fatal(err)
}
id := s.Tasks()[0].ID
if _, err := s.Lease(id, "h1", time.Millisecond); err != nil {
t.Fatal(err)
}
leased, _ := s.Task(id)
epoch := leased.Lease.Epoch
time.Sleep(2 * time.Millisecond)
if _, err := s.ExpireLease(id, time.Now()); err != nil {
t.Fatal(err)
}
after, _ := s.Task(id)
if after.Lease != nil {
t.Fatal("expired task still holds a lease")
}
if after.LastLeaseEpoch != epoch || epoch == "" {
t.Fatalf("last lease epoch %q, want %q", after.LastLeaseEpoch, epoch)
}
}