I've been pondering upon the following code for a function.
public extension Database {
func transaction<T>(_ closure: @Sendable @escaping (Database) async throws -> T) async throws -> T {
try await self.transaction { db -> EventLoopFuture<T> in
let promise = self.eventLoop.makePromise(of: T.self)
promise.completeWithTask{ try await closure(db) }
return promise.futureResult
}.get()
}
}
This is actual code from the Vapor framework.
Here is the URL for reference: https://github.com/vapor/fluent-kit/blob/main/Sources/FluentKit/Concurrency/Database%2BConcurrency.swift
In this transaction
function, the argument closure
has the @Sendable
and @escaping
attributes.
I am wondering if both could be removed.
In particular, the @Sendable
attribute implies that the types passed down to the closure must be of Sendable
,
which imposes quite strict limitations.
Hence, if we could omit it, that would be much more convenient.
The reason I think it could be removed is because,
while this closure
is indeed passed onto the event loop thread,
by the time it leaves the transaction
function,
it is awaiting EventLoopFuture.get()
,
ensuring that the function invocation of closure
is complete.
In other words, there is no chance of closure
being simultaneously called from two different threads.
Indeed, theoretically since the eventLoop
type is abstracted to any EventLoop
,
implementing a way to store the closure passed to completeWithTask
into a global variable or something similar is possible,
but that would clearly be unnatural behavior for EventLoop
and EventLoopFuture
and I don't think there's anything to worry about.
Furthermore, I think @escaping
can be eliminated for the same reason. The closure
doesn't actually escape.
Could the above thoughts be right?
I'd love to hear thoughts from someone with more insight to weigh in.
For reference, the specific implementation would look as follows:
public extension Database {
func transaction<T>(_ closure: (any Database) async throws -> T) async throws -> T {
try await withoutActuallyEscaping(closure) { (closure) in
let closureBox = UncheckedSendableBox(closure)
return try await self.transaction { db -> EventLoopFuture<T> in
let dbBox = UncheckedSendableBox(db)
let promise = self.eventLoop.makePromise(of: T.self)
promise.completeWithTask {
let db = dbBox.value
let closure = closureBox.value
return try await closure(db)
}
return promise.futureResult
}.get()
}
}
}
And I believe this thought could be generalized.
I refer to these functions that take a value and allow for flexible processing using a closure, as scope functions.
Such scope functions can indeed be asynchronous, however, even when they're asynchronous,
in my opinion, as long as the closure execution in the scope functions is done serially,
they don't necessarily have to be @Sendable
or @escaping
.
What are your thoughts about this?