Thanks for putting together the proposal. To me, it's directionally the right thing to do but unfortunately it fails to address one issue that could even lead to denial of service attacks for services: Impossibility of full resource reclamation which arguably violates Structured Concurrency.
Concretely, the issue is that swift_task_enqueueGlobalWith* will consume resources in the executor that cannot be freed until the timer/deadline expires. To address this, the proposal should return a handle to the scheduled timer that can be discarded when no longer needed.
Practically, it means that a service which reasonably implements handling requests wrapping the actual work in a withDeadline function would accumulate resources of already-finished requests.
Pseudo code example:
func handleRequest(_ request: Request) async throws -> Response {
// All the APIs used in here are for demonstrative purposes only, they don't actually exist.
let now = Clock.now()
let deadlineClamped = clamped(
request.deadline,
now + .seconds(1) ..< now + .minutes(5)
)
return try await withDeadline(clampedDeadline) { // (1) Triggers cancellation when deadline is reached
try await handleRequestImpl(request) // (2)
} // (3)
}
The issue here is that if handleRequestImpl actually does its job very efficiently, we'll still keep around the deadline that withDeadline would need to schedule. withDeadline would have to schedule a deadline in case handleRequestImpl takes too long, for example it might reach out to other services which might be too slow.
Under Structured Concurrency this would be non-compliant because
The core concept is the encapsulation of concurrent threads of execution (here encompassing kernel and userland threads and processes(*)) by way of control flow constructs that have clear entry and exit points and that ensure all spawned threads have completed before exit.
(*) in Swift Concurrency parlance that'd be a task.
In the above example code, what I would like to see is the following:
(1) should under the hood call swift_task_enqueueGlobalWithDeadline with the appropriate deadline
- So if
(2) completes quickly, ...
- ...
(3) should call a dispose/cancel/... method that can remove the enqueued global deadline from the executor -- This step impossible under the current proposal
If it helps, to make the problem visible today, you could run this
cat > /tmp/repro.swift <<"EOF"
@main
struct Example {
static func main() async throws {
while true {
do {
async let _ = Task.sleep(for: .seconds(1000000000000000))
try? await Task.sleep(for: .nanoseconds(0))
} // EDIT: This is non-obvious but this `}` _will cancel_ all unawaited `async let`s such
// as the `async let _ = Task.sleep(for: .seconds(1000000000000000))` above.
// And yet: It accumulates resources --> Structured Concurrency violation
}
}
}
EOF
and then
swiftc -O -parse-as-library -o repro repro.swift
./repro # wait 30 seconds, then look at the memory consumption of this binary
On my machine for example it produces 2 GB of timer waste in 30 seconds:
$ /usr/bin/time -l /tmp/repro & sleep 30 ; pkill repro
[1] 58273
johannes:~
$ time: command terminated abnormally
30.04 real 14.34 user 36.25 sys
[...]
2077149008 peak memory footprint