package service import ( "fmt" "strconv" runtime "iop/packages/go/execution" iop "iop/proto/gen/iop" ) const recoveryHandoffConfirmed = "confirmed" type receivedTerminalDisposition uint8 const ( receivedTerminalUntracked receivedTerminalDisposition = iota receivedTerminalAccepted receivedTerminalRejected ) func isTerminalRunEvent(e *iop.RunEvent) bool { t := e.GetType() return t == "complete" || t == "error" || t == "cancelled" } // releaseLease frees the resources held by one lease, exactly once, whatever the // cause: a send failure, a normalized terminal run event, a tunnel END/ERROR // frame, a tunnel close, or a node disconnect. All of them converge here, and the // lease map — mutated only under m.mu — decides which caller performs the single // transition into released. A lease id that is already gone is a no-op, so a // losing racer can never decrement a counter that now belongs to another request. func (m *modelQueueManager) releaseLease(leaseID uint64, reason string) { if leaseID == 0 { return } m.mu.Lock() defer m.mu.Unlock() if _, ok := m.leases[leaseID]; !ok { return } if !m.releaseLeaseLocked(leaseID) { return } // Pump every group, not just the one that held the lease: the freed slot // belongs to a provider resource that other model groups queue against, and // the earliest waiter for it may be in any of them. m.pumpAllLocked() } // releaseLeaseLocked performs the reserved/tracked → released transition and // frees the slot the lease holds, without pumping the queue. Returns true when // this call is the one that performed the transition. Must be called with m.mu // held. func (m *modelQueueManager) releaseLeaseLocked(leaseID uint64) bool { lease, ok := m.leases[leaseID] if !ok { return false } delete(m.leases, leaseID) if lease.runID != "" { delete(m.leaseByRun, lease.runID) } lease.state = leaseStateReleased m.decrementSlotLocked(lease.groupKey, lease.nodeID, lease.providerID, lease.long) return true } // receivedHealthEvidence is the fully validated Node health evidence carried by // one typed response-stalled terminal. It contains no caller-controlled fields. type receivedHealthEvidence struct { providerHealth string sequence uint64 } func parseReceivedHealthEvidence(runID string, failure *iop.ExecutionFailure) (receivedHealthEvidence, bool) { if failure == nil || failure.GetCode() != string(runtime.FailureCodeResponseStalled) || !failure.GetRetryable() { return receivedHealthEvidence{}, false } metadata := failure.GetMetadata() if metadata["failure_code"] != string(runtime.FailureCodeResponseStalled) || metadata["attempt_fence"] != "confirmed" || metadata["run_id"] != runID || metadata["attempt_id"] != runID || metadata["adapter"] == "" || metadata["target"] == "" { return receivedHealthEvidence{}, false } sequence, err := strconv.ParseUint(metadata["health_observation_seq"], 10, 64) if err != nil || sequence == 0 { return receivedHealthEvidence{}, false } health := metadata["provider_health"] classification := metadata["liveness_classification"] switch { case health == string(runtime.ProviderStatusUnavailable) && classification == string(runtime.ProviderUnhealthy): case health == string(runtime.ProviderStatusAvailable) && classification == string(runtime.RequestStalled): case health == string(runtime.ProviderStatusUnknown) && classification == string(runtime.HealthUnknown): default: return receivedHealthEvidence{}, false } return receivedHealthEvidence{providerHealth: health, sequence: sequence}, true } func annotateRecoveryHandoff(failure *iop.ExecutionFailure, envelopeMetadata *map[string]string, providerID, providerHealth string) { if failure.Metadata == nil { failure.Metadata = make(map[string]string) } failure.Metadata["provider_id"] = providerID failure.Metadata["provider_health"] = providerHealth failure.Metadata["recovery_handoff"] = recoveryHandoffConfirmed if envelopeMetadata == nil { return } if *envelopeMetadata == nil { *envelopeMetadata = make(map[string]string) } (*envelopeMetadata)["provider_id"] = providerID (*envelopeMetadata)["provider_health"] = providerHealth (*envelopeMetadata)["recovery_handoff"] = recoveryHandoffConfirmed } // applyReceivedHealthEvidenceLocked sequence-fences one fully bound terminal. // Every accepted observation advances the high-water mark. Only unavailable // lowers effective provider health; available/unknown stall observations never // recover an already unavailable provider. func (m *modelQueueManager) applyReceivedHealthEvidenceLocked(lease *providerLease, evidence receivedHealthEvidence) providerHealthObservation { observation := providerHealthObservation{ source: "stall", evidenceHealth: evidence.providerHealth, decision: "inconclusive", } if lease == nil || lease.providerID == "" || lease.adapter == "" || lease.target == "" { observation.decision = "rejected_binding" return observation } key := providerRuntimeHealthKey{ nodeID: lease.nodeID, generation: lease.generation, providerID: lease.providerID, } overlay := m.runtimeHealth[key] observation.fromHealth = runtimeOverlayHealth(overlay) observation.toHealth = observation.fromHealth if overlay != nil && evidence.sequence <= overlay.observationSeq { observation.decision = "rejected_stale" return observation } if overlay == nil { overlay = &providerRuntimeHealthOverlay{} m.runtimeHealth[key] = overlay } overlay.observationSeq = evidence.sequence if evidence.providerHealth == string(runtime.ProviderStatusUnavailable) { overlay.adapter = lease.adapter overlay.target = lease.target overlay.unavailable = true } observation.decision = "applied" observation.toHealth = runtimeOverlayHealth(overlay) observation.stateChanged = observation.fromHealth != observation.toHealth return observation } // settleReceivedTerminal validates authoritative reception identity against the // immutable lease before any correctness state changes. A current terminal // releases once even when its optional health evidence is missing or rejected. // A mismatched node/generation is rejected and cannot release another owner's // lease. For accepted bound stall evidence, handoff annotation, any fresh overlay // transition, release, and queue pumping all occur under m.mu. func (m *modelQueueManager) settleReceivedTerminal(nodeID string, generation uint64, runID string, failure *iop.ExecutionFailure, envelopeMetadata *map[string]string) receivedTerminalDisposition { if runID == "" { return receivedTerminalUntracked } m.mu.Lock() leaseID, tracked := m.leaseByRun[runID] if !tracked { m.mu.Unlock() return receivedTerminalUntracked } lease := m.leases[leaseID] if lease == nil { delete(m.leaseByRun, runID) m.mu.Unlock() return receivedTerminalUntracked } if nodeID == "" || generation == 0 || lease.nodeID != nodeID || lease.generation != generation { m.mu.Unlock() return receivedTerminalRejected } var observation *providerHealthObservation if evidence, ok := parseReceivedHealthEvidence(runID, failure); ok { metadata := failure.GetMetadata() if lease.providerID != "" && metadata["adapter"] == lease.adapter && metadata["target"] == lease.target { // Handoff confirms authoritative reception, immutable lease binding, // and the local attempt fence. Sequence freshness governs only the // provider-wide overlay; an out-of-order terminal still carries its // request-local handoff and still releases its own lease. annotateRecoveryHandoff(failure, envelopeMetadata, lease.providerID, evidence.providerHealth) result := m.applyReceivedHealthEvidenceLocked(lease, evidence) observation = &result } else { result := providerHealthObservation{source: "stall", evidenceHealth: evidence.providerHealth, decision: "rejected_binding"} observation = &result } } if m.releaseLeaseLocked(leaseID) { m.pumpAllLocked() } m.mu.Unlock() m.observeProviderHealth(observation) return receivedTerminalAccepted } // resolveCurrentProbeProviderLocked resolves CAPABILITIES evidence against the // authoritative current Node provider catalog. Runtime overlays are a health // projection, not an identity source: a healthy sibling provider with the same // adapter/target must make the evidence ambiguous as well. Must be called with // m.mu held. func (m *modelQueueManager) resolveCurrentProbeProviderLocked(nodeID, adapter, target string) (string, bool) { if m.store == nil { return "", false } record, ok := m.store.FindByID(nodeID) if !ok || record == nil { return "", false } matchedProviderID := "" for _, provider := range record.Providers { if provider.ID == "" || providerAdapterKey(provider) != adapter || !providerCanServe(provider, target) { continue } if matchedProviderID != "" { return "", false } matchedProviderID = provider.ID } return matchedProviderID, matchedProviderID != "" } // applyProviderProbeEvidence offers one CAPABILITIES health observation to the // current provider catalog. The exact adapter/target must identify one and only // one current-generation provider. A strictly newer available observation is // recorded even if the provider is already effectively available, so a delayed // lower-sequence terminal cannot later mark it unavailable. Only an actual // unavailable-to-available transition pumps the queue and reports recovery. func (m *modelQueueManager) applyProviderProbeEvidence(nodeID string, generation uint64, adapter, target string, status runtime.ProviderStatus, sequence uint64, isCurrentOwner func() bool) bool { observation := providerHealthObservation{source: "probe", evidenceHealth: string(status), decision: "inconclusive"} if nodeID == "" || generation == 0 || adapter == "" || target == "" || status != runtime.ProviderStatusAvailable || sequence == 0 { m.observeProviderHealth(&observation) return false } m.mu.Lock() if isCurrentOwner != nil && !isCurrentOwner() { m.mu.Unlock() observation.decision = "rejected_binding" m.observeProviderHealth(&observation) return false } providerID, ok := m.resolveCurrentProbeProviderLocked(nodeID, adapter, target) if !ok { m.mu.Unlock() observation.decision = "rejected_ambiguous" m.observeProviderHealth(&observation) return false } key := providerRuntimeHealthKey{nodeID: nodeID, generation: generation, providerID: providerID} overlay := m.runtimeHealth[key] observation.fromHealth = runtimeOverlayHealth(overlay) observation.toHealth = observation.fromHealth if overlay != nil && sequence <= overlay.observationSeq { m.mu.Unlock() observation.decision = "rejected_stale" m.observeProviderHealth(&observation) return false } if overlay == nil { overlay = &providerRuntimeHealthOverlay{} m.runtimeHealth[key] = overlay } recovered := overlay.unavailable && overlay.adapter == adapter && overlay.target == target overlay.observationSeq = sequence if overlay.unavailable && !recovered { m.mu.Unlock() observation.decision = "rejected_binding" observation.toHealth = runtimeOverlayHealth(overlay) m.observeProviderHealth(&observation) return false } overlay.adapter = adapter overlay.target = target overlay.unavailable = false if recovered { m.pumpAllLocked() } observation.decision = "applied" observation.toHealth = runtimeOverlayHealth(overlay) observation.stateChanged = observation.fromHealth != observation.toHealth m.mu.Unlock() m.observeProviderHealth(&observation) return recovered } func runtimeOverlayHealth(overlay *providerRuntimeHealthOverlay) string { if overlay != nil && overlay.unavailable { return string(runtime.ProviderStatusUnavailable) } return string(runtime.ProviderStatusAvailable) } // fenceNodeGenerationLocked fences the disconnected connection identified by // (nodeID, generation): it settles leases through the exactly-once release path // so each provider resource counter is returned per lease, and marks matching // provider resources orphan. Under a generation-scoped fence a strictly newer // reconnect's leases and resources are left untouched; under a whole-node fence // (generation 0) it also zeroes any residual counters seeded by fixtures // without a backing lease. Returns whether any lease was settled so the caller // can tell an authoritative fence from a stale no-op. Must be called with m.mu // held. func (m *modelQueueManager) fenceNodeGenerationLocked(nodeID string, generation uint64) bool { fenceAll := generation == 0 settledLease := m.settleLeasesForNodeLocked(nodeID, generation) for key, res := range m.resources { if key.nodeID != nodeID { continue } if !fenceAll && res.generation > generation { continue } if fenceAll { res.inFlight = 0 res.longInFlight = 0 } res.orphan = true } for key := range m.runtimeHealth { if key.nodeID == nodeID && (fenceAll || key.generation <= generation) { delete(m.runtimeHealth, key) } } return settledLease } // resolveAndPumpAllLocked rebuilds every queued item's candidate universe // through its live resolver (or filters orphaned and fenced-node snapshot // candidates when no resolver is attached) and then runs the global pump. // Terminal no-candidate items are settled immediately so the pump only deals // with dispatchable or temporarily-blocked waiters. excludeNodeID, excludeGen // and fenceAll describe the disconnect context so snapshot items that lack a // live resolver are also purged of the fenced node — the same signal the old // candidate-snapshot removal loop used — while provider-pool items with a // resolver get a fresh resolution against current state. This is the // authoritative disconnect settlement pass: surviving-provider fallback, // terminal unavailable, and normal dispatch all resolve in the same // deterministic global pass, independent of event bus delivery. Must be called // with m.mu held. func (m *modelQueueManager) resolveAndPumpAllLocked(excludeNodeID string, excludeGen uint64, fenceAll bool) { for _, group := range m.groups { for _, item := range group.queue { if item.resolveCandidates != nil { candidates, outcome, resolveErr := m.resolveQueuedCandidatesLocked(item) if outcome == resolveNoCandidates { // Terminal no-candidate: remove immediately, before the pump, // so the dispatch pass only sees items that can actually run. m.removeQueuedItemLocked(group, item) select { case item.waitCh <- admitResult{err: fmt.Errorf("model group %q: %w", group.key, errProviderUnavailable)}: default: } continue } if outcome == resolveResolverError { // Leave the item queued for a later pump; the resolver fault // is recoverable and must not block the dispatch pass. continue } if outcome == resolveTerminalError { m.removeQueuedItemLocked(group, item) select { case item.waitCh <- admitResult{err: resolveErr}: default: } continue } if candidates != nil { item.candidates = candidates } continue } // Snapshot-candidate path: filter orphaned resources AND the fenced // node itself so a pump dispatch cannot hand them to an offline // connection. A strictly newer reconnect on the same node is // preserved (generation-scoped fence). filtered := make([]candidateNode, 0, len(item.candidates)) for _, c := range item.candidates { if c.entry == nil { filtered = append(filtered, c) continue } if excludeNodeID != "" && (fenceAll || c.generation <= excludeGen) && c.entry.NodeID == excludeNodeID { continue } key := providerResourceKey{nodeID: c.entry.NodeID, providerID: c.providerID} if r, ok := m.resources[key]; ok && r.orphan { continue } filtered = append(filtered, c) } item.candidates = filtered } } // One global pump after every group's candidates are refreshed, so a waiter // can only be handed a provider that is still live, and a waiter in any // group can pick up the remaining ones. m.pumpAllLocked() } // releaseNode fences the disconnected connection identified by (nodeID, // generation): it drops that connection's leases, zeroes the matching provider // resources to offline, rebuilds every queued item's candidate universe through // its live resolver, and runs one global pump so surviving-provider fallback, // terminal unavailable, and normal dispatch all resolve in the same pass. A // generation of 0 fences the node unconditionally (untracked/legacy disconnect). // A specific generation fences only resources whose current owner generation is // that one or older: a stale callback for an already-superseded connection // cannot zero the slot a live reconnect now holds, so its live resource and // events are left unchanged. func (m *modelQueueManager) releaseNode(nodeID string, generation uint64, reason string) { m.mu.Lock() defer m.mu.Unlock() fenceAll := generation == 0 // Fence the generation: settle leases and mark matching resources orphan. settledLease := m.fenceNodeGenerationLocked(nodeID, generation) // A generation-scoped callback that fenced neither a lease nor a resource is // a stale close for an already-superseded connection: the live owner's state // and queued candidate lists are authoritative, so leave them untouched. if !fenceAll && !settledLease { return } // A whole-node fence (generation 0) clears any residual legacy/provider slot // counters this node still holds — legacy (nodeID-only) fixtures and any // counter a lease did not account for. Provider-pool counters live on the // resource state already marked orphan above. if fenceAll { for _, group := range m.groups { for slot := range group.inflight { if colonIdx := findLastColon(slot); colonIdx > 0 { if slot[:colonIdx] == nodeID { delete(group.inflight, slot) delete(group.longInflight, slot) } } else if slot == nodeID { delete(group.inflight, slot) delete(group.longInflight, slot) } } } } // Rebuild every queued item's candidate universe through the live resolver // and run one global pump so that surviving-provider fallback, terminal // unavailable, and normal dispatch all resolve in the same deterministic // pass. This replaces the previous candidate-snapshot removal loop: items // with a live resolver get re-evaluated against the current store/catalog/ // registry state (including orphan filtering), and items without a live // resolver have the fenced node and orphaned resources purged from their // candidate lists, so the pump sees only live targets and can settle // terminal no-candidate items directly. m.resolveAndPumpAllLocked(nodeID, generation, fenceAll) } // activateNode is the reconnect counterpart to releaseNode: it restores the // provider resources owned by an accepted connection identified by (nodeID, // generation) to an available state for that generation, then rebuilds every // queued item's candidate universe through its live resolver and runs one global // pump. A disconnect marked those resources orphan and stranded the waiters // queued against them; the reconnect alone re-activates the resources and // re-dispatches the waiters in global enqueue order, with no new request, config // refresh, or lease release to trigger the pump. // // No disconnect context is passed to the rebuild pass (excludeNodeID empty, // fenceAll false), so nothing is purged: the reconnected node's candidates are // made live and dispatched, and every other group's waiters are re-evaluated // against current state in the same deterministic pass. // // isCurrentOwner is the ownership linearization gate. It is evaluated under m.mu, // in the same critical section that mutates the resources and pumps, and it is // serialized against releaseNode (which also holds m.mu). A ready callback whose // connection a disconnect settled — unregistered its entry and released its // leases — between the transport handshake and this activation therefore observes // the disconnect and is a no-op: the check and the activation cannot straddle the // disconnect the way a check performed before m.mu was acquired could. The // callback itself reads the registry (m.mu → registry-lock order, matching the // pump's live resolver); the registry lock is never held across m.mu, so no lock // inversion is introduced. A nil callback is treated as always-current for // untracked/legacy callers and fixtures. func (m *modelQueueManager) activateNode(nodeID string, generation uint64, isCurrentOwner func() bool) { m.mu.Lock() defer m.mu.Unlock() if isCurrentOwner != nil && !isCurrentOwner() { // A stale/superseded generation: the disconnect that removed this // connection already settled its resources under this same lock, and // re-activating them here would resurrect a slot the live owner (or none) // now holds. return } m.activateNodeGenerationLocked(nodeID, generation) m.resolveAndPumpAllLocked("", 0, false) } // settleLeasesForNodeLocked returns the resources held by the leases a disconnect // fences on a node, routing each through the exactly-once releaseLeaseLocked // decrement path so per-lease provider counters are returned — never zeroed // wholesale. A generation of 0 fences every lease on the node (untracked/legacy // disconnect); a specific generation fences that owner and any older one, leaving // a strictly newer reconnect's leases — and the shared resource counters they // still hold — untouched. It reports whether it settled any lease so the caller // can tell an authoritative fence from a stale no-op. Must be called with m.mu // held. func (m *modelQueueManager) settleLeasesForNodeLocked(nodeID string, generation uint64) bool { fenceAll := generation == 0 // Collect first: releaseLeaseLocked mutates m.leases, so it cannot run inside // the range over the same map. matching := make([]uint64, 0, len(m.leases)) for id, lease := range m.leases { if lease.nodeID != nodeID { continue } if !fenceAll && lease.generation > generation { continue } matching = append(matching, id) } for _, id := range matching { m.releaseLeaseLocked(id) } return len(matching) > 0 } // releaseRun releases the lease owning a terminated run and dispatches the next // queued item if one is waiting. Safe to call for an unknown or already-released // run: the lease index resolves nothing and the call is a no-op. func (m *modelQueueManager) releaseRun(runID, reason string) { m.mu.Lock() defer m.mu.Unlock() leaseID, ok := m.leaseByRun[runID] if !ok { return } if m.leases[leaseID] == nil { delete(m.leaseByRun, runID) return } if !m.releaseLeaseLocked(leaseID) { return } m.pumpAllLocked() } // releaseSlot decrements the in-flight count and tries to dispatch the next // queued item. Used when admit-path I/O fails after the slot was reserved. // For provider-pool dispatches (nodeID:providerID slot), pass providerID. // For legacy dispatches, pass empty providerID so nodeID is used directly. func (m *modelQueueManager) releaseSlot(groupKey, nodeID string, providerID ...string) { var pid string if len(providerID) > 0 { pid = providerID[0] } m.releaseSlotWithLong(groupKey, nodeID, pid, false) } // releaseSlotWithLong releases a reserved slot, additionally freeing a long-context // slot when longSlot is true. Used by admit-path failure/race handling where the // caller knows whether a long slot was reserved. func (m *modelQueueManager) releaseSlotWithLong(groupKey, nodeID, providerID string, longSlot bool) { m.mu.Lock() m.releaseSlotLocked(groupKey, nodeID, providerID, longSlot) m.mu.Unlock() } func (m *modelQueueManager) releaseSlotLocked(groupKey, nodeID string, providerID string, longSlot bool) { if group := m.decrementSlotLocked(groupKey, nodeID, providerID, longSlot); group == nil { return } m.pumpAllLocked() } // decrementSlotLocked frees the resources a reservation holds without pumping the // queue, returning the group so the caller can decide whether to dispatch. Must // be called with m.mu held. func (m *modelQueueManager) decrementSlotLocked(groupKey, nodeID, providerID string, longSlot bool) *modelQueueGroup { group, ok := m.groups[groupKey] if !ok { return nil } // Use provider-aware slot key for provider-pool dispatches. slot := nodeID if providerID != "" { slot = nodeID + ":" + providerID key := providerResourceKey{nodeID: nodeID, providerID: providerID} if res, ok := m.resources[key]; ok { res.release(longSlot) if res.orphan && res.inFlight == 0 && res.longInFlight == 0 { delete(m.resources, key) } } else { if group.inflight[slot] > 0 { group.inflight[slot]-- } if longSlot && group.longInflight[slot] > 0 { group.longInflight[slot]-- } } } else { if group.inflight[slot] > 0 { group.inflight[slot]-- } if longSlot && group.longInflight[slot] > 0 { group.longInflight[slot]-- } } return group }