Two updates returning the same exact nanosecond timestamp. Backend method call (Motoko) is not being rejected when called immediately after the game is over

I am trying to create a minesweeper game, the game ends when you hit a bomb. In my backend update method, when the game ends, I remove the active game log from the activeMineGameMap, therefore any new attempt to call this method should get rejected because I have an if statement to check if the game exists in this map.

Here’s a snippet of code, the arrows shows that if the game has finished, it should be deleted from the Map, and the next request should get rejected… but it’s not getting rejected? This is where I get confused. During my testing, I was still able to call this method and get a response a split second after the game is over.

Two questions:

  1. Does the backend not complete each state change before allowing another request to initiate the same process? Is this because both requests are in the same confirmation block? If so, can subsequent calls within the same block get through if statements they shouldn’t be able to get through since the state update hasn’t been completed? How do I prevent this?
  2. Can this second request change state? It looks like it is able to read state, but not update state.

I added timestamps in both my js and in my motoko. You can see in the obj that isBomb is true twice. Which shouldn’t be possible because if isBomb is true, the game will get deleted, thus rejecting any future calls.

Thank you in advance for any help. Let me know if anything is confusing.

I believe your first question is right, you can have multiple update calls at the same time and they are executed (aprox) at the same time.

If you check the ICRC1 standard, they have a check tx time function that prevents exactly this - duplicate calls.

As stated in the ICRC1 standard readme:

The created_at_time parameter indicates the time (as nanoseconds since the UNIX epoch in the UTC timezone) at which the client constructed the transaction. The ledger SHOULD reject transactions that have created_at_time argument too far in the past or the future, returning variant { TooOld } and variant { CreatedInFuture = record { ledger_time = … } } errors correspondingly.

1 Like

Your code performs two await’s before deleting the player from the game map. An await is an inline message receive, i.e., not atomic and can therefore yield to other pending calls before resuming. That is, you need to make a suitable state change before the first await in order to prevent concurrent entry into the conditional.

It would be great if Motoko could somehow warn about state dependencies crossing await points, since this appears to be a common misunderstanding that I suspect results in frequent (and often subtle) concurrency bugs.

1 Like

Andreas, thank you for your response. I am now looking more closely at this, could you elaborate on a “suitable state change.”

Should I design all my public update methods to not contain any awaits to prevent these concurrency related bugs?

Regarding the time: The spec says:

the time, as observed by the canister, is monotonically increasing, even across canister upgrades.

It doesn’t say it’s strictly monotonically increasing. In practice that means that all calls within the same consensus round see the same system time.

As for atomicity and message ordering you can read a lot more here and here.

1 Like

You can use await, but if a public method is supposed to result in some global state change for the actor that is supposed to affect future calls, then it must somehow record that before the first await. The details of course depend on your application. In general, this may require implementing some sort of lock-like state flags or even a state-machine-style protocol.

1 Like

Why are your functions freezeGameLog and updateEarnings asynchronous? Why do you have to await them? If you can make them synchronous then your problem goes away.

1 Like

I have updated my code and removed all unnecessary awaits. Future public methods still depend on a global state change, so I applied state flags and it looks like it is holding up to my testing. Thank you for the help!

@timo @rossberg