Daemons/Event Loop CLA in Swift 5.7+?

The previous pattern I had used to implement daemons in Swift was this:

static func main() async throws {
    // setup app

    RunLoop.main.run()
}

But this doesn't seem to work in Swift 5.7 as the app terminates and the compiler gives this warning:

Instance method 'run' is unavailable from asynchronous contexts; run cannot be used from async contexts.; this is an error in Swift 6

I was able to find a hack workaround using CheckedContinuation...

static func main() async throws {
    // setup app

#if swift(>=5.7)
    // FIXME: Find a better workaround than this
    var strongDone: CheckedContinuation<Void, Never>?
    await withCheckedContinuation({ (done: CheckedContinuation<Void, Never>) in
        strongDone = done
    })
    print(String(describing: strongDone)) // avoids `Variable 'strongDone' was written to, but never read`
#else
    RunLoop.main.run()
#endif
}

But that doesn't seem like the right thing to do.

I think an alternative would be to get rid of the async from main() and throw any async tasks in a Task.

Is that the correct approach? Or is there a replacement to RunLoop.main.run() when in an async context (i.e. await <app termination>)?

By using an UnsafeContinuation you don't need to store it since CheckedContinuation will crash your app if the continuation is deinitialized before resumed. So you could implement a park function which never resumes

func park() async -> Never {
    await withUnsafeContinuation { _ in }
}

and then you can suspend the task basically forever

static func main() async throws {
    // setup app

    await park()
}

This has the same effect as the RunLoop.main.run() version since dispatchMain() is called under the hoods when the main() is suspended.

1 Like

I think this is a good opportunity to substantially reconsider the way this code is written in order to take advantage of structured concurrency.

Ideally you'd write your daemon as an object that operates on a Task tree. The root Task is responsible for defining the lifetime of the daemon itself, and is listening for new work (however you consume it). It them spawns child tasks to handle instances of the work. We have something of an example in the SwiftNIO repository, though we haven't entirely finalised the pattern.

The advantage of this is that your daemon can now rely on all the guarantees of structured concurrency, including ensuring that task lifetimes are well bounded and that errors propagate as you want them to.

5 Likes