This is, sadly, as expected.
The problem can be illustrated by the following:
- create task 1
- create task 2
- task 2 runs / PERHAPS even concurrently to: task 1 runs
- task 2 invokes handle(received:)
- task 2: just happens to be slightly quicker to get to the invocation
- task 1 invokes handle(completion:)
There is zero ordering guarantees between âinvoke handle(received:)â and âinvoke(handle:completion)â because the moment we hit the Task{}
we left all scheduling to completely independent tasks with no relationship to eachother at all.
â
I feel a lot of code will be âbittenâ by this andâpersonallyâwould love for the introduction of something akin to âsend
â i.e. enqueue the invocation on the actor and then return â thanks to such operation (sometimes called âtellâ in other actor runtimes), weâd be able to guarantee ordering in such uni-directional calls.
We canât express this today.
We either can await
for the entire call to complete, or we have to Task{}
and cause concurrent execution. The actor has no idea that it will have any calls until those concurrent tasks eventually run and invoke things on it.
A âtell
â/âsend
â operation would operate differently: Instead of Task { await self.handle(completion: completion) }
weâd say send self.handle(completion: completion)
. which would mean âdonât wait for the response but return once the invocation has been enqueued in the actorâs mailbox, this way there is no additional indeterminism injected into the order by the addition of those Tasks that we only created because we donât want to wait for the results really.
Itâs up in the air if weâll get such operations though â we definitely have hit this issue multiple times already thought and I (personally) really do think itâd be useful to allow such thing.
â
The entire issue can already be seen in a simplified case, like this:
async let a = actor.tell(âAâ)
async let b = actor.tell(âBâ)
// but we actually meant
// donât wait actor.tell(âAâ)
// donât wait actor tell(âBâ)
It is not deterministic what order the actor gets those messages in, and we have no way to express âenqueue but donât waitâ, so the only way to get the ordering we want to so await on both calls in the current task. The actor is sequential, sure, but the async lets create new tasks and those are executing concurrently, so the actor will get the invocations enqueued in any order.