UNPKG

hamok

Version:

Lightweight Distributed Object Storage on RAFT consensus algorithm

224 lines (184 loc) 6.71 kB
import { createLogger } from '../common/logger'; import { HamokMessage } from './HamokMessage'; const logger = createLogger('PendingRequest'); export type PendingRequestConfig = { readonly requestId: string; timeoutInMs?: number; neededResponses?: number; remotePeers?: ReadonlySet<string>; storageId?: string, } export type PendingRequestState = 'pending' | 'resolved' | 'rejected'; export class PendingRequest { public readonly responses = new Map<string, HamokMessage>(); private _postponeTimeout = false; private _state: PendingRequestState = 'pending'; private _receivedResponses = 0; private _timer?: ReturnType<typeof setTimeout>; private _promise: Promise<HamokMessage[]>; private _resolve?: () => void; private _reject?: (reason: string) => void; // private _promise?: CompletablePromise<Message[]>; public constructor(public readonly config: PendingRequestConfig) { if (this.config.timeoutInMs) { if (this.config.timeoutInMs < 1) { throw new Error('Timeout for a pending promise if given must be greater than 0'); } const process = () => { if (this._timer === undefined) { return; } this._timer = undefined; if (!this._promise) { return; } if (this._postponeTimeout) { this._postponeTimeout = false; return (this._timer = setTimeout(process, this.config.timeoutInMs)); } if (this._reject) { this._reject(`Timeout. requestId: ${this.id}, number of received response: ${this._receivedResponses}`); } else { logger.warn(`No reject function is defined for pending promise ${this} but the timeout elpased.`); } }; this._timer = setTimeout(process, this.config.timeoutInMs); logger.trace('Pending Request %s is created with timeout %d', this, this.config.timeoutInMs); } this._promise = new Promise<HamokMessage[]>((resolve, reject) => { this._resolve = () => { if (this._timer) { clearTimeout(this._timer); this._timer = undefined; } const response = Array.from(this.responses.values()); logger.trace(`Pending request is resolved by responses ${response}`); /* eslint-disable @typescript-eslint/no-non-null-assertion */ resolve(response); this._resolve = undefined; this._reject = undefined; this._state = 'resolved'; }; this._reject = (reason: string) => { if (this._timer) { clearTimeout(this._timer); this._timer = undefined; } reject(reason); this._resolve = undefined; this._reject = undefined; this._state = 'rejected'; }; }); } public get id() { return this.config.requestId; } public get completed() { return this._state !== 'pending'; } public get state() { return this._state; } public get promise(): Promise<HamokMessage[]> { return this._promise; } // public then<TResult1 = HamokMessage[], TResult2 = never>(onfulfilled?: ((value: HamokMessage[]) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: string) => TResult2 | PromiseLike<TResult2>) | null): Promise<TResult1 | TResult2> { // return this._promise.then(onfulfilled, onrejected); // } // public catch<TResult = never>(onrejected?: ((reason: string) => TResult | PromiseLike<TResult>) | null): Promise<HamokMessage[] | TResult> { // return this._promise.catch(onrejected); // } // public finally(onfinally?: (() => void) | null): Promise<HamokMessage[]> { // return this._promise.finally(onfinally); // } // private _resolve(): void { // if (this._timer) { // clearTimeout(this._timer); // this._timer = undefined; // } // if (!this._promise) { // return logger.warn(`Attempted to resolve a not pending request (${this})`); // } // const response = Array.from(this._responses.values()); // logger.trace(`Pending request is resolved by responses ${response}`); // /* eslint-disable @typescript-eslint/no-non-null-assertion */ // this._promise!.resolve(response); // } public accept(message: HamokMessage): void { if (message.sourceId === undefined || message.requestId === undefined) { logger.warn('No source or request id is assigned for message:', message); return; } const prevResponse = this.responses.get(message.sourceId); if (prevResponse) { logger.warn(`Remote endpoint ${message.sourceId} overrided its previous response for request ${this.id}. removed response`, message); } this.responses.set(message.sourceId, message); this.refresh(); } /** * Explicitly reject the pending request if it is in the pending state * @param reason * @returns */ public reject(reason: string): void { if (this.completed || !this._reject) return logger.warn(`Attempted to reject a not pending request (${this})`); this._reject(reason); } /** * Explicitly resolve the pending request if it is in the pending state * @returns */ public resolve(): void { if (this.completed || !this._resolve) return logger.warn(`Attempted to resolve a not pending request (${this})`); if (!this.isReady) { logger.warn(`Resolving a pending request ${this} before it is ready`); } this._resolve(); } /** * Check if the pending request can be resolved or not * This is automatically called after any message accepted, but if the endpoints changed or * some other external event happened it can be called explicitly. * @returns */ public refresh(): void { if (this.completed || !this._resolve) return; if (!this.isReady) return; this._resolve(); } private get isReady() { let noMoreNeededResponse = true; if (this.config.neededResponses) { ++this._receivedResponses; noMoreNeededResponse = this.config.neededResponses <= this._receivedResponses; } let noMorePendingPeers = true; if (this.config.remotePeers) { let pendingPeerIds = this.config.remotePeers.size; for (const resolvedPeerId of this.responses.keys()) { if (this.config.remotePeers.has(resolvedPeerId)) { --pendingPeerIds; } else { logger.warn(`Remote peer ${resolvedPeerId} is not in the list of remote peers for request ${this}`); } } noMorePendingPeers = pendingPeerIds < 1; } return noMoreNeededResponse && noMorePendingPeers; } public postponeTimeout(): void { this._postponeTimeout = true; logger.debug(`Pending Request ${this} is postponed`); } public get [Symbol.toStringTag](): string { return [ `PendingRequest (${this.id})`, `neededResponses: ${this.config.neededResponses}`, `remotePeers: ${Array.from(this.config.remotePeers ?? []).join(', ')}`, `timeoutInMs: ${this.config.timeoutInMs}`, `state: ${this._state}`, ].join(', '); } }