On March 26, 2017 at 12:09:55 AM, Brent Royal-Gordon (br...@architechies.com) wrote:
On Mar 23, 2017, at 9:01 AM, Joe Groff via swift-evolution < swift-evolution@swift.org> wrote: setjmp and longjmp do *not* work well with Swift since they need compiler support to implement their semantics, and since Swift does not provide this support, setjmp-ing from Swift is undefined behavior. Empirical evidence that small examples appear to work is not a good way of evaluating UB, since any changes to Swift or LLVM optimizations may break it. Ease of implementation is also not a good criterion for designing things. *Supporting* a trap hook is not easy; it still has serious language semantics and runtime design issues, and may limit our ability to do something better. Could we do something useful here without setjmp/longjmp? Suppose we had a function in the standard library that was roughly equivalent to this: typealias UnsafeFatalErrorCleanupHandler = () -> Void // Note the imaginary @_thread_local property behavior @_thread_local var _fatalErrorCleanupHandlers: [UnsafeFatalErrorCleanupHandler] = [] func withUnsafeFatalErrorCleanupHandler<R>(_ handler: UnsafeFatalErrorCleanupHandler, do body: () throws -> R) rethrows -> R { _fatalErrorCleanupHandlers.append(handler) defer { _fatalErrorCleanupHandlers.removeLast() } return try body() } And then, just before we trap, we do something like this: // Mutating it this way ensures that, if we reenter fatalError() in one of the handlers, // it will pick up with the next handler. while let handler = _fatalErrorCleanupHandlers.popLast() { handler() } I think that would allow frameworks to do something like: class Worker { let queue = DispatchQueue(label: "Worker") typealias RequestCompletion = (RequestStatus) -> Void enum RequestStatus { case responded case crashing } func beginRequest(from conn: IOHandle, then completion: RequestCompletion) { queue.async { withUnsafeFatalErrorCleanupHandler(fatalErrorHandler(completion)) { // Handle the request, potentially crashing } } } func fatalErrorHandler(_ completion: RequestCompletion) -> UnsafeFatalErrorCleanupHandler { return { completion(.crashing) } } } class Supervisor { let queue = DispatchQueue(label: "Supervisor") var workerPool: Pool<Worker> func startListening(to sockets: [IOHandle]) { … } func stopListening() { … } func bootReplacement() { … } func connected(by conn: IOHandle) { dispatchPrecondition(condition: .onQueue(queue)) let worker = workerPool.reserve() worker.beginRequest(from: conn) { status in switch status { case .responded: conn.close() self.queue.sync { self.workerPool.relinquish(worker) } case .crashing: // Uh oh, we're in trouble. // // This process is toast; it will not survive long beyond this stack frame. // We want to close our listening socket, start a replacement server, // and then just try to hang on until the other workers have finished their // current requests. // // It'd be nice to send an error message and close the connection, // but we shouldn't. We don't know what state the connection or the // worker are in, so we don't want to do anything to them.We risk a // `!==` check because it only involves a memory address stored in // our closure context, not the actual object being referenced by it. // Go exclusive on the supervisor's queue so we don't try to handle // two crashes at once (or a crash and something else, for that matter). self.queue.sync { self.stopListening() self.bootReplacement() // Try to keep the process alive until all of the surviving workers have // finished their current requests. To do this, we'll perform barrier blocks // on all of their queues, attached to a single dispatch group, and then // wait for the group to complete. let group = DispatchGroup() for otherWorker in self.workerPool.all where otherWorker !== worker { // We run this as `.background` to try to let anything else the request might // enqueue run first, and `.barrier` to make sure we're the only thing running. otherWorker.queue.async(group: group, qos: .background, flags: .barrier) { // Make sure we don't do anything else. otherWorker.queue.suspend() } } // We do not use `notify` because we need this stack frame to keep // running so we don't trap yet. group.wait(timeout: .now() + .seconds(15)) } // Okay, we can now return, and probably crash. } } } } It's definitely not a full actor model, and you have to be very careful, but it might be a useful subset. -- Brent Royal-Gordon Architechies Well, it seems like it would work pretty well since a thread would be needed anyway to do the cleanup. At the end it still boils down to a fatalError handling proposal and the web framework developer(s) put the implmentation. If the actor model sufficiently replaces it, then it makes still a good bridge between now and then ie. Swift 5-7. If a shared actor trips, then it also trips the rest of the non-shared actors who use it, so it still has some potential to hold that shared actor open until the requests using it finish execution.
_______________________________________________ swift-evolution mailing list swift-evolution@swift.org https://lists.swift.org/mailman/listinfo/swift-evolution