What’s New in Hazelcast Node.js Client
Rejoice, JavaScript people! There are many new features in Hazelcast Node.js Client 0.4.1. In the 10 minutes, you spend reading this blog I will cover “what’s new”! Also, if you read this blog post till the end, you will also learn what’s coming to future releases.
It has been a while since out first blog post on Hazelcast Node.js Client [1]. Our brave developers have been very busy adding new features and making Hazelcast Node.js Client faster!
What’s new
node v6.3.1 (npm v3.10.3)
with Hazelcast Client 0.4.1
. Many examples contain modern EcmaScript 6 syntax constructs, like classes and ⇒
aka «arrow function» [2].Alright. Since our previous announcement of v0.2, we added the most of the frequently used features of Hazelcast to Node.js client. Including:
- Distributed Map aka
IMap
with support of Predicates and Entry Processors - MultiMap
- Set
- List
- Distributed Locks aka
ILock
- Queue
Also, we brought full interoperability with other Hazelcast clients [3].
I don’t know how to put this, but it’s kind of a big deal.
Anchorman: The Legend of Ron Burgundy
Serialization and Interoperability with other Hazelcast clients
Hazelcast Node.js client now supports all native serialization techniques that Hazelcast supports. Meaning you can just connect your Node.js client to your working Hazelcast cluster and read what is already there, put new objects and read them from other clients. The fact that Node.js client supports Hazelcast native serialization means it is also fully compatible with all available client languages. The client serializes string, number, and array data types automatically. This makes text-based serialization formats like JSON or XML suitable candidate for a message. Developers can provide serializers for custom objects.
For example, a Java application uses Hazelcast Java Client to store the results of a long-running computation (like Map/Reduce job), and Node.js, .NET
or Python applications can consume the results for displaying for the web (Node.js), in Rich Desktop Application (.NET / C#) or for further research and data science with Python math packages.
To demonstrate this approach, let’s write Java
⇐⇒
Node.js
application. We will use Person
object from the previous blog post. It has 3 properties – firstName
, lastName
and age
.
public class Person implements IdentifiedDataSerializable { <i class="conum" data-value="1"></i><b>(1)</b> String firstName; String lastName; int age; public Person() { } public Person(String firstName, String lastName, int age) { this.firstName = firstName; this.lastName = lastName; this.age = age; } @Override public void writeData(ObjectDataOutput out) throws IOException { <i class="conum" data-value="2"></i><b>(2)</b> out.writeUTF(firstName); out.writeUTF(lastName); out.writeInt(age); } @Override public void readData(ObjectDataInput in) throws IOException { <i class="conum" data-value="3"></i><b>(3)</b> this.firstName = in.readUTF(); this.lastName = in.readUTF(); this.age = in.readInt(); } @Override public int getFactoryId() { return 42; } @Override public int getId() { return 42; } }
1 | A Person object implements IdentifiedDataSerializable – fast serialization from Hazelcast. |
2 | A writeData method defines how property values will be written to the binary output. |
3 | A readData method defines how values can be retrieved from binary input. |
Make sure you read from input in the same order you as you wrote to the binary output. Detailed description of IdentifiedDataSerializable methods can be found «Serialization» [4] section of Hazelcast Documentation. |
class Person { constructor(firstName, lastName, age) { <i class="conum" data-value="1"></i><b>(1)</b> this.firstName = firstName; this.lastName = lastName; this.age = age; } getFactoryId() { return 42; } getClassId() { return 42; } writeData(dataOutput) { <i class="conum" data-value="2"></i><b>(2)</b> dataOutput.writeUTF(this.firstName); dataOutput.writeUTF(this.lastName); dataOutput.writeInt(this.age); } readData(dataInput) { <i class="conum" data-value="3"></i><b>(3)</b> this.firstName = dataInput.readUTF(); this.lastName = dataInput.readUTF(); this.age = dataInput.readInt(); } }
1 | JavaScript doesn’t have interfaces as Java. So, it’s just a JavaScript class. |
2 | Similarly to Java object, we need to implement writeData … |
3 | … and readData methods. |
DataSerializableFactory
in client config objectvar config = new Config.ClientConfig(); config.serializationConfig.dataSerializableFactories[42] = { create (type) { if (type === 42) { <i class="conum" data-value="1"></i><b>(1)</b> return new Person(); } } };
1 | Based on typeId , Hazelcast will figure out what object will be restored from the binary data. |
You can checkout Serialization Section and Node.js documentation about how to register custom serializers.
Predicates
Hazelcast IMap is an essentially key-value store. And usually, a developer uses the keys to retrieve data. But in certain cases, a developer doesn’t know a key. Or when a developer needs to find many entries satisfy a condition from a distributed Map. In this case, you needed to retrieve all entries from that map and filter them on the client side. But this method leads to the substantial amount of network communion. If you are looking for a small subset of the entries, it is more efficient to retrieve only the entries you are looking for using newly introduced predicates.
Let’s say you keep ages of people in a Hazelcast map.
map.putAll(['Alice', 34], ['Joe', 22], ['George', 27]);
You can quickly retrieve entries of people that are older than 25 with following code snippet
const Predicates = require('hazelcast-client').Predicates; map.entrySetWithPredicate(Predicates.greaterThan('this', 25)) .then((people) => { people.forEach(person => console.log(`Person: ${person[0]}, age: ${person[1]}`)); });
Above snippet will print names and ages of Alice
and George
.
If you only need their names but not ages,
map.keySetWithPredicate(Predicates.greaterThan('this', 25));
will return only names of Alice
and George
.
You can find a full list of available predicates at API docs.
MultiMap, Set, List
MultiMap is a particular version of a Map that supports multiple values associated with a single key.
let mmap = hazelcastClient.getMultiMap('restaurants'); <i class="conum" data-value="1"></i><b>(1)</b> mmap.put('New York', 'Red Lobster') <i class="conum" data-value="2"></i><b>(2)</b> .then(() => mmap.put('New York', 'Eataly')) .then(() => mmap.get('New York')) .then(list => console.log(list)); mmap.put('Las Vegas', 'Burgr') <i class="conum" data-value="3"></i><b>(3)</b> .then(() => mmap.put('Las Vegas', 'Alibi')) .then(() => mmap.put('Las Vegas', 'Pub & Grill')) .then(() => mmap.get('Las Vegas')) .then(list => console.log(list));
1 | In this example we have MultiMap of restaurants |
2 | Name of the city used as a key – New York or Las Vegas |
3 | When we need to get a collection of entries. Hazelcast MultiMap supports two types of values – Set (doesn’t allow duplicates, default) and List (preserves order). It can be configured using cluster config object [5]. |
[ 'Eataly', 'Red Lobster' ] [ 'Pub & Grill', 'Alibi', 'Burgr' ]
You can find more info here – MultiMap API.
Distributed Lock
If you need to synchronize your data access through the cluster, Hazelcast’s distributed lock implementation will come useful.
var globalLock = client.getLock('global-lock'); globalLock.lock(); // you can do some job here which doesn't allow shared access globalLock.unlock();
All supported lock operations are listed in ILock API
Messaging with Queue
Hazelcast distributed queue enables all cluster members and client to interact with it. Using Hazelcast distributed queue, you can add an item from one client and read it from another. FIFO ordering will apply to all queue operations across the cluster.
let logger = hazelcastClient.loggingService; let queue = hazelcastClient.getQueue('tasks'); // slow consumer setInterval(() => { queue.take().then(task => logger.info("Consumer", `executing task: ${task}`)); }, 1000);
let logger = hazelcastClient.loggingService; let queue = hazelcastClient.getQueue('tasks'); // fast producer setInterval(() => { var task = tasks[Math.floor(Math.random() * tasks.length)]; logger.info("Producer", `publishing task: ${task}`); queue.offer(task); }, 500);
In this example, Hazelcast’s uses a «buffer» to separate a fast producer from a slow consumer and this prevents consumer overloading.
Data Affinity
One of the things that we brought to v0.4.1
is ability increase locality of computations and data access on a cluster. Developers will be able to control on which partitions each key is stored. It is only a matter of adding a getPartitionKey()
function to user objects.
getPartitionKey
method'use strict'; let Client = require('hazelcast-client').Client; class Company { <i class="conum" data-value="1"></i><b>(1)</b> constructor(name, address) { this.name = name; this.address = address; } getName() { return this.name; } } class Associate { <i class="conum" data-value="2"></i><b>(2)</b> constructor(firstName, lastName, companyName) { this.firstName = firstName; this.lastName = lastName; this.companyName = companyName; } getCompanyName() { return this.companyName; } } class PartitionAwareKey { constructor(key, partitionKey) { this.key = key; this.partitionKey = partitionKey; } getPartititionKey() { <i class="conum" data-value="3"></i><b>(3)</b> return this.partitionKey; } } Client.newHazelcastClient().then((hazelcastClient) => { let companyMap = hazelcastClient.getMap('companyMap'); let associateMap = hazelcastClient.getMap('associateMap'); let partitionService = hazelcastClient.getPartitionService(); let company = new Company('IBM', 'Armonk, North Castle, NY'); let associate = new Associate('John', 'Smith', company.getName()); let key1 = new PartitionAwareKey('k1', company.getName()); <i class="conum" data-value="4"></i><b>(4)</b> let key2 = new PartitionAwareKey('a1', associate.getCompanyName()); <i class="conum" data-value="4"></i><b>(4)</b> console.log(partitionService.getPartitionId(key1)); <i class="conum" data-value="5"></i><b>(5)</b> console.log(partitionService.getPartitionId(key2)); <i class="conum" data-value="5"></i><b>(5)</b> companyMap.set(key1, company).then(() => associateMap.set(key2, associate)); <i class="conum" data-value="6"></i><b>(6)</b> });
1 | A class Company contains name and address of a company. |
2 | A class Associate contains info about company’s employee. |
3 | PartitionAwareKey (sort of a composite key) should have getPartitionId method that Hazelcast will use to collocate related data. |
4 | A companyName property used as partition id. |
5 | A partitionId for both keys will be the same… |
6 | …meaning «John Smith» and «IBM» will be co-located on the same partition. |
Having selected key partition explicitly, users can benefit from on the cluster processing of entries using EntryProcessor’s. Entry processor eliminates the cost of transferring entries between cluster and clients back and forth for simple transformations.
To learn more about Data Affinity in Hazelcast, check official documentation [6].
Sneak Peek of v0.5 and beyond
Even more Data Structures!
New release of Node.js client will introduce new data structures such as RingBuffer
and Topic
. These data structures are suitable for implementing pub-sub use cases. Together with Queue
, RB
and Topic
enable messaging capabilities for your application. Check Messaging with Queue section for peer-to-peer communication example.
Fresh Meat
Even though v0.5 is not released yet, you don’t have to wait for to try these new features. You can build the client locally [7].
master
branch.npm install git+https://[email protected]:hazelcast/hazelcast-nodejs-client.git
The feedback and pull requests are greatly appreciated.
Resources
As always, please, stay in touch. There a bunch of way to provide the feedback:
- Hazelcast Node.js Client repository on github.
- if you have found a bug, please report
- Chat with the developers
- Google Group
- Stackoverflow
sales at Hazelcast dot com
to learn more.