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.
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?
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.
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.
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.
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.
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.
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!