fix: make worker handoff rotation durable
This commit is contained in:
@@ -138,14 +138,34 @@ func (s *Store) apply(e domain.Event) error {
|
||||
if v, ok := p["description"].(string); ok {
|
||||
t.Description = v
|
||||
}
|
||||
if v, ok := p["acceptance"].([]any); ok {
|
||||
for _, item := range v {
|
||||
if text, ok := item.(string); ok {
|
||||
t.Acceptance = append(t.Acceptance, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
if v, ok := p["quality_gate"].(string); ok {
|
||||
t.QualityGate = v
|
||||
}
|
||||
s.external[t.Source+"\x00"+t.ExternalID] = t.ID
|
||||
case "TaskLeased":
|
||||
t.State = domain.StateLeased
|
||||
t.Lease = &domain.Lease{HarnessID: p["harness_id"].(string), Until: time.Unix(0, int64(p["until_ns"].(float64)))}
|
||||
case "TaskLeaseRenewed":
|
||||
t.Lease = &domain.Lease{HarnessID: p["harness_id"].(string), Until: time.Unix(0, int64(p["until_ns"].(float64)))}
|
||||
case "TaskReleased":
|
||||
t.State = domain.StateQueued
|
||||
t.Lease = nil
|
||||
t.HandoffRef, _ = p["handoff_ref"].(string)
|
||||
t.ReleaseTransaction, _ = p["transaction_id"].(string)
|
||||
t.ReleaseAnchor, _ = p["anchor_sha"].(string)
|
||||
t.PickupTransaction, t.PickupLeaseVersion = "", 0
|
||||
case "TaskPickupValidated":
|
||||
t.PickupTransaction, _ = p["transaction_id"].(string)
|
||||
if v, ok := p["lease_version"].(float64); ok {
|
||||
t.PickupLeaseVersion = int(v)
|
||||
}
|
||||
case "TaskCompleted":
|
||||
t.State = domain.StateCompleted
|
||||
t.Lease = nil
|
||||
@@ -156,6 +176,10 @@ func (s *Store) apply(e domain.Event) error {
|
||||
t.State = domain.StateBlocked
|
||||
t.Lease = nil
|
||||
t.Blocker, _ = p["blocker"].(string)
|
||||
t.BlockReason = domain.InferBlockReason(t.Blocker)
|
||||
if v, ok := p["block_reason"].(string); ok && domain.BlockReason(v).Valid() {
|
||||
t.BlockReason = domain.BlockReason(v)
|
||||
}
|
||||
t.BlockedAt = e.At
|
||||
t.LastPaneID, _ = p["pane_id"].(string)
|
||||
t.LastHarness, _ = p["harness_id"].(string)
|
||||
@@ -200,6 +224,25 @@ func (s *Store) apply(e domain.Event) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Terminal and release events may carry an owner-produced snapshot from
|
||||
// immediately before a worker/coordinator drops its live session mapping.
|
||||
// Preserve it independently of the current task state so historical task
|
||||
// pages never have to imply a pane is live just because its ID is known.
|
||||
if raw, ok := p["session_evidence"].(map[string]any); ok {
|
||||
var evidence domain.SessionEvidence
|
||||
if b, err := json.Marshal(raw); err == nil && json.Unmarshal(b, &evidence) == nil {
|
||||
t.LastSession = evidence
|
||||
if evidence.PaneID != "" {
|
||||
t.LastPaneID = evidence.PaneID
|
||||
}
|
||||
if evidence.HarnessID != "" {
|
||||
t.LastHarness = evidence.HarnessID
|
||||
}
|
||||
if evidence.PaneState != "" {
|
||||
t.PaneState = evidence.PaneState
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Version = e.Version
|
||||
s.tasks[e.TaskID] = t
|
||||
return nil
|
||||
@@ -410,12 +453,33 @@ func (s *Store) Lease(id, harness string, ttl time.Duration) (domain.Event, erro
|
||||
payload := map[string]any{"harness_id": harness, "ttl": ttl.Seconds(), "until_ns": time.Now().Add(ttl).UnixNano(), "expected_version": t.Version}
|
||||
if t.HandoffRef != "" {
|
||||
payload["handoff_ref"] = t.HandoffRef
|
||||
payload["transaction_id"] = t.ReleaseTransaction
|
||||
payload["anchor_sha"] = t.ReleaseAnchor
|
||||
}
|
||||
p, _ := json.Marshal(payload)
|
||||
e := domain.Event{ID: domain.NewID(), Type: "TaskLeased", TaskID: id, Version: t.Version + 1, Payload: p, Surface: string(authz.System)}
|
||||
return e, s.Append(e)
|
||||
}
|
||||
|
||||
// RenewLease atomically extends the current owner's lease. The observed task
|
||||
// version is part of the request so an old worker can never renew a lease
|
||||
// after release/reassignment.
|
||||
func (s *Store) RenewLease(id, harness string, expectedVersion int, ttl time.Duration) (domain.Event, error) {
|
||||
if ttl <= 0 {
|
||||
return domain.Event{}, fmt.Errorf("%w: ttl must be positive", domain.ErrInvalid)
|
||||
}
|
||||
t, ok := s.Task(id)
|
||||
if !ok {
|
||||
return domain.Event{}, domain.ErrNotFound
|
||||
}
|
||||
if t.State != domain.StateLeased || t.Lease == nil || t.Lease.HarnessID != harness || t.Version != expectedVersion {
|
||||
return domain.Event{}, domain.ErrConflict
|
||||
}
|
||||
p, _ := json.Marshal(map[string]any{"harness_id": harness, "until_ns": time.Now().Add(ttl).UnixNano(), "expected_version": expectedVersion})
|
||||
e := domain.Event{ID: domain.NewID(), Type: "TaskLeaseRenewed", TaskID: id, Version: t.Version + 1, Payload: p, Surface: string(authz.System)}
|
||||
return e, s.Append(e)
|
||||
}
|
||||
|
||||
func (s *Store) ExpireLeases(now time.Time) ([]domain.Event, error) {
|
||||
var out []domain.Event
|
||||
for _, t := range s.Tasks() {
|
||||
|
||||
Reference in New Issue
Block a user