Actor Model fundamentals compromised?

Following the context of the definitions

Motoko abstracts the complexity of the Internet Computer with a well known, higher-level abstraction: the actor model. Each canister is represented as a typed actor.

…its messages are processed one-at-a-time, even when issued in parallel by concurrent actors.

So far so good. And then I read the following:

Between suspension and resumption around the await , the state of the enclosing actor may change due to concurrent processing of other incoming actor messages.

It seems as a contradiction and the violation of one the fundamental principles of Actor Model namely the definition of an actor behaviour transition - actor does not receive the next communication until the current accepted communication is processed and replacement behaviour is determined.

If the actor’s shared methods represent a list of accepted actor’s communications and its corresponding handlers, handling any specific communication (executing a shared method) should be atomic, deterministic, and free of concurrency as far as the actor’s state is concerned.

What do I miss?

C.J

I think it says somewhere that functions that do not await in their body compute atomically.

I don’t think it violates that principle. It’s just the syntax of motoko kinda makes it less obvious.

Let’s say you call the foo public function, inside of which there’s an await expression. This implies foo is not to be seen an atomic transition, but one that should be segmented into multiple phases.

When encountering an await, the actor’s behavior actually becomes foo PLUS an implicit behavior that’s waiting a response to previous await to continue. This is allowed in other actor vocabulary too.

Asynchronousity is orthogonal to atomic transition. It’s just that sometimes you would want to block receiving new message when awaiting, but motoko does not provide such feature AFAIK.

1 Like

It does.

A function that does not await in its body is guaranteed to execute atomically - in particular, the environment cannot change the state of the actor while the function is executing. If a function performs an await , however, atomicity is no longer guaranteed.

However, the point is that this sort of allowed behaviour renders mute the promise of Actor Model of being synchronization free.

… and opens the door for many complex situations produced by us as humans

It is the programmer’s responsibility to guard against non-synchronized state changes.

Agree. The question is not Async vs. Atomicity. Rather, why the fundamental feature of Actor Model that makes it so beautiful - one does not have to synchronize anything between the communications - is not preserved. Is it by design? Is it interim?

Just to give the context

I’m not sure. My guts feeling is that to support blocking is hard, so they just opt to simpler implementation.

Having the ability to retain your call stack whilst calling another canister is essential to keeping the programming model simple. The alternative would be to manually make a copy of the state of your computation (stack data) every time you make a call. This would be a nightmare. Motoko has async/await for the same reason JavaScript does: to simplify asynchronous programming.

Canisters would not be scalable if they blocked by default on every external call.

That’s the thing. Actor Systems by definition are distributed and message driven, hence, do not have a call stack concept in the conventional sense. We are offered to think of Motoko actors in the context of Actor Model.

Shared Methods are types of communications accepted.
Return type is immutable which in the definition of serializable message that goes over the network.

I keep reasoning in terms of Actor Model and come to the point where it deviates - I need to be responsible for synchronization.

Want to understand the reason for that.
Hard to implement? Will be offered in the future?

Thank you guys. Great discussion

C.J

1 Like

Are you a software engineer? Virtually all programming languages are based on call stacks. Even Erlang has function calls. There is seriously no problem here. After playing around with Motoko for a while, the architecture should start making sense.

(Edit: removed harsh language.)

Can you define what do you mean by synchronization? And how is this problem solved in other lang, e.g. elixir? Just wanna make sure we’re on the same page to begin with.

@Nick, Yes I do write code and not just that for the last 20 years. For the last 7 - distributed systems is my interest.

Message based systems have traceable footprint but not the call stack since they involve multiple processes that distributed in time and space.

And the question is not about language but rather about conceptual paradigm that we are offered.

My questioning of the paradigm, which I am familiar with should not deserve comments that were edited after.

I think we can manage fruitful conversation, can we?

Appreciate it

C.J

1 Like

Synchronization of the actor state that documentation is referring to to make sure that while it awaits the state has not been changed.

Between suspension and resumption around the await , the state of the enclosing actor may change due to concurrent processing of other incoming actor messages. It is the programmer’s responsibility to guard against non-synchronized state changes.

Meaning that while the current message being processed it is possible for the state to be changed by other shared function - which is by definition is the processing of another message - while the previous has not been finished.

1 Like

Given your experience I guess it’s more efficient to point you to this resource:

Especially the Abstract Behavior section. There’s more details hidden there.

1 Like

It was also to my surprise when I first come across such property of motoko. After reading that doc I kinda have more understanding of what they’re really saying.

I cannot claim I understand everything (and I surely hope core team share more on the topic, cc @nomeata). To me the key take away is, message has a queue property, value of which is either Unordered or { from: A, to: B }, both A, B is principal of canisters.

The only guarantee of message ordering is that, two or more messages with same queue: { from, to } (that is between the same pair of canisters) are processed first-come-first-serve.

To me this sounds roughly a guarantee of non-surprising behavior in everyday use case.

Say canister A has only one foo public function, inside which it calls canister B and await multiple times. Now Alice and Bob both call A.foo, Alice comes first.

Although canister A will not block until finished handling Alice’s message (it will be able to serve Bob after encountering first await keyword) the above ordering guarantee ensures that the final result is effective the same as if canister A were blocking.

I think the idea here is that each actor-message is atomic & in the synchronization, but in the motoko language, the await keyword finishes that webassembly actor-message and creates a new message for the await call, using function-passing-callbacks.

2 Likes

You are probably all right, and the dissonance disappears when we notice that we have two layers of abstraction at play here.

The low layer is that of the actor model: Actors (or canisters) exchange messages, and we have all the nice atomicity guarantees we expect. This layer is also visible in the Internet Computer system: It’s the stuff that conensus, messaging, scheduling etc. is about.

On top of this layer, we built higher-level concept, and introduce the concept of a call. In particular, at this level we distinguish between messages that initiate calls, and those that respond to calls. This layer is implemented in the execution engine (e.g. to guarantee that every call will receive exactly one response, keeping track of outstanding responses). The atomicity guarantees of the lower layer still only apply to each message execution.

In that sense, Motoko actors are still actors, but what kind of messages they exchange when is additionally restricted by the concept of calls and call contexts. And, mostly for convenience, the await syntax allows you to program in this model a bit more conveniently than if you had to handle all responses via top-level functions as well (which would be the alternative, and actually has some benefits, e.g. the ability to upgrade while calls are in flight). But it’s just convenience, and as such obscures some of things happen underneath this layer, and sometimes that is dangerous.

So it is true that a serious developer on our platform will have to understand both layers, and understand which guarantees are provided where, and how the language and CDK they are using map to these concepts.

12 Likes

Great rundown. How does Rust compare to Motoko in this context?

Does Rust have the bility for the “upgrade while the call is in the flight” ?

@nomeata would a compelling minimal example of this behavior for Motoko be something like the following?

// Within the context of an `actor`
// which keeps some state that
// includes a `balance`
if balance == 0 {
  await ...;
  // Here we can’t assume that `balance`
  // is still `0` since other messages may
  // have mutated it in the meantime.
};
3 Likes