mage-module-shard
Version:
Server-side sharding and broadcasting module for MAGE apps.
486 lines (485 loc) • 13.2 kB
TypeScript
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;
}