import Foundation /// Owns at most one running source operation for each logical serve key. /// /// Cancellation is advisory in Swift. Timed-out work therefore remains owned /// until its source body actually exits. A config successor can wait behind it, /// but can never overlap it. actor CLIServeOperationCoordinator { typealias Instant = ContinuousClock.Instant typealias Now = @Sendable () -> Instant typealias SleepUntil = @Sendable (Instant) async throws -> Void struct Snapshot: Equatable, Sendable { let operationCount: Int let waiterCount: Int let timerCount: Int let isShutDown: Bool } private struct Waiter { let id: UUID let continuation: CheckedContinuation let timeoutValue: Value } private enum OperationPhase { case running case accepting case timedOut } private struct Operation { let generation: UInt64 let fingerprint: String var deadline: Instant? let makeValue: @Sendable () async -> Value let acceptValue: @Sendable (Value) async -> Value var sourceTask: Task? var deadlineTask: Task? var waiters: [Waiter] var phase: OperationPhase } private struct Slot { var active: Operation? var pending: Operation? } private struct OperationRequest: Sendable { let key: String let fingerprint: String let deadline: Instant? let timeoutValue: Value let makeValue: @Sendable () async -> Value let acceptValue: @Sendable (Value) async -> Value } private enum WaitTarget { case active case pending } private let now: Now private let sleepUntil: SleepUntil // Callers validate route keys and provider keys come from UsageProvider, so // retained canceled work has a small, closed key space. private var slots: [String: Slot] = [:] private var nextGeneration: UInt64 = 0 private var isShutDown = false init( now: @escaping Now = { ContinuousClock().now }, sleepUntil: @escaping SleepUntil = { deadline in try await ContinuousClock().sleep(until: deadline) }) { self.now = now self.sleepUntil = sleepUntil } func value( for key: String, fingerprint: String, deadline: Instant?, timeoutValue: Value, accept: @Sendable @escaping (Value) async -> Value = { $0 }, operation makeValue: @Sendable @escaping () async -> Value) async -> Value { let request = OperationRequest( key: key, fingerprint: fingerprint, deadline: deadline, timeoutValue: timeoutValue, makeValue: makeValue, acceptValue: accept) guard !self.isShutDown, !self.isExpired(deadline) else { return timeoutValue } self.expireOverdueOperations(for: key) guard let slot = self.slots[key], let active = slot.active else { return await self.installActive(request) } if active.fingerprint == fingerprint, active.phase != .timedOut { guard self.deadlinesAreCompatible(active.deadline, deadline) else { return timeoutValue } guard self.canJoinAccepting(active, deadline: deadline) else { return timeoutValue } return await self.awaitValue(for: request, target: .active) } if let pending = slot.pending, pending.fingerprint == fingerprint { guard self.deadlinesAreCompatible(pending.deadline, deadline) else { return timeoutValue } return await self.awaitValue(for: request, target: .pending) } if let pending = self.slots[key]?.pending { self.timeoutPending(for: key, generation: pending.generation) } return await self.installPending(request) } func shutdown() { guard !self.isShutDown else { return } self.isShutDown = true let active = self.slots.compactMap { key, slot in slot.active.map { (key, $0.generation) } } let pending = self.slots.compactMap { key, slot in slot.pending.map { (key, $0.generation) } } for (key, generation) in pending { self.timeoutPending(for: key, generation: generation) } for (key, generation) in active { self.shutdownActive(for: key, generation: generation) } } func snapshot() -> Snapshot { let operations = self.slots.values.flatMap { slot in [slot.active, slot.pending].compactMap(\.self) } return Snapshot( operationCount: operations.count, waiterCount: operations.reduce(0) { $0 + $1.waiters.count }, timerCount: operations.reduce(0) { $0 + ($1.deadlineTask == nil ? 0 : 1) }, isShutDown: self.isShutDown) } private func allocateGeneration() -> UInt64 { self.nextGeneration &+= 1 return self.nextGeneration } private func installActive(_ request: OperationRequest) async -> Value { let generation = self.allocateGeneration() let waiterID = UUID() return await withTaskCancellationHandler { await withCheckedContinuation { continuation in guard !Task.isCancelled, !self.isShutDown, !self.isExpired(request.deadline) else { continuation.resume(returning: request.timeoutValue) return } var operation = self.makeOperation( request, generation: generation, waiter: Waiter( id: waiterID, continuation: continuation, timeoutValue: request.timeoutValue)) operation.sourceTask = self.makeSourceTask( key: request.key, generation: generation, makeValue: request.makeValue) operation.deadlineTask = self.makeDeadlineTask( key: request.key, generation: generation, deadline: request.deadline) self.slots[request.key] = Slot(active: operation, pending: nil) } } onCancel: { Task { await self.cancelWaiter(id: waiterID, for: request.key) } } } private func installPending(_ request: OperationRequest) async -> Value { guard self.slots[request.key]?.active != nil else { return await self.installActive(request) } let generation = self.allocateGeneration() let waiterID = UUID() return await withTaskCancellationHandler { await withCheckedContinuation { continuation in guard !Task.isCancelled, !self.isShutDown, !self.isExpired(request.deadline) else { continuation.resume(returning: request.timeoutValue) return } guard var slot = self.slots[request.key], slot.active != nil else { continuation.resume(returning: request.timeoutValue) return } var operation = self.makeOperation( request, generation: generation, waiter: Waiter( id: waiterID, continuation: continuation, timeoutValue: request.timeoutValue)) operation.deadlineTask = self.makeDeadlineTask( key: request.key, generation: generation, deadline: request.deadline) slot.pending = operation self.slots[request.key] = slot } } onCancel: { Task { await self.cancelWaiter(id: waiterID, for: request.key) } } } private func awaitValue(for request: OperationRequest, target: WaitTarget) async -> Value { let waiterID = UUID() return await withTaskCancellationHandler { await withCheckedContinuation { continuation in guard !Task.isCancelled, !self.isShutDown, !self.isExpired(request.deadline), var slot = self.slots[request.key] else { continuation.resume(returning: request.timeoutValue) return } let waiter = Waiter( id: waiterID, continuation: continuation, timeoutValue: request.timeoutValue) switch target { case .active: guard var operation = slot.active, operation.fingerprint == request.fingerprint, operation.phase != .timedOut else { continuation.resume(returning: request.timeoutValue) return } guard self.canJoinAccepting(operation, deadline: request.deadline) else { continuation.resume(returning: request.timeoutValue) return } self.tightenDeadline(&operation, to: request.deadline, for: request.key) operation.waiters.append(waiter) slot.active = operation case .pending: guard var operation = slot.pending, operation.fingerprint == request.fingerprint else { continuation.resume(returning: request.timeoutValue) return } self.tightenDeadline(&operation, to: request.deadline, for: request.key) operation.waiters.append(waiter) slot.pending = operation } self.slots[request.key] = slot } } onCancel: { Task { await self.cancelWaiter(id: waiterID, for: request.key) } } } private func makeOperation( _ request: OperationRequest, generation: UInt64, waiter: Waiter) -> Operation { Operation( generation: generation, fingerprint: request.fingerprint, deadline: request.deadline, makeValue: request.makeValue, acceptValue: request.acceptValue, sourceTask: nil, deadlineTask: nil, waiters: [waiter], phase: .running) } private func makeSourceTask( key: String, generation: UInt64, makeValue: @Sendable @escaping () async -> Value) -> Task { Task.detached { [self] in let value = await makeValue() await self.sourceProduced(value, for: key, generation: generation) } } private func makeDeadlineTask( key: String, generation: UInt64, deadline: Instant?) -> Task? { guard let deadline else { return nil } let sleepUntil = self.sleepUntil return Task.detached { [self] in do { try await sleepUntil(deadline) } catch { return } await self.deadlineReached(for: key, generation: generation) } } private func cancelWaiter(id: UUID, for key: String) { guard var slot = self.slots[key] else { return } if var active = slot.active, let index = active.waiters.firstIndex(where: { $0.id == id }) { let waiter = active.waiters.remove(at: index) slot.active = active self.slots[key] = slot waiter.continuation.resume(returning: waiter.timeoutValue) if active.waiters.isEmpty, active.phase == .running { self.timeoutActive(for: key, generation: active.generation) } return } guard var pending = slot.pending, let index = pending.waiters.firstIndex(where: { $0.id == id }) else { return } let waiter = pending.waiters.remove(at: index) waiter.continuation.resume(returning: waiter.timeoutValue) if pending.waiters.isEmpty { pending.deadlineTask?.cancel() slot.pending = nil } else { slot.pending = pending } self.store(slot, for: key) } private func deadlineReached(for key: String, generation: UInt64) { if self.slots[key]?.active?.generation == generation { self.timeoutActive(for: key, generation: generation) } else if self.slots[key]?.pending?.generation == generation { self.timeoutPending(for: key, generation: generation) } } private func expireOverdueOperations(for key: String) { if let active = self.slots[key]?.active, active.phase == .running, self.isExpired(active.deadline) { self.timeoutActive(for: key, generation: active.generation) } if let pending = self.slots[key]?.pending, self.isExpired(pending.deadline) { self.timeoutPending(for: key, generation: pending.generation) } } private func timeoutActive(for key: String, generation: UInt64) { guard var slot = self.slots[key], var operation = slot.active, operation.generation == generation, operation.phase == .running else { return } operation.phase = .timedOut operation.sourceTask?.cancel() operation.deadlineTask?.cancel() operation.deadlineTask = nil let waiters = operation.waiters operation.waiters.removeAll() slot.active = operation self.slots[key] = slot for waiter in waiters { waiter.continuation.resume(returning: waiter.timeoutValue) } } private func shutdownActive(for key: String, generation: UInt64) { guard var slot = self.slots[key], var operation = slot.active, operation.generation == generation else { return } if operation.phase == .running { self.timeoutActive(for: key, generation: generation) return } guard operation.phase == .accepting else { return } operation.phase = .timedOut operation.sourceTask?.cancel() let waiters = operation.waiters operation.waiters.removeAll() slot.active = operation self.slots[key] = slot for waiter in waiters { waiter.continuation.resume(returning: waiter.timeoutValue) } } private func timeoutPending(for key: String, generation: UInt64) { guard var slot = self.slots[key], let operation = slot.pending, operation.generation == generation else { return } operation.deadlineTask?.cancel() slot.pending = nil self.store(slot, for: key) for waiter in operation.waiters { waiter.continuation.resume(returning: waiter.timeoutValue) } } private func sourceProduced(_ value: Value, for key: String, generation: UInt64) async { guard var slot = self.slots[key], let observed = slot.active, observed.generation == generation else { return } if observed.phase == .running, self.isExpired(observed.deadline) { self.timeoutActive(for: key, generation: generation) guard let refreshed = self.slots[key] else { return } slot = refreshed } guard var operation = slot.active, operation.generation == generation else { return } if operation.phase == .timedOut { slot.active = nil self.store(slot, for: key) self.promotePending(for: key) return } // Source completion wins the deadline only on this actor turn. `accept` // is restricted to bounded in-process projection/cache work: keep the // slot owned through that commit so a newer generation cannot start and // then be overwritten by this older result. Shutdown still releases the // waiters even if a commit is briefly queued on another actor. operation.phase = .accepting operation.deadlineTask?.cancel() operation.deadlineTask = nil slot.active = operation self.slots[key] = slot let acceptedValue = await operation.acceptValue(value) guard var refreshed = self.slots[key], let completed = refreshed.active, completed.generation == generation else { return } refreshed.active = nil self.store(refreshed, for: key) if completed.phase == .accepting { for waiter in completed.waiters { waiter.continuation.resume(returning: acceptedValue) } } self.promotePending(for: key) } private func promotePending(for key: String) { guard !self.isShutDown, var slot = self.slots[key], slot.active == nil, var pending = slot.pending else { return } if self.isExpired(pending.deadline) { self.timeoutPending(for: key, generation: pending.generation) return } pending.sourceTask = self.makeSourceTask( key: key, generation: pending.generation, makeValue: pending.makeValue) slot.active = pending slot.pending = nil self.slots[key] = slot } private func tightenDeadline(_ operation: inout Operation, to requested: Instant?, for key: String) { guard operation.phase == .running, let current = operation.deadline, let requested, requested < current else { return } operation.deadlineTask?.cancel() operation.deadline = requested operation.deadlineTask = self.makeDeadlineTask( key: key, generation: operation.generation, deadline: requested) } private func deadlinesAreCompatible(_ active: Instant?, _ requested: Instant?) -> Bool { (active == nil) == (requested == nil) } private func canJoinAccepting(_ operation: Operation, deadline: Instant?) -> Bool { guard operation.phase == .accepting, let activeDeadline = operation.deadline, let deadline else { return true } // Source acceptance already canceled the shared timer. An older-budget // follower cannot safely shorten that completed arbitration while the // bounded cache commit is in progress, so fail it closed. return deadline >= activeDeadline } private func isExpired(_ deadline: Instant?) -> Bool { guard let deadline else { return false } return deadline <= self.now() } private func store(_ slot: Slot, for key: String) { if slot.active == nil, slot.pending == nil { self.slots[key] = nil } else { self.slots[key] = slot } } }