UNPKG

slavery-js

Version:

A simple clustering app that allows you to scale an application on multiple thread, containers or machines

295 lines (218 loc) 13 kB
# slavery-js A clustering library for Node.js that lets you scale an application across multiple processes, threads, or machines. Services and worker nodes communicate over Socket.IO, so you can distribute work horizontally and call between services over the network. ## What it does - **Multiple services**: Define several named services (e.g. `waiter`, `logger`, `api`). Each service has one master process and optionally many worker nodes. - **Peer discovery**: When using the high-level entry API, a discovery server runs so services find each other by name without hardcoding hosts/ports. - **Worker pool**: A service can expose *slave methods*. The master sends work to idle workers; you can fix the number of workers or enable auto-scaling. - **Service-to-service calls**: From a master you get clients for other services and call their methods (e.g. `await waiter.wait(2)`). Calls are request/response over the network. - **Stash**: A shared key-value store per service, readable and writable from the master and from workers (for sharing state or config). ## Installation ```bash npm install slavery-js ``` ## Quick start (entry API) The entry API starts a **peer discovery** server and gives you a proxy. Every method you call on the proxy defines a **service** with that name. Services discover each other automatically. ```javascript import slavery from 'slavery-js'; slavery({ host: 'localhost', port: 3000, }) .waiter({ wait: async (seconds) => { await new Promise((r) => setTimeout(r, seconds * 1000)); return seconds; }, }) .logger({ log: async (msg) => console.log(msg), }) .main(async ({ self, waiter, logger }) => { const result = await waiter.wait(2); await logger.log(`waited ${result} seconds`); await waiter.exit(); await logger.exit(); await self.exit(); }); ``` - `**.waiter(...)**` and `**.logger(...)**` define services that only have worker methods (no master logic). You pass an object of named functions; each name becomes a callable method from other services. - `**.main(async (context) => { ... })**` defines a service whose **master** runs the given function. The context object contains: - **Peer service clients** by name: `waiter`, `logger`, etc. Call their methods (e.g. `waiter.wait(2)`). - `**self`**: the current service — use `self.exit()` to shut it down. - `**slaves**`: present only if this service has worker methods; it’s the node manager (see [Direct Service API](#direct-service-api)). - `**master**`: the service instance (advanced). Each call returns the same proxy so you can chain. Order of definition does not determine startup order; services run as soon as they are created. ## Entry API reference ```javascript slavery(options) .serviceName(masterCallback, slaveMethods?, serviceOptions?) ``` - `**options**`: `{ host?, port? }` — used for peer discovery and as defaults for services. Defaults: `host: 'localhost'`, `port: 3000`. - `**serviceName**`: Any method name **except** reserved: `slave`, `slaves`, `master`, `self` (see [Prohibited names](#prohibited-names)). - `**masterCallback`**: `(context) => void | Promise<void>`. Required. If you only want workers, pass a no-op: `() => {}`. - `**slaveMethods**`: Optional object of functions. Each key is a method name callable by other services and by the master (via the service client). Signature: `(params, { slave, self }) => result`. See [Slave methods](#slave-methods). - `**serviceOptions**`: Optional. Same as [Service options](#service-options) (e.g. `number_of_nodes`, `port`, `host` for this service). Forms: - `.name(masterCallback)` — master only. - `.name(masterCallback, serviceOptions)` — master only, with options. - `.name(masterCallback, slaveMethods)` — master + workers. - `.name(masterCallback, slaveMethods, serviceOptions)` — master + workers + options. - `.name(slaveMethods)` — workers only (master is a no-op). - `.name(slaveMethods, serviceOptions)` — workers only + options. ## Direct Service API For full control (fixed addresses, no proxy), use the **Service** class and **peer discovery** or explicit peer addresses. ### Using peer discovery Services register with a discovery server and get the list of other services dynamically: ```javascript import slavery, { Service } from 'slavery-js'; // Start peer discovery (e.g. in one process or reuse slavery() entry) // If you use slavery({ host, port }), discovery is already running at that host:port. const service = new Service({ service_name: 'myService', peerDiscoveryAddress: { host: 'localhost', port: 3000 }, mastercallback: async (context) => { const { self, otherService } = context; const result = await otherService.someMethod(42); await otherService.exit(); await self.exit(); }, slaveMethods: { someMethod: async (n, { slave }) => n + 1, }, options: { host: 'localhost' }, // port can be 0 to auto-assign }); service.start(); ``` ### Using fixed peer addresses Skip discovery and list peers explicitly: ```javascript import { Service } from 'slavery-js'; const service = new Service({ service_name: 'worker', peerServicesAddresses: [ { name: 'api', host: 'localhost', port: 3001 }, ], mastercallback: async ({ api, self }) => { const data = await api.fetchData(); await self.exit(); }, slaveMethods: { doWork: async (payload, { slave }) => { /* ... */ return result; }, }, options: { host: 'localhost', port: 3002 }, }); service.start(); ``` ### Master callback context The first argument to `mastercallback` is an object: - **Peer service clients** (e.g. `api`, `waiter`): one per entry in `peerServicesAddresses` or per service known via peer discovery. Call their methods; they return promises. - `**slaves`**: the **NodeManager** for this service (only if `slaveMethods` is non-empty). Use it to get an idle worker and run a method: `const worker = await slaves.getIdle(); await worker.run('methodName', params)`. - `**self`**: the current **Service** instance. Use `self.exit()` to shut down, `self.set(key, value)` / `self.get(key)` for the service stash. - `**master`**: same as the service instance (alias). ### Slave methods `slaveMethods` is an object of functions. Each is invoked on a worker when the master (or another service) calls that method on the service client. Signature: ```javascript async (params, { other_services, self }) => result ``` - `**params**`: value passed by the caller (e.g. `await client.wait(5)` → `params === 5`). - `**slave**`: the worker node object. Has `id`, and you can attach state: `slave['myKey'] = value` for the lifetime of that worker. - `**self**`: the node’s stash interface: `self.get(key)`, `self.set(key, value)` (or `getStash`/`setStash`). Data is stored on the master’s stash and is shared across workers. Values must be JSON-serializable. Special method names: - `**_startup**`: run once when the worker starts (before it accepts work). - `**_cleanup**`: run when the worker is shutting down. Example: ```javascript slaveMethods: { _startup: async (_, { slave }) => { slave.cache = new Map(); }, process: async (item, { slave, self }) => { const config = await self.get('config'); return processItem(item, slave.cache, config); }, _cleanup: async (_, { slave }) => { slave.cache = null; }, } ``` ### Calling other services From the master you get a client per peer service. The client exposes: - **Slave methods** you defined (e.g. `wait`, `process`). - **Built-in methods** (see below). Examples: ```javascript const result = await awaiter.wait(2); await awaiter._add_node(3); const count = await awaiter._get_nodes_count(); await awaiter.exit(); ``` ### Selecting specific workers To target one or more workers instead of any idle one: ```javascript await serviceClient._number_of_nodes_connected(3); const oneNode = await serviceClient.select(1); // one worker const threeNodes = await serviceClient.select(3); // three workers const allNodes = await serviceClient.select('all'); const id = await oneNode.getId(); // if you expose getId in slaveMethods const result = await oneNode.myMethod(arg); ``` `select(n)` returns a **new** client that sends subsequent calls to the selected node(s). If multiple nodes are selected, methods return an array of results. ### Built-in service methods Every service client supports these (they are implemented by the framework): | Method | Description | | ------------------------------- | ---------------------------------------------------------- | | `_get_nodes_count` | Number of worker nodes. | | `_get_nodes` | List of nodes with `id` and `status`. | | `_get_idle_nodes` | Idle node list. | | `_get_busy_nodes` | Busy node list. | | `_number_of_nodes_connected(n)` | Resolves when at least `n` nodes are connected. | | `_select(n | 'all')` | Used internally by `.select()`. | | `_add_node(n?)` | Add one or `n` workers. | | `_kill_node(id?)` | Kill one worker (by id), or one arbitrary worker if no id. | | `_queue_size` | Current request queue size. | | `_turn_over_ratio` | Queue turnover metric. | | `exit()` | Shut down the service. | | `_exec(codeString)` | Run a code string on a worker (advanced). | | `exec_master(codeString)` | Run a code string on the **master** (advanced). | ### Stash (shared state) - **Master**: `self.set(key, value)` and `self.get(key)` on the service instance. - **Workers**: inside slave methods, `self.get(key)` and `self.set(key, value)` (or `getStash`/`setStash`) use the same stash. Values must be JSON-serializable. Use the stash for config, caches, or coordination between master and workers. ### Service options Pass as the last argument to the entry proxy or in `options` when creating a `Service`: | Option | Description | | -------------------------------------------- | ---------------------------------------------------------------- | | `host` | Bind / discovery host. | | `port` | Bind / discovery port (use `0` to auto-assign). | | `nm_host`, `nm_port` | Node manager bind address (defaults from `host`). | | `number_of_nodes` | Fixed number of workers. If set, auto-scaling is off by default. | | `auto_scale` | If `true`, scale workers by queue size and idle count. | | `max_number_of_nodes`, `min_number_of_nodes` | Bounds when auto-scaling. | | `timeout` | Request timeout (ms). | | `onError` | `'throw' \| 'log' \| 'ignore'` — how to handle worker errors. | ### Prohibited names **Service names** (proxy method names) you cannot use: - `slave`, `slaves`, `master`, `self` **Slave method names** you cannot use (reserved by the framework): - `all`, `select`, `selectOne`, `one`, `connect`, `disconnect`, `reconnect`, `exit` - `_run`, `_name`, `_id`, `_listeners`, `_set_listeners`, `_set_services`, `_ping`, `_pong`, `_exit` - `_connect_service`, `_is_idle`, `_is_busy`, `_is_error`, `_has_done`, `_set_status` - `_get_nodes_count`, `_get_nodes`, `_get_idle_nodes`, `_get_busy_nodes`, `_select_node`, `_select_nodes`, `_add_node`, `_kill_node`, `_queue_size`, `_turn_over_ratio` See `docs/prohibited_varible_names.txt` for the full list. ## Imports ```javascript // ESM import slavery from 'slavery-js'; import { Service, PeerDiscoverer } from 'slavery-js'; // CommonJS const { default: slavery, Service, PeerDiscoverer } = require('slavery-js'); ``` - `**PeerDiscoverer**`: default export of `src/app/peerDiscovery` is the **PeerDiscoveryServer**. If you use the entry `slavery({ host, port })`, it starts this server for you. You can also start it manually and then use `Service` with `peerDiscoveryAddress`. ## Running tests ```bash npm test ``` Tests are in `test/`, e.g. `test/app/entry.test.ts`, `test/service/service_request.test.ts`, `test/slaves/slaves.test.ts`. They start multiple services and assert on cross-service calls, scaling, stash, and peer discovery. ## License ISC