TeraRuntimeResourceTask.swift (5312B)
1 import Foundation 2 3 /// Creation completion and adoption are separate events. Only the transition 4 /// to abandoned may start cleanup; adoption permanently transfers that duty. 5 final class TeraRuntimeResourceTask<Value: Sendable>: Sendable { 6 private enum State { 7 case pending 8 case created(Value) 9 case adopted 10 case abandoned 11 } 12 13 private let ownership: Ownership 14 private let task: TeraRuntimeBoundedTask<Value> 15 16 init( 17 deadlineNanoseconds: UInt64, 18 cleanupDeadlineNanoseconds: UInt64, 19 operation: @escaping @Sendable () async -> Result<Value, TeraRuntimeFailure>, 20 cleanup: @escaping @Sendable (Value) async throws -> Void 21 ) { 22 let ownership = Ownership(deadline: cleanupDeadlineNanoseconds, cleanup: cleanup) 23 self.ownership = ownership 24 task = TeraRuntimeBoundedTask(deadlineNanoseconds: deadlineNanoseconds) { 25 let result = await operation() 26 if case let .success(value) = result { 27 ownership.created(value) 28 } 29 return result 30 } onAbandonedResult: { _ in 31 ownership.abandon() 32 } 33 } 34 35 func value(cancelsOperationWhenWaiterCancelled: Bool = true) async -> TeraRuntimeBoundedOutcome<Value> { 36 let outcome = await task.value(cancelsOperationWhenWaiterCancelled: cancelsOperationWhenWaiterCancelled) 37 switch outcome { 38 case .timedOut: 39 ownership.abandon() 40 case .cancelled where cancelsOperationWhenWaiterCancelled: 41 ownership.abandon() 42 default: 43 break 44 } 45 return outcome 46 } 47 48 func adopt() -> Bool { 49 ownership.adopt() 50 } 51 52 func cancel() { 53 ownership.abandon() 54 task.cancel() 55 } 56 57 /// Drain creation and its cleanup, retaining a failed resource for a later 58 /// explicit close attempt. This wait belongs to the bounded shutdown owner. 59 func finishAbandonment() async throws { 60 cancel() 61 if case .failure = await task.settle() { 62 return 63 } 64 try await ownership.finishCleanup() 65 } 66 67 /// Every mutable field is lock protected. Cleanup starts outside the lock, 68 /// has the existing host deadline, and retains its resource until it returns. 69 private final class Ownership: @unchecked Sendable { 70 private let lock = NSLock() 71 private var state = State.pending 72 private var cleanupTask: Cleanup? 73 private var cleanupWaiters: [CheckedContinuation<Cleanup?, Never>] = [] 74 private let deadline: UInt64 75 private let cleanup: @Sendable (Value) async throws -> Void 76 77 init(deadline: UInt64, cleanup: @escaping @Sendable (Value) async throws -> Void) { 78 self.deadline = deadline 79 self.cleanup = cleanup 80 } 81 82 func created(_ value: Value) { 83 let abandoned = lock.withLock { 84 if case .abandoned = state { 85 return true 86 } 87 state = .created(value) 88 return false 89 } 90 if abandoned { 91 startCleanup(value) 92 } 93 } 94 95 func adopt() -> Bool { 96 lock.withLock { 97 guard case .created = state else { return false } 98 state = .adopted 99 return true 100 } 101 } 102 103 func abandon() { 104 let value: Value? = lock.withLock { 105 switch state { 106 case let .created(value): 107 state = .abandoned 108 return value 109 case .pending: 110 state = .abandoned 111 return nil 112 case .adopted, .abandoned: 113 return nil 114 } 115 } 116 if let value { 117 startCleanup(value) 118 } 119 } 120 121 private func startCleanup(_ value: Value) { 122 let task = Cleanup(value: value, deadline: deadline, cleanup: cleanup) 123 let waiters = lock.withLock { 124 cleanupTask = task 125 let pending = cleanupWaiters 126 cleanupWaiters.removeAll() 127 return pending 128 } 129 for waiter in waiters { 130 waiter.resume(returning: task) 131 } 132 } 133 134 func finishCleanup() async throws { 135 let task = await withCheckedContinuation { continuation in 136 let immediate = lock.withLock { () -> (Bool, Cleanup?) in 137 if case .adopted = state { 138 return (true, nil) 139 } 140 if let cleanupTask { 141 return (true, cleanupTask) 142 } 143 cleanupWaiters.append(continuation) 144 return (false, nil) 145 } 146 if immediate.0 { 147 continuation.resume(returning: immediate.1) 148 } 149 } 150 try await task?.finish() 151 } 152 } 153 154 private actor Cleanup { 155 private let operation: @Sendable () async -> Result<Void, TeraRuntimeFailure> 156 private let deadline: UInt64 157 private var task: TeraRuntimeBoundedTask<Void> 158 159 init(value: Value, deadline: UInt64, cleanup: @escaping @Sendable (Value) async throws -> Void) { 160 let operation: @Sendable () async -> Result<Void, TeraRuntimeFailure> = { 161 do { 162 try await cleanup(value) 163 return .success(()) 164 } catch { 165 return .failure(TeraRuntimeClient.failure(from: error, operation: "runtime.resource.close")) 166 } 167 } 168 self.operation = operation 169 self.deadline = deadline 170 task = TeraRuntimeBoundedTask(deadlineNanoseconds: deadline, operation: operation) 171 } 172 173 func finish() async throws { 174 if case .failure = task.settlement() { 175 task = TeraRuntimeBoundedTask(deadlineNanoseconds: deadline, operation: operation) 176 } 177 try await task.settle().get() 178 } 179 } 180 }