When running in the actual online environment, it is found that IC is not the “single thread mode” described in the developer documentation.
We are now developing a web3 application based on IC motoko, with thousands of new registrations every day.
Through the actual operation of the scale, it is found that it is not a true single thread, which makes it impossible to guarantee data consistency.
Sample code snippet:
let uid = LibAcc.nextUid(); // Through buffer.size() Build
let did = U.generateDID(uid);
let account : T.Account = {
index = uid;
did = did;
// ...
}
Accounts.add(account); // Accounts is Buffer
return (did,uid);
// Because it needs to meet the needs of large-scale users, Account is a multi-container structure that can be created horizontally, so uid needs to be in global increment mode.
// In the current user scale of the code, about 0.2% of the user uids are incorrect (the actual data is the previous uid)
Because we cannot use the lock function to ensure consistency like traditional databases, we feel very painful.
Is there any good solution?
Thank you, I hope to get community support and help.
At a high level, there is indeed a single execution thread per canister. The effects you describe are likely due to the async nature of execution: there might be a few messages “in flight,” and they move closer to completion, one at a time.
For example, if canister A receives message 1, but needs to check with canister B to respond, it sends a nested call. While waiting for a reply, it can start executing other incoming messages 2 and 3.
Once a response is received from canister B, canister A can finally reply to the original message 1. Meanwhile, messages 2 and 3, despite arriving after message 1, might already be answered because they didn’t require a nested call.
Hopefully, this makes it a bit clearer. Now, regarding the solutions: Motoko’s actor model is supposed to be the solution for this. Each actor instance should encapsulate the message progress state within itself, and hence, there should be no locks or global state issues. But I’m not a Motoko expert, so I’ll let the Motoko team answer that part… cc @ggreif@claudio
You need to show the real code. The one displayed doesn’t have anything async and should work unless there are bugs in these functions, but that has nothing to do with concurrency.
function add() {
let id = nextId;
await addUser(id);
nextId += 1;
return id
}
This will result in same ids if they were added at the same time
function add() {
let id = nextId;
nextId += 1;
await addUser(id);
return id
}
This will work well. nextId’s state gets saved before calling addUser
You must be aware that every await introduces a context switch and an associated commit point for the heap (actually all memories). So at that point another message might get scheduled and may see an inconsistent state, since the tail end of the await (the continuation) — that would normally ensure the consistency — hasn’t run yet.
This is a well-documented property of the IC and explicit locking is one of the strategies to mitigate this. See also @infu’s comment above.
Thank you very much for your sincere reply and support!
This is a real code snippet, there is an await in it, and I don’t know how to modify it at the moment
let uid = LibAcc.nextUid();
let did = U.generateDID(uid);
let token = U.generateToken(uid, 13);
let defaultAddress = U.generateMoneyAddress(uid, 0);
let ?mobNumber = Nat.fromText(e164 # mob) else throw Error.reject("Mobile phone number format is abnormal");
let _bool = await CanisterMap.mobileActor.createOne2(mobNumber, did);
let account : T.Account = {
index = uid;
var mobile = e164 # " " # mob;
var historyMobile = [];
cCode = cCode;
// ....
};
LibAcc.createOne(account); // Here the id will be automatically incremented
Just a nit, when you have a record field initialised from a homonymous binding from the environment, you can use punning, i.e. just mention the field name without the following =, like so:
cCode;
Dually, you can pattern-match on a record field, and bind it to a provided variable name:
switch account {
case { cCode = theCODE } { use theCODE ... };
}