# cluster-client

Sharing Connection among Multi-Process Nodejs

[![NPM version][npm-image]][npm-url]
[![CI](https://github.com/node-modules/cluster-client/actions/workflows/nodejs.yml/badge.svg)](https://github.com/node-modules/cluster-client/actions/workflows/nodejs.yml)
[![Test coverage][codecov-image]][codecov-url]
[![Known Vulnerabilities][snyk-image]][snyk-url]
[![npm download][download-image]][download-url]

[npm-image]: https://img.shields.io/npm/v/cluster-client.svg?style=flat-square
[npm-url]: https://npmjs.org/package/cluster-client
[codecov-image]: https://codecov.io/gh/node-modules/cluster-client/branch/master/graph/badge.svg
[codecov-url]: https://codecov.io/gh/node-modules/cluster-client
[snyk-image]: https://snyk.io/test/npm/cluster-client/badge.svg?style=flat-square
[snyk-url]: https://snyk.io/test/npm/cluster-client
[download-image]: https://img.shields.io/npm/dm/cluster-client.svg?style=flat-square
[download-url]: https://npmjs.org/package/cluster-client

As we know, each Node.js process runs in a single thread. Usually, we split a single process into multiple processes to take advantage of multi-core systems. On the other hand, it brings more system overhead, sush as maintaining more TCP connections between servers.

This module is designed to share connections among multi-process Nodejs.

## Theory

- Inspired by [Leader/Follower pattern](http://www.cs.wustl.edu/~schmidt/PDF/lf.pdf).
- Allow ONLY one process "the Leader" to communicate with server. Other processes "the Followers" act as "Proxy" client, and forward all requests to Leader.
- The Leader is selected by "Port Competition". Every process try to listen on a certain port (for example 7777), but ONLY one can occupy the port, then it becomes the Leader, the others become Followers.
- TCP socket connections are maintained between Leader and Followers. And I design a simple communication protocol to exchange data between them.
- If old Leader dies, one of processes will be selected as the new Leader.

## Diagram

normal (without using cluster client)

```js
+--------+   +--------+
| Client |   | Client |   ...
+--------+   +--------+
    |  \     /   |
    |    \ /     |
    |    / \     |
    |  /     \   |
+--------+   +--------+
| Server |   | Server |   ...
+--------+   +--------+

```

using cluster-client

```js
             +-------+
             | start |
             +---+---+
                 |
        +--------+---------+
      __| port competition |__
win /   +------------------+  \ lose
   /                           \
+--------+     tcp conn     +----------+
| Leader |<---------------->| Follower |
+--------+                  +----------+
    |
+--------+
| Client |
+--------+
    |  \
    |    \
    |      \
    |        \
+--------+   +--------+
| Server |   | Server |   ...
+--------+   +--------+

```

## Protocol

- Packet structure

```js
 0       1       2               4                                                              12
 +-------+-------+---------------+---------------------------------------------------------------+
 |version|req/res|    reserved   |                          request id                           |
 +-------------------------------+-------------------------------+-------------------------------+
 |           timeout             |   connection object length    |   application object length   |
 +-------------------------------+---------------------------------------------------------------+
 |         conn object (JSON format)  ...                    |            app object             |
 +-----------------------------------------------------------+                                   |
 |                                          ...                                                  |
 +-----------------------------------------------------------------------------------------------+
```

- Protocol Type
  - Register Channel
  - Subscribe/Publish
  - Invoke
- Sequence diagram

```js
 +----------+             +---------------+          +---------+
 | Follower |             |  local server |          |  Leader |
 +----------+             +---------------+          +---------+
      |     register channel     |       assign to        |
      + -----------------------> |  --------------------> |
      |                          |                        |
      |                                subscribe          |
      + ------------------------------------------------> |
      |       subscribe result                            |
      | <------------------------------------------------ +
      |                                                   |
      |                                 invoke            |
      + ------------------------------------------------> |
      |          invoke result                            |
      | <------------------------------------------------ +
      |                                                   |
```

## Install

```bash
npm install cluster-client --save
```

Node.js >= 6.0.0 required

## Usage

```js
'use strict';

const co = require('co');
const Base = require('sdk-base');
const cluster = require('cluster-client');

/**
 * Client Example
 */
class YourClient extends Base {
  constructor(options) {
    super(options);

    this.options = options;
    this.ready(true);
  }

  subscribe(reg, listener) {
    // subscribe logic
  }

  publish(reg) {
    // publish logic
  }

  * getData(id) {
    // invoke api
  }

  getDataCallback(id, cb) {
    // ...
  }

  getDataPromise(id) {
    // ...
  }
}

// create some client instances, but only one instance will connect to server
const client_1 = cluster(YourClient)
  .delegate('getData')
  .delegate('getDataCallback')
  .delegate('getDataPromise')
  .create({ foo: 'bar' });
const client_2 = cluster(YourClient)
  .delegate('getData')
  .delegate('getDataCallback')
  .delegate('getDataPromise')
  .create({ foo: 'bar' });
const client_3 = cluster(YourClient)
  .delegate('getData')
  .delegate('getDataCallback')
  .delegate('getDataPromise')
  .create({ foo: 'bar' });

// subscribe information
client_1.subscribe('some thing', result => console.log(result));
client_2.subscribe('some thing', result => console.log(result));
client_3.subscribe('some thing', result => console.log(result));

// publish data
client_2.publish('some data');

// invoke method
client_3.getDataCallback('some thing', (err, val) => console.log(val));
client_2.getDataPromise('some thing').then(val => console.log(val));

co(function*() {
  const ret = yield client_1.getData('some thing');
  console.log(ret);
}).catch(err => console.error(err));
```

## API

- `delegate(from, to)`:
  create delegate method, `from` is the method name your want to create, and `to` have 6 possible values: [ `subscribe`, `unSubscribe`, `publish`, `invoke`, `invokeOneway`, `close` ],  and the default value is invoke
- `override(name, value)`:
  override one property
- `create(…)`
  create the client instance
- `close(client)`
  close the client
- `APIClientBase`  a base class to help you create your api client

## Best Practice

1. DataClient

- Only provider data API, interact with server and maintain persistent connections etc.
- No need to concern `cluster` issue

1. APIClient

- Using `cluster-client` to wrap DataClient
- Put your bussiness logic here

### DataClient

```js
const Base = require('sdk-base');

class DataClient extends Base {
  constructor(options) {
    super(options);
    this.ready(true);
  }

  subscribe(info, listener) {
    // subscribe data from server
  }

  publish(info) {
    // publish data to server
  }

  * getData(id) {
    // asynchronous API
  }
}
```

### APIClient

```js
const DataClient = require('./your-data-client');
const { APIClientBase } = require('cluster-client');

class APIClient extends APIClientBase {
  constructor(options) {
    super(options);
    this._cache = new Map();
  }
  get DataClient() {
    return DataClient;
  }
  get delegates() {
    return {
      getData: 'invoke',
    };
  }
  get clusterOptions() {
    return {
      name: 'MyClient',
    };
  }
  subscribe(...args) {
    return this._client.subscribe(...args);
  }
  publish(...args) {
    return this._client.publish(...args);
  }
  * getData(id) {
    // write your business logic & use data client API
    if (this._cache.has(id)) {
      return this._cache.get(id);
    }
    const data = yield this._client.getData(id);
    this._cache.set(id, data);
    return datal
  }
}
```

```js
|------------------------------------------------|
| APIClient                                      |
|       |----------------------------------------|
|       | ClusterClient                          |
|       |      |---------------------------------|
|       |      | DataClient                      |
|-------|------|---------------------------------|
```

For more information, you can refer to the [discussion](https://github.com/eggjs/egg/issues/322)

[MIT](LICENSE)

<!-- GITCONTRIBUTOR_START -->

## Contributors

|[<img src="https://avatars.githubusercontent.com/u/1207064?v=4" width="100px;"/><br/><sub><b>gxcsoccer</b></sub>](https://github.com/gxcsoccer)<br/>|[<img src="https://avatars.githubusercontent.com/u/156269?v=4" width="100px;"/><br/><sub><b>fengmk2</b></sub>](https://github.com/fengmk2)<br/>|[<img src="https://avatars.githubusercontent.com/u/456108?v=4" width="100px;"/><br/><sub><b>shaoshuai0102</b></sub>](https://github.com/shaoshuai0102)<br/>|[<img src="https://avatars.githubusercontent.com/u/6897780?v=4" width="100px;"/><br/><sub><b>killagu</b></sub>](https://github.com/killagu)<br/>|[<img src="https://avatars.githubusercontent.com/u/32174276?v=4" width="100px;"/><br/><sub><b>semantic-release-bot</b></sub>](https://github.com/semantic-release-bot)<br/>|[<img src="https://avatars.githubusercontent.com/u/227713?v=4" width="100px;"/><br/><sub><b>atian25</b></sub>](https://github.com/atian25)<br/>|
| :---: | :---: | :---: | :---: | :---: | :---: |
[<img src="https://avatars.githubusercontent.com/u/546535?v=4" width="100px;"/><br/><sub><b>leoner</b></sub>](https://github.com/leoner)<br/>|[<img src="https://avatars.githubusercontent.com/u/2160731?v=4" width="100px;"/><br/><sub><b>mansonchor</b></sub>](https://github.com/mansonchor)<br/>|[<img src="https://avatars.githubusercontent.com/u/19849579?v=4" width="100px;"/><br/><sub><b>sinkhaha</b></sub>](https://github.com/sinkhaha)<br/>|[<img src="https://avatars.githubusercontent.com/u/6457450?v=4" width="100px;"/><br/><sub><b>limitMe</b></sub>](https://github.com/limitMe)<br/>

This project follows the git-contributor [spec](https://github.com/xudafeng/git-contributor), auto updated at `Tue Jun 20 2023 12:29:14 GMT+0800`.

<!-- GITCONTRIBUTOR_END -->
