Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ There are multiple ways to contribute:
Note that you don't need to be a developer to help us.
Contributions that improve the documentation are always appreciated.

If you feel yourself in need of assistance, please reach us directly via [Slack](https://slack.hazelcast.com/).
We have a separate [channel](https://hazelcastcommunity.slack.com/archives/C01PP55A667) for the Node.js client.

## Issue Reports

Thanks for reporting your issue.
Expand Down
72 changes: 69 additions & 3 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -1803,6 +1803,70 @@ Hazelcast Map uses `MapListener` to listen to the events that occur when the ent
evicted/removed from the Map. See the [Map Listener section](#8521-map-listener) for information on how to create a map
listener object and register it.

`IMap` provides an entry locking mechanism: [Locking Maps](https://docs.hazelcast.com/hazelcast/5.7/data-structures/locking-maps).
You can lock and unlock entries via the `Map.lock` and `Map.unlock` methods respectively.
Here is an example:
```javascript
await map.lock('key');
// no other client instance can access the entry
try {
await map.set('key', 'some-value');
}
finally {
// the entry can be used by other client instances
map.unlock('key');
}
```

The lock is always reentrant for the same client, which may cause data races.
Consider the following example:
```javascript
const client = await Client.newHazelcastClient();
const key = 'k1';
const map = await client.getMap('my-map');
await map.put(key, 0);

async function task() {
// each task runs concurrently
await map.lock(key);
try {
const value = await map.get(key);
await map.put(key, value + 1);
} finally {
await map.unlock(key);
}
}

// 100 concurrent tasks
const tasks = Array.from({length: 100}, task);
await Promise.all(tasks);
const value = await map.get(key);
console.log('value:', value);

await client.shutdown();
```

Although the expected value is `100`, the actual value is `1`, since there is a data race.
In order to prevent the race, execute the lock related code in `LockContext.run`:
```javascript
// updated task function
async function task() {
// each task runs concurrently
// create a lock context to prevent race
await LockContext.run(async () => {
await map.lock(key);
try {
const value = await map.get(key);
await map.put(key, value + 1);
} finally {
await map.unlock(key);
}
});
}
```

With that change the data race is prevented, and the expected value `100` is printed.

### 8.4.2. Using MultiMap

Hazelcast `MultiMap` is a distributed and specialized map where you can store multiple values under a single key. For details,
Expand Down Expand Up @@ -1831,6 +1895,9 @@ Hazelcast MultiMap uses `EntryListener` to listen to the events that occur when
MultiMap. See the [Entry Listener section](#8522-entry-listener) for information on how to create an entry listener object and
register it.

`MultiMap` supports locks, similarly to `IMap`.
See [Using Map](#841-using-map).

### 8.4.3. Using Replicated Map

Hazelcast `ReplicatedMap` is a distributed key-value data structure where the data is replicated to all members in the cluster.
Expand Down Expand Up @@ -4473,10 +4540,9 @@ Test script automatically downloads `hazelcast-remote-controller` and Hazelcast.

# 11. Getting Help

You can use the following channels for your questions and development/usage issues:
You can use the following channel for your questions and development/usage issues:

* This repository by opening an issue.
* [Slack](https://slack.hazelcast.com)

# 12. Contributing

Expand All @@ -4489,6 +4555,6 @@ you can always open a pull request on this repository for your other requests su

# 14. Copyright

Copyright (c) 2008-2022, Hazelcast, Inc. All Rights Reserved.
Copyright (c) 2008-2026, Hazelcast, Inc. All Rights Reserved.

Visit [www.hazelcast.com](http://www.hazelcast.com) for more information.
4 changes: 0 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@
<a href="https://www.npmjs.com/package/hazelcast-client">
<img src="https://img.shields.io/npm/v/hazelcast-client" alt="NPM version">
</a>
<a href="https://slack.hazelcast.com">
<img src="https://img.shields.io/badge/slack-chat-green.svg" alt="Chat on Slack">
</a>
<a href="https://twitter.com/Hazelcast">
<!-- markdownlint-disable-next-line MD013 -->
<img src="https://img.shields.io/twitter/follow/Hazelcast.svg?style=flat-square&colorA=1da1f2&colorB=&label=Follow%20on%20Twitter" alt="Follow on Twitter">
Expand Down Expand Up @@ -150,7 +147,6 @@ You can use the following channels for your questions and development/usage issu
* [GitHub repository](https://github.com/hazelcast/hazelcast-nodejs-client)
* [Complete documentation](https://github.com/hazelcast/hazelcast-nodejs-client/blob/master/DOCUMENTATION.md)
* [API documentation](http://hazelcast.github.io/hazelcast-nodejs-client)
* [Slack](https://slack.hazelcast.com)

## Contributing

Expand Down
51 changes: 51 additions & 0 deletions code_samples/map_lock_context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright (c) 2008-2026, Hazelcast, Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';

const { Client, LockContext } = require('hazelcast-client');

(async () => {
const client = await Client.newHazelcastClient();
const key = 'k1';
const map = await client.getMap('my-map');
await map.put(key, 0);

async function task() {
// each task runs concurrently
// create a lock context to prevent race
await LockContext.run(async () => {
await map.lock(key);
try {
const value = await map.get(key);
await map.put(key, value + 1);
} finally {
await map.unlock(key);
}
});
}

// 100 concurrent tasks
const tasks = Array.from({length: 100}, task);
await Promise.all(tasks);
const value = await map.get(key);
// v should be 100
console.log('value:', value);

await client.shutdown();
})().catch(err => {
console.error('Error occurred:', err);
process.exit(1);
});
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hazelcast-client",
"version": "5.6.0",
"version": "5.7.0",
"description": "Hazelcast - a real-time stream processing platform - Node.js Client",
"main": "lib/index.js",
"types": "lib/index.d.ts",
Expand All @@ -14,7 +14,7 @@
},
"devDependencies": {
"@istanbuljs/nyc-config-typescript": "~1.0.2",
"@types/node": "~10.17.60",
"@types/node": "~22.13.14",
"@typescript-eslint/eslint-plugin": "~5.45.0",
"@typescript-eslint/parser": "~5.45.0",
"chai": "~4.3.6",
Expand All @@ -35,8 +35,8 @@
"source-map-support": "~0.5.21",
"thrift": "~0.16.0",
"ts-node": "~10.9.1",
"typedoc": "~0.22.18",
"typescript": "~4.7.4",
"typedoc": "~0.23.28",
"typescript": "~4.9.5",
"winston": "~3.8.2",
"yargs": "~17.5.1"
},
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ export * from './PartitionService';
export * from './CPSubsystem';
export * from './sql';
export * from './security';
export {LockContext} from './proxy/LockContext';
3 changes: 2 additions & 1 deletion src/network/ConnectionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -941,7 +941,8 @@ export class ConnectionManager extends EventEmitter implements MembershipListene
const tokenCredentials = securityConfig.token;
if (tokenCredentials != null) {
const token = tokenCredentials.token;
const encoding = tokenCredentials.encoding;
// cast the encoding to BufferEncoding to make TypeScript happy
const encoding = (tokenCredentials.encoding.toLowerCase() as unknown) as BufferEncoding;
payload = Buffer.from(token, encoding);
} else {
// If we are this far, we ruled out the possibility of credentials being
Expand Down
2 changes: 2 additions & 0 deletions src/proxy/IMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,7 @@ export interface IMap<K, V> extends DistributedObject {
* **Important:**
* In the Node.js client, all lock operations from the same client instance share the same lock ownership.
* This differs from the Java client where each thread has separate lock ownership.
* Use `LockContext.run` method to limit the lock ownership in concurrent code.
*
* @param key the key of the map entry
* @param leaseTime lock is automatically unlocked after `leaseTime`
Expand Down Expand Up @@ -554,6 +555,7 @@ export interface IMap<K, V> extends DistributedObject {
*
* **Important:**
* In the Node.js client, all lock operations from the same client instance share the same lock ownership.
* Use `LockContext.run` method to limit the lock ownership in concurrent code.
*
* @param key the key of the map entry
* @param timeout server waits for `timeout` milliseconds to acquire
Expand Down
28 changes: 28 additions & 0 deletions src/proxy/LockContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@

import { AsyncLocalStorage } from 'node:async_hooks';
import * as Long from 'long';

const lockContextStorage = new AsyncLocalStorage();
let lockID = Long.ZERO
Comment thread
emreyigit marked this conversation as resolved.
Resolved

export class LockContext {

/**
* Runs the given async function / promise with a new lock ID.
*
* @param f Promise the async function to run with the lock ID.
*/
static async run(f: () => Promise<void>) {
lockID = lockID.add(1);
await lockContextStorage.run(lockID, f);
}
}

export function getLockID(): Long {
const lid = lockContextStorage.getStore();
if (lid) {
return lid as Long;
}
return Long.ZERO;
}

Loading
Loading