It's definitely a topic worthy of a design and solution...
We're often forced into "with..." style when some cleanup is needed, but the fact that things are then in a closure can be quite problematic.
actor X {
var state: [Int: Int] = [:]
func take(_: Bar) async {
await PseudoSpan.$current.withValue("updating state") {
self.state[1] = 1 // ERROR: Actor-isolated property 'state' cannot be passed 'inout' to 'async' function call
return await test()
}
}
}
The error is "right" in the sense that if the call suspends, you would have an exclusivity violation on actor state. But really, the only reason such TaskLocal using APIs are using closures is because they need to ensure cleanup, like this:
actor X {
var state: [Int: Int] = [:]
func take(_: Bar) async {
// push a task-local Span
self.state[1] = 1
return await test()
// pop a task-local Span
}
}
So this is very unfortunate since it makes traces much harder to use when we also mutate state like this.
If we had a form of:
actor X {
var state: [Int: Int] = [:]
func take(_: Bar) async {
let span = using PseudoSpan.start("updating state")
// setup:
// push task-local Span
self.state[1] = 1
return await test()
// cleanup:
// pop a task-local Span
}
}
The important thing here being that span must be kept alive until the end of the scope, and it would be semantically wrong to clean it up earlier. If it is just a variable we don't really have such strong guarantees where it'll be deinitialized; Though @Andrew_Trick may need to double check me there...
Relying on people manually doing a "pop the task-local" is unsafe since they'll / I'll forget and it'll corrupt the task-local stack. So... currently there is no good way to do this, unless we had some way to ensure "live until the end of this scope" for a variable, be it some using ... or other marker or something else...
So... yeah, I think this is an important topic worthy of a solution.