tqwewe / kameo

Fault-tolerant Async Actors Built on Tokio
https://docs.page/tqwewe/kameo
Apache License 2.0
634 stars 16 forks source link

[FEATURE] - more flexible concurrency #80

Closed blueforesticarus closed 2 weeks ago

blueforesticarus commented 2 weeks ago

Motivation

Lets say my actor represents a client to an API. Among the things this actor does is request data on Items. The requests are batched using an internal queue and a separate task, but we want the actor to be able for the individual items. The initial handling of the request does need &mut, (to add items to the queue), but waiting on the result does not need (and cannot hold up) the &mut Self.

ie. it is a class of problem which has a sequential mutable part, and a concurrent part (and really it might need the &mut after the current part as well.)

Proposed Solution

None. Want to open this discussion.

Alternatives Considered

Returning a one-shot channel.

tqwewe commented 2 weeks ago

If I understand correctly, could this be solved by using only tell requests between the actors instead of ask? That way, your actor which needs to mutate the state can handle the initial request, and then tell a message to some other part of your app (actor pool, tokio task, etc), and then when that other part finished processing, it sends a different tell message to the original actor which can finalize whatever it needs to using its &mut self access.

Would this approach possibly solve your issue? Its the more "pure" way of messaging between actors and can be pretty powerful.

blueforesticarus commented 2 weeks ago

Yes, that is an option.

Here is my counterpoint: there is a reason kameo supports ask requests.

It is common and useful to have that bidirectional request-style communication. In my mind the greatest benefit is that it makes actors kinda like async objects (in the java/oop sense), where the message passing looks like functions. I think this is an easier model to think-in than channels (and also the reason why alot of net protocols are also request response, rather than bidirectional pubsub); it's hard to think about control flow when the response is disconnected from the request ("ie. the data/event goes here, then here, then eventually works its way back around to the caller, hopefully...").

Here is my stab at a current way to do it.

  1. Create a one-shot channel in the handler, pass the Tx to the inner handler (spawn, queue, another actor, whatever), return the Rx to the caller (asker).
  2. The inner task does whatever, then send a message (tell) back to the actor with the Tx
  3. The actor updates its state and then sends the result through the Tx
  4. The caller asks the actor and then awaits the returned channel. (A side benefit of this method is the caller can decide whether to await the completion or do something in the meantime).

In the course of writing this, this feels more solid. I'll try it out and post some code.

A side note: one of the things making my head hurt here is the difference between fast and slow async. Ie. putting something in an async queue is fast, network requests are slow. When designing things, one kind has to know how long they expect any given .await to block for and when and why.

tqwewe commented 2 weeks ago

Kameo does let you get control of the reply oneshot channel on the Context struct using .reply_sender(). You can store this and reply at any later time which might give you some of the flexibility you're looking for

blueforesticarus commented 2 weeks ago

A random thought, but async tasks could probably be written to take a RefCell (where Self is the actor) and then as long as borrows are not held across an await point (which I believe the borrow checker would catch). It ought to be possible to write long running tasks as regular async functions, without having to spawn tasks, or deal with the shared state spawning creates. (ie. no members of the Actor struct need Arc/Mutex)

( Then the question is what polls the tasks, and how would tasks be defined and queued )