UNPKG

mage-module-shard

Version:

Server-side sharding and broadcasting module for MAGE apps.

486 lines (485 loc) 13.2 kB
import * as mage from 'mage'; /** * Shard module attribute mapping */ export declare type ShardAttribute<T> = T extends (...args: infer P) => Promise<infer R> ? (...args: P) => Promise<R> : T extends (...args: infer P) => infer R ? (...args: P) => Promise<R> : Promise<T>; /** * Shard proxy type returned with module.createShard() */ export declare type Shard<T extends AbstractShardedModule> = IShard & { readonly [A in keyof T]: ShardAttribute<T[A]>; }; /** * Responses for broadcast */ export interface IHashMap<T> { [key: string]: T; } /** * Broadcast module attribute mapping */ export declare type BroadcastAttribute<T> = T extends (...args: infer P) => Promise<infer S> ? (...args: P) => Promise<[IHashMap<Error>, IHashMap<S>]> : T extends (...args: infer P) => infer R ? (...args: P) => Promise<[IHashMap<Error>, IHashMap<R>]> : Promise<[IHashMap<Error>, IHashMap<T>]>; /** * Broadcast proxy type returned with module.createBroadcast() */ export declare type Broadcast<T extends AbstractShardedModule> = { readonly [A in keyof T]: BroadcastAttribute<T[A]>; }; /** * IShardedRequestMeta * * IShardedRequestMeta are used as keys to access ShardedRequest * instances stored in a map; they contain the information required to identify * the related request, and a timestamp used to determine whether a request * has timed out or not. * * @export * @interface IShardedRequestMeta */ export interface IShardedRequestMeta { id: string; timestamp: number; } /** * RemoteError class * * RemoteErrors encapsulate an error that occured * either while sending a ShardedRequest, or * while executing the request on the remote MAGE node. * * @export * @class RemoteError */ export declare class RemoteError extends Error { constructor(data: any); } /** * ShardedRequests * * @export * @class ShardedRequest */ export declare class ShardedRequest { /** * Request ID * * This ID is used by the remote server when sending back a response, * and by the local server to route the response back to the correct ShardedRequest * instance. * * @type {string} * @memberof ShardedRequest */ id: string; /** * The mmrpNode instance to use to send the message * * @type {any} * @memberof ShardedRequest */ mmrpNode: any; /** * The event name * * Should normally be `AbstractShardedModule.REQUEST_EVENT_NAME` * for the class which will instanciate it * * @type {string} * @memberof ShardedRequest */ eventName: string; /** * Target MAGE node * * @type {string[]} * @memberof ShardedRequest */ target: string[]; /** * Method to be executed * * @type {string} * @memberof ShardedRequest */ method: string; /** * Arguments to feed to the method * * @type {any[]} * @memberof ShardedRequest */ args?: any[]; /** * Resolve function (see `send`) * * @memberof ShardedRequest */ resolve: (data: any) => void; /** * Reject function (see `send`) * * @memberof ShardedRequest */ reject: (error: Error) => void; constructor(id: string, mmrpNode: any, eventName: string, target: string[], method: string, args?: any[]); /** * Send the request * * Note that the sending module is responsible for listening for the * response, and passing it to this request through `resolve` or * `reject`. * * The sending module may also call `reject` should it judge a request * has timed out. * * @returns * @memberof ShardedRequest */ send(): Promise<{}>; } /** * IShard interface * * This represents the data structure of * a shard. * * @export */ export interface IShard { id: string; } /** * AbstractShardedModule * * This abstract module will take care of maintaining * a consistent view of the cluster, and of creating * proxy objects (or shards) which can then be used to * consistently forward requests to a server. * * See Readme.md for more details on how to use this class. * * @export * @class AbstractShardedModule */ export default abstract class AbstractShardedModule { /** * Service name * * By default, the service name will be the name of the class * * @type {string} * @memberof AbstractShardedModule */ name: string; /** * Hashing algorithm to for sharding */ private hashingAlgorithm; /** * Module logger instance */ private logger; /** * Request event label * * Request messages follow the following format: * * [requestId, method, ...args] * * @type {string} * @memberof AbstractShardedModule */ private REQUEST_EVENT_NAME; /** * Response event label * * Response messages follow the following format: * * [requestId, response, error] * * @type {string} * @memberof AbstractShardedModule */ private RESPONSE_EVENT_NAME; /** * Service instance * * @type {mage.core.IService} * @memberof AbstractShardedModule */ private service; /** * Hashes of available workers in the clusters * * A given node in the cluster may be running more than * one worker; each worker will have its own accessible address. * * @type {string[]} * @memberof AbstractShardedModule */ private addressHashes; /** * The address hash for this local server * * This is used for two purposes: * * 1. Call directly methods locally if the shard points to the * local server * * 2. Allow for the creation of a shard reference that points to the * local server * * @type {string[]} * @memberof AbstractShardedModule */ private localNodeHash; /** * Hash to MMRP address key-value map * * @type {{ [hash: string]: string[] }} * @memberof AbstractShardedModule */ private clusterAddressMap; /** * Number of nodes in the cluster * * @type {number} * @memberof AbstractShardedModule */ private clusterSize; /** * Pendng requests that are being executed on a remote MAGE node * * When requests are forwarded to a remote MAGE node, a reference to * the original request will be placed here; upon reception of * the response (an error or a value), the code execution * will then continue locally. * * Requests will timeout after a certain amount of time * * @type {Map<IShardedRequestMeta, ShardedRequest>} * @memberof AbstractShardedModule */ private pendingRequests; /** * Key-value for fetching the pendingRequests * map key * * This is used to allow quick by-reference access to * pending requests stored in this.pendingRequests * * ```typescript * const key = this.pendingRequestsKeyMap['some-id'] * const request = this.pendingRequests(key) * ``` * * @memberof AbstractShardedModule */ private pendingRequestsKeyMap; /** * Garbage collection timer reference for dealing with stalled requests * * Should requests not be replied to within a certain amount of time, * an error will be returned instead. * * @private * @type {number} * @memberof AbstractShardedModule */ private gcTimer; /** * Creates an instance of AbstractShardedModul * * @param {string} [name] * @memberof AbstractShardedModule */ constructor(name?: string, hashingAlgorithm?: string, gcTimeoutTime?: number); /** * Setup method called by MAGE during initialization * * @param {mage.core.IState} _state * @param {(error?: Error) => void} callback * @memberof AbstractShardedModule */ setup(_state: mage.core.IState, callback: (error?: Error) => void): Promise<void>; /** * Teardown method called by MAGE during shutdown * * @param {mage.core.IState} _state * @param {(error?: Error) => void} callback * @memberof AbstractShardedModule */ teardown(_state: mage.core.IState, callback: (error?: Error) => void): void; /** * Retrieve a shard using a deserialized shard instance (IShard) * * @param {IShard} shard */ getShard(shard: IShard): Shard<this>; /** * Retrieve a shard using a deserialized shard instance (IShard) * * */ createBroadcast(): Broadcast<this>; /** * Retrieve the local shard value * * Useful when you wish to make it so that future requests * be routed to the current local server. * * Note that this does NOT return a proxy; this returns just the forwardable * data of a proxy. */ getLocalShard(): { id: string; }; /** * Create a new shard from a shard key * * You will want to call this to receive the shard * reference. Then, for subsequent related calls, * you will want to make sure to use the returned * reference to forward your requests. * * @param {string} shardKey */ createShard(shardKey: string): Shard<this>; /** * * @param {string} shardKey */ protected getShardId(shardKey: string): { id: string; }; /** * Hash a string using the configured algorithm * * Inspired by https://github.com/3rd-Eden/node-hashring/blob/master/index.js#L13 * * @param str */ private hash; private getMmrpNode; private getServiceDiscovery; /** * Make up a fake port * * The current serviceDiscovery will consider an announced service to * be the same if the hostnames and ports are the same; this is an issue in * our case, since each workers on a single server will be announced, * and one might want to run more than one MAGE instance on a single server. * * To palliate to this issue, we pretend the PID is a port. Since PID's, depending * on the subsystem, can possibly be very large (Linux's default is 32768, but is * configurable through /proc/sys/kernel/pid_max), we use a modulo operator to limit * how big the port can be. * * 2017/07 (stelcheck): This is a huge hack, and I am aware this may break under specific * circumstances; namely, if other external services are announced on the same host, * on the same port. If you believe you are hitting this case, please let me know. */ private getPseudoPort; /** * Retrieve the current classes's name * * @returns {string} * @memberof AbstractShardedModule */ private getClassName; /** * Schedule garbage collection * * Note that this will cancel any previously scheduled GC, the timeout * time value passed as an argument will be used for all future GC schedulings * * @private * @param {number} gcTimeoutTime * @memberof AbstractShardedModule */ private scheduleGarbageCollection; /** * * * @private * @param {mage.core.IServiceNode} node * @memberof AbstractShardedModule */ private registerNodeAddress; /** * * * @private * @param {mage.core.IServiceNode} node * @memberof AbstractShardedModule */ private unregisterNodeAddress; /** * * * @private * @param {MmrpEnvelopeMessage[]} messages * @returns * @memberof AbstractShardedModule */ private onRequest; /** * Process a response * * @private * @param {MmrpEnvelopeMessage[]} messages * @returns * @memberof AbstractShardedModule */ private onResponse; /** * Create a request, and add it to our list of pending requests * * The request is returned so that the calling code may * call the request's `send` method and `await` a response. * * @private * @param {string[]} target * @param {string} method * @param {any[]} args * @returns * @memberof AbstractShardedModule */ private addPendingRequest; /** * Retrieve a pending response by request ID * * @private * @param {string} id * @returns * @memberof AbstractShardedModule */ private getPendingRequest; /** * Delete a pending response * * This is normally called once a request has been completed, * or when a request has timed out. * * @private * @param {string} id * @memberof AbstractShardedModule */ private deletePendingRequest; /** * Retrieve a request, by ID, then delete it from the * list of pending requests * * @private * @param {string} id * @returns * @memberof AbstractShardedModule */ private getAndDeletePendingRequest; /** * Ensure that we know this cluster ID * * @param id */ private assertClusterId; }