I tried to achieve "well-designed" actor system. B...
# coroutines
j
I tried to achieve "well-designed" actor system. But in my experimentations I found very difficult to compose actors without using the ask & wait anti-pattern. It is possible, but it quickly leads overcomplicated code IMHO. Do you have good resources where I could learn how to nicely compose actors without ask & wait?
e
what do you mean “ask and wait”? can you show code examples?
Another example, using explicit message types:
Copy code
sealed class Message
data class Ask(val question: Question, val response: CompletableDeferred<Response>)

suspend fun usage() {
  val response = CompletableDeferred<Response>()
  
  actor.send(Ask(Question(), response)) // ask
  val result = response.await() // wait
  
  // use the result
}
d
You might be able to copy the new actor draft from the branch in the repo... but then I suppose you probably saw that too? https://github.com/Kotlin/kotlinx.coroutines/tree/actors-prototype
I personally think there is a
actWithResult { }
pattern missing there... but on one discussion with @[removed] it might not be so difficult to implement. I think the use case is running using a regular
act { }
and getting results in another context using
actWithResult { }
, or simply having a background context to call
actWithResult { }
, the actor would insure that any requests will be handled synchronously...
j
Indeed it is easy to write a
actAndReply
suspending function on top of
act
. The problem is more about best and bad practices. @[removed] and @elizarov said that such pattern should be discouraged by kotlinx.coroutines, because it is an anti pattern (https://github.com/Kotlin/kotlinx.coroutines/pull/485#discussion_r208939367). So my question is more: How one can write simple actor composition and avoid this ant-pattern?
d
Is that what you're looking for @Jonathan?
j
@dave08 Not really sorry. I knew all that, and I did implement my own version of the abstract actor with an
act
and
actAndReply
in order to play with it. The thing is, since the ask & wait is considered an anti-pattern, I'd like to avoid it. But I struggle to compose actors without it.
d
Maybe have
actAndReply
return
Deferred
?
That way we avoid the extra param passing... @Jonathan?
j
hmmm... Usually if a method returns a deferred it is better to simply makes the method suspend.
suspend fun foo(): Result
is equivalent and should be preferred to
fun foo(): Deferred<Result>
.
And the thing is in both cases, it would still be an ask & wait which is what I want to avoid.
e
You avoid it by structuring your code as data processing pipe-line, instead of a regular request-response.
j
The answer is probably in the actor-model itself, which is then not really related to Kotlin. But after reading many articles and blog (usually with examples in Scala) I still don't know.
e
It scales better and it just leads to saner code
j
Yes. I got that. Now the question is how to compose actors?
(I can do it, but it quickly becomes complicated without the ask&wait)
d
Right, but at least the user opts into when they block on the call just like CompletableDeferred param passing... which is currently recommended in the docs... @elizarov That's exactly what we did, but I found myself with tons of side channels for progress notification etc... is that what you mean by composing @Jonathan?
So it ends up not being a pipeline anymore...
e
If you need ask&wait, then you’ll like not need actors in that places at allo.
d
It's all one resource being used by multiple services, and it needs to report its state in different ways in my case...
e
Just write regular functions, top-level, pure functions
j
Is actor-composition a good practice?
e
What is actor-composition? And actor is just than thing on the other side of a channels that keeps some state for you. You usually pipe-line them (one actor does something, sends something down the pipelines, etc…). That’s how you build a system out of them
Sometimes you need to get back data back from an actor. But you should not wait. You should give your channel to that actor and let it send of that channel, so that you can process responses asynchronousy.
j
Yes, and that's what I did. But then I ended with complicated code. Because when I receive a message from an actor to which I delegated work, I have to retrieve the context of the request in order to know what to do with that message.
Maybe actor is simply not the good abstraction for composition then.
(By actor composition I meant creating a (composed) actor, which decompose and delegate its work to other (component) actors)
e
You “compose” actor if and only if you need to scale them differently. (have X copies for actor doing one piece of work and Y copies for actor doing antoher piece of work)
If you don’t need that, you don’t need actor. You use plain suspending functions
j
Yes, I was playing in this context. I tried to create an actor delegating smaller piece of work to x copies of sub-actor A and y copies of sub-actor B.
e
In this case they have to use channels to communicate back-and-forth. If they ask & wait, it will not scale the way you want to.
j
Maybe my mistake is to use actor to perform work? I should only use them to maintain state? Therefore the whole concept of decomposing work would become meaningless.
e
You use actors to maintain state. You use worker pool (a set of N coroutines) to perform concurrency-limited work (we don’t have nice wrapper for that pattern yet, but will do in a future)
👍 4
j
Thanks for your enlightenments @elizarov.