field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

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 }