-
Notifications
You must be signed in to change notification settings - Fork 63
Description
Describe the bug
When using pessimistic locking (map.lock() / map.unlock()) in the Hazelcast Node.js client, concurrent updates to a shared counter result in lost increments.
Despite the expectation that locking should ensure exclusive access to the key across all threads, multiple clients appear to perform overlapping updates.
This leads to an incorrect final counter value — significantly smaller than expected — which suggests that the Node.js client’s implementation of map.lock() does not fully guarantee atomicity under concurrency.
Expected behavior
await map.lock(key);
const value = await map.get(key);
await map.put(key, value + 1);
await map.unlock(key);
no other client should be able to acquire the lock for the same key until it is released.
After 10 threads increment the same counter key 10 000 times each, the expected final value is:
10 threads × 10 000 increments = 100 000
Actual behavior
The test consistently produces incorrect results such as:
map-pessimistic: 6.871s
Final (pessimistic): 12 210 — expected 100 000
Even though the lock() and unlock() calls succeed, it appears that multiple asynchronous operations overlap during the network round-trip, breaking exclusive access guarantees.
Environment (please complete the following information):
-
Exact Node.js client version that you use (_e.g
4.1.0
): 5.3.0 -
Exact Hazelcast version that you use (e.g.
4.0.1
, also whether it is a minor release, or the latest snapshot): 5.4.0 -
Cluster size, i.e. the number of Hazelcast cluster members: 3
-
Number of clients: 1
-
Java version. It is also helpful to mention the JVM parameters: Open JDK17, inside Doker
-
Node.js version. It is also useful to mention
node
command line arguments if there are any: 20.19.5 -
Operating system. If it is Linux, kernel version is helpful: macOS 10.15.7 host with Docker Desktop (Linux 5.15 kernel)
To Reproduce
const map = await client.getMap("counters");
await map.put("counter", 0);
await Promise.all(
Array.from({ length: 10 }, async () => {
for (let i = 0; i < 10000; i++) {
await map.lock("counter");
try {
const v = await map.get("counter");
await map.put("counter", v + 1);
} finally {
await map.unlock("counter");
}
}
})
);