How does ICP canister handle asynchronous calls and event queue?

Let’s define a canister like this:

actor Counter {

  stable var counter = 0;

  public query func get() : async Nat {
    return counter;
  };

  public func set1() : async () {
    let a = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15...1000000];
    label l for(i in a.vals()){
      continue l;
    };
    counter := 1;
  };

  public func set2() : async () {
    counter := 2;
  };

};

The set2() will directly set counter to 2 while set1() will set counter to 1 after a loop.

So suppose user1 calls set1() and then user2 calls set2(). The 2 calls are almost same time but user1 call before user2. So what is result to call get() after user1 and user2?

In the other words, how does IC canister work for asynchronous calls? Does canister have a event queue manager to manage the inquiry? Or we have to deal with asynchronous calls to by ourselves in motoko?

Canister message execution is single threaded. So the update call that is received first will be processed first, and the update call that is received second will be processed second.

If both update calls arrive in this same order but right around the same time, they’ll probably be queued up together and processed in that order in the same consensus round, meaning that both calls should take ~2 sec total to complete, and not ~ 2+2=4 sec total.

Hi ICME thank you for answering. Just want to confirm it: if two calls are queued and executed in single threaded, why the total completed time is not 2+2 = 4s ?

Say in my case. user1 calls set1() 1 nano sec before user2 calls set2(). When 2 calls finish, we check counter by get(). Should we get 1 or 2?

In response to your first question:

I’m not an expert on consensus or the canister message queue, but from my understanding messages that are processed together in the same consensus round are batch processed together during that round, with messages being processed in the order they appear in the message queue. That single consensus round should take ~2sec.

First off, this depends on when the call is received by the canister, and then the subsequent ordering of that message in the queue (not when the call is made by the frontend).

In the case you described (where set1() is processed before set2() (even if both messages were received at the same time), get() should return 2.

I think this is relevant as well.

If no inter-canister call then everything is fine since the canister state won’t change when program is running. But the atomicity will be broken when inter-canister call happens and still await.

So in my case it should be okay. But getting involved with inter-canister call will be another story.