ox icon indicating copy to clipboard operation
ox copied to clipboard

Actors impl can easily lead to multithreading issues

Open kostaskougios opened this issue 3 months ago • 7 comments

I think the way ask & tell are impl they can lead to unwanted behaviour, i.e. say we have an actor that can calculate the max of a list. We would expect the actor to be called with a readily available list, but a call like:

actorRef.tell(_.max(calcMyList(...)))

means that the actor thread will also have to calculate the list.

The correct way to call it would be:

val l=calcMyList(...)
actorRef.tell(_.max(l))

but this can easily be missed by the developer writing the code, leading to slow throughput of the actor (because it is also calculating arbitrary lists, not just their maximums)

More silly code could lead to unpredictable behaviour:

var i=0
actor.tell(_.addOne(i))
while true do i+=1

// what will be the arg value to addOne?

Is there a solution to this? Maybe if:

actorRef.tell(_.max , calcMyList(...)) 

where the tell and ask methods take a FunctionN and then separately by-value their arguments like

class ActorRef...:
   def tell[A](f:Function1[A,Unit],arg1:A) ...

kostaskougios avatar Sep 26 '25 15:09 kostaskougios

Yes, you're right that there's a couple of gotchas when using actors - exposing mutable state to the actor, or exposing mutable state from the actor, or calculating parameters.

Some background on the current design is here: https://github.com/softwaremill/ox/blob/master/doc/adr/0006-actors.md

As long as we'll want to stick with the method-invocation syntax, without creating a dedicated AST representing commands/messages as in Akka Typed, I don't think much can be done about mutability. Maybe capture checking with the mutability extensions would help, but that's quite far off still.

The fact that parameters are pre-computed could be verified by a macro (possibly one extending the macro from the ADR), or could use the exploded syntax. Although this solves only some problems.

I can see the problems, but I'm not sure how to best solve them :)

adamw avatar Sep 29 '25 13:09 adamw

what I was thinking was more in terms of :

var i=0
actor.tell(_.addOne,i)
while true do i+=1

That way we know i will be 0.

Also it sorts the problem that calculating the param may be done inside the actor's thread. If the call is like:

def complexCalculation:Int = ... a complex/slow calculation
actor.tell(_.addOne,complexCalculation)

then complexCalculation invocation is done before tell method is called. That helps with the exceptions too, as we would expect any exceptions of complexCalculation to be thrown not inside the actor.

The change would be to add a few methods to the actor like (this is from my project , what I am doing right now):

  def askF[B, P1, P2](f: A => (P1, P2) => B, p1: P1, p2: P2): B = ask(f(_)(p1, p2))

and this can be called like a.askF(_.add2, 5, 6)

It solves this issue.

Anyway, it is just an idea, and I can always impl some util methods on my own project to do this. But at the moment I can't use Ox actors because they require supervised, I don't have control of the flow where my code is executed and I need long lived actors. Maybe I'll open a different issue to have whatever is in supervised available outside of blocks so it's lifecycle can be managed externally.

Exposing the actors mutable state can be limited with proper encapsulation. But there is an other way (TLDR) by changing the ask to return a function instead of actually doing the call, with this:

  def createAsk[B, P1, P2](f: A => (P1, P2) => B): (P1,P2)=> B = (P1 , P2) => ask(f(_)(p1, p2))

Then

val add2 : (Int,Int) => Int = a.createAsk(_.add2)
val add3 : (Int,Int) => Int = a.createAsk(_.add3)

Then expose vals add2 and add3 and hide the actor a. This can get a bit cumbersome to manage if there are many actor methods and also ask vs tell. This seems the most correct approach but I am not using it at the moment in my project due to overhead of managing all these functions.

kostaskougios avatar Sep 30 '25 19:09 kostaskougios

Yes, I like the idea of separating the method & parameters, indeed it solves a couple of problems.

As for running global actors - it's an "axiom" of Ox that forks can only be created within concurrency scopes. But, if you need a global process, you can start a scope at the top-level (in your main), or use OxApp which does it for you. Then, you get a concurrency scope which lives as long as your app.

adamw avatar Oct 01 '25 07:10 adamw

The issue in my case is that I don't control the flow of my app, a native app starts and spawns a jvm and then within that jvm some methods of mine are called, all which should return quickly, so there is no method that I can create the implicit required by ox. I need to be able to create that implicit and let it be stored in a val inside a class. Then I'll have to manually shut it down before exiting the app.

kostaskougios avatar Oct 01 '25 16:10 kostaskougios

@kostaskougios Ah I see, yeah that's a problem. Yeah, I guess you'd have to start a "raw" virtual thread on the first callback that you receive, and in that virtual thread create the scope and the actors. You can then use actors outside of scopes, they just send messages over a channel. And when the app exits, interrupt that "raw" thread which will cleanup properly.

adamw avatar Oct 02 '25 12:10 adamw

Let's leave this open as an idea for improving the actor API.

adamw avatar Oct 02 '25 12:10 adamw

@kostaskougios Ah I see, yeah that's a problem. Yeah, I guess you'd have to start a "raw" virtual thread on the first callback that you receive, and in that virtual thread create the scope and the actors. You can then use actors outside of scopes, they just send messages over a channel. And when the app exits, interrupt that "raw" thread which will cleanup properly.

yes that's what I would have to do. The other thing I need in my code is to be able to remove old messages from the actor's queue, anyway I'll open a couple of feature requests, not sure the 2nd one makes sense for Ox but will explain better in the request.

kostaskougios avatar Oct 05 '25 11:10 kostaskougios