UNPKG

@stoplight/moleculer

Version:

Fast & powerful microservices framework for Node.JS

160 lines (133 loc) 3.51 kB
/* * moleculer * Copyright (c) 2019 MoleculerJS (https://github.com/moleculerjs/moleculer) * MIT Licensed */ "use strict"; const _ = require("lodash"); const BaseStrategy = require("./base"); const crypto = require("crypto"); const LRU = require("lru-cache"); const { isFunction } = require("../utils"); /** * Sharding invocation strategy * * Using consistent-hashing. More info: https://www.toptal.com/big-data/consistent-hashing * * @class ShardStrategy */ class ShardStrategy extends BaseStrategy { constructor(registry, broker, opts) { super(registry, broker, opts); this.opts = _.defaultsDeep(opts, { shardKey: null, vnodes: 10, ringSize: null, cacheSize: 1000 }); this.cache = new LRU({ max: this.opts.cacheSize, maxAge: null }); this.needRebuild = true; this.ring = []; broker.localBus.on("$node.**", () => (this.needRebuild = true)); } /** * Get key field value from Context. * * @param {Context} ctx * @returns {any} * @memberof ShardStrategy */ getKeyFromContext(ctx) { if (!this.opts.shardKey) return null; if (isFunction(this.opts.shardKey)) return this.opts.shardKey.call(this, ctx); if (this.opts.shardKey.startsWith("#")) return _.get(ctx.meta, this.opts.shardKey.slice(1)); return _.get(ctx.params, this.opts.shardKey); } /** * Select an endpoint by sharding. * * @param {Array<Endpoint>} list * @param {Context} ctx * @returns {Endpoint} * @memberof ShardStrategy */ select(list, ctx) { let key = this.getKeyFromContext(ctx); if (key != null) { if (this.needRebuild) this.rebuild(list); const nodeID = this.getNodeIDByKey(key); if (nodeID) return list.find(ep => ep.id == nodeID); } // Return a random item (no key) return list[_.random(0, list.length - 1)]; } /** * Get nodeID by a hashed numeric key. * * @param {Number} key * @returns {String} * @memberof ShardStrategy */ getNodeIDByKey(key) { if (this.cache) { const cached = this.cache.get(key); if (cached) return cached; } const hashNum = this.getHash(key.toString()); let found; const ringLen = this.ring.length; for (let i = 0; i < ringLen; i++) { if (hashNum <= this.ring[i].key) { found = this.ring[i]; break; } } if (found) { if (this.cache) this.cache.set(key, found.nodeID); return found.nodeID; } return null; } /** * Calculate 8 bit integer hash from string key based on MD5 hash. * * @param {String} key * @returns {Number} * @memberof ShardStrategy */ getHash(key) { const hash = crypto.createHash("md5").update(key).digest("hex"); const hashNum = parseInt(hash.substring(0, 8), 16); return this.opts.ringSize ? hashNum % this.opts.ringSize : hashNum; } /** * Rebuild the node hashring. * * @param {Array<Endpoint>} list * @memberof ShardStrategy */ rebuild(list) { this.cache.reset(); this.ring = []; const arr = list.map(ep => ep.id).sort(); const total = arr.length * this.opts.vnodes; const ringSize = this.opts.ringSize ? this.opts.ringSize : Math.pow(2, 32); const slice = ringSize / total; for (let j = 0; j < this.opts.vnodes; j++) { for (let i = 0; i < arr.length; i++) { const nodeID = arr[i]; this.ring.push({ key: Math.floor(slice * (this.ring.length + 1)), nodeID: nodeID }); } } // Set the latest value to the last slice. this.ring[this.ring.length - 1].key = ringSize; this.needRebuild = false; } } module.exports = ShardStrategy;