What’s New in Hazelcast Node.js Client

Facebooktwittergoogle_plusredditlinkedin
node

TL;DR

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!

If you haven’t read previous Getting Started blog post, it’s a perfect opportunity to press Pause button now. That “Getting started” post contains all required information to get the Hazelcast Node.js Client gears moving.

What’s new

The code example in this article were tested under 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].

Let’s have a quick look at this first because:

I don’t know how to put this, but it’s kind of a big deal.

— Ron Burgundy
Anchorman: The Legend of Ron Burgundy

ron

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.

Person.java
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.
Let’s look what a JavaScript counterpart object looks like.
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.
Last step – register DataSerializableFactory in client config object
var 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.

The restaurants MultiMap
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].
Output looks like follows
[ '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.

Client 1 – Consumer of tasks
let logger = hazelcastClient.loggingService;
let queue = hazelcastClient.getQueue('tasks');

// slow consumer
setInterval(() => {
    queue.take().then(task => logger.info("Consumer", `executing task: ${task}`));
}, 1000);

Client 2 – Producer of tasks
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.

A developer needs to implement 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

wonka

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].

Or, thanks to NPM, install Hazelcast Client from master branch.
npm install git+https://git@github.com: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:


8. It is a community chat / forum but not a support portal. We can help with answering the questions, and provide pointers but we’re not going to write code for you. We are encouraging people in the community to share the knowledge, please, don’t abuse it. If you’re interested in 24/7 support, we have a dedicated support portal available on commercial terms. Contact sales at Hazelcast dot com to learn more.

Related:

Document Mini Preview

Distributed Caching for Your Next Node.js Project

Webinar

In this presentation you will learn how to leverage Hazelcast IMDG, the leading open source, distributed, elastic, in-memory platform, as a cache for your Node.js applications.

Watch Now