Open 0xBruceLiao opened 1 day ago
I experimented with this code using both official Akka F# and Akkling libraries. Here's what I've found.
actor computational expression assumes use of a recursive function (loop in the example above), and like every recursive function it introduces a risk of stack overflow in case F# compiler is not able to do a tail call optimization. More about it here: https://cyanbyfuchsia.wordpress.com/2014/02/12/recursion-and-tail-recursion-in-f/
Because of p.1 I always review use of return! and try to have only single return after mailbox.Receive. I would write the code above like this:
let! msg = mailbox.Receive()
return!
match msg with
| Hello -> loop ()
| Bye s -> loop ()
This code works without problems whether I use official Akka F# API or Akkling.
Not only the original code causes stack overflow when using Akka F# API, it also becomes grotesquely slow after running a few thousand cycles. In fact I could not wait until stack overflow exception will be thrown and just terminated the test app after about 20000 messages received by the actor.
The same code worked fine and quickly when using Akkling.
I checked the implementation of Return method in both libraries.
Akka F#:
member __.ReturnFrom(x) = x
member __.Return x = Return x
Akkling:
member __.ReturnFrom (effect: Effect<'Message>) = effect
member __.Return (value: Effect<'Message>) : Effect<'Message> = value
When using return with a bang (!), ReturnFrom method is called, and both implementations of ReturnFrom are the same. Return member is implemented differently but it should not be used in the code above. But there are some major differences between official Akka F# and Akkling API, so it's more than just the implementation of a single method that affects the execution. I know that Bartosz (Akkling writer) made some optimization improvements in actor computation expression while working on Akkling library.
To summarize:
Actually I've found an old discussion between myself and @Horusiath about early return (https://github.com/Horusiath/Akkling/issues/149) and misbehavior of the code with early return, and we both agreed that early return is not something expected in expression-based languages like F#, so the code should be rewritten properly.
There are some code examples implementing computation expression with early return, like this one (https://gist.github.com/Savelenko/e721e3fb286f15104e5be2425f9cfe66), but I don't think it should prioritized for F# API.
Can I mark this issue as closed then?
Version Information
My paket.lock info:
Describe the bug
First, I switched from MailboxProcessor to Akka,MailboxProcessor does not have this bug, but when I switched the code to Akka, this exception occurred. After testing, I found that this bug is triggered whenever my code has an early return and also a return at the end of the function,Like this:
Exception:
To Reproduce
Run the following code and then wait:
If all branches use
return! loop()
and there is noreturn! loop()
at the end of the function, then there will be no exception. Alternatively, if only the end of the function usesreturn! loop()
, there will also be no exception.But I think that this coding style should not produce an exception。