reliable-zeromq
Version:
A collection of reliable zeromq messaging constructs
280 lines (237 loc) • 7.67 kB
text/typescript
import { Queue } from "typescript-collections";
import uniqid from "uniqid";
import * as zmq from "zeromq";
import Config from "./Config";
import {
DEFAULT_ZMQ_REQUEST_ERROR_HANDLERS,
TRequestHwmWarning,
TZMQRequestErrorHandlers,
} from "./Errors";
import { CancellableDelay } from "./Utils/Delay";
import { RESPONSE_CACHE_EXPIRED } from "./ZMQResponse";
export type TRequestBody = [requesterId: string, nonce: string, message: string];
type TRequestResolver = (aResult: TRequestResponse) => void;
type TZmqSendRequest =
{
Request: TRequestBody;
Resolve: () => void;
};
export enum ERequestBody
{
RequesterId,
Nonce,
Message,
}
export enum ERequestResponse
{
SUCCESS = "SUCCESS",
TIMEOUT = "TIMEOUT",
CACHE_ERROR = "CACHE_ERROR",
}
export type TSuccessfulRequest =
{
ResponseType: ERequestResponse.SUCCESS;
Response: string;
};
export type TRequestTimeOut =
{
ResponseType: ERequestResponse.TIMEOUT;
MessageNonce: number;
RequestBody: TRequestBody;
};
export type TResponseCacheError =
{
ResponseType: ERequestResponse.CACHE_ERROR;
Endpoint: string;
MessageNonce: number;
};
export type TRequestResponse = TSuccessfulRequest | TRequestTimeOut | TResponseCacheError;
export class ZMQRequest
{
private readonly mCancellableDelay: CancellableDelay = new CancellableDelay();
private mDealer!: zmq.Dealer;
private readonly mEndpoint: string;
private readonly mErrorHandlers: TZMQRequestErrorHandlers;
private readonly mOurUniqueId: string;
private readonly mPendingRequests: Map<number, TRequestResolver> = new Map();
private mRequestNonce: number = -1;
private readonly mRoundTripMax: number;
private mSafeToSend: boolean = true;
private readonly mSendQueue: Queue<TZmqSendRequest> = new Queue();
public constructor(aReceiverEndpoint: string, aErrorHandlers?: TZMQRequestErrorHandlers)
{
this.mRoundTripMax = Config.MaximumLatency * 2; // Send + response latency
this.mEndpoint = aReceiverEndpoint;
this.mErrorHandlers = aErrorHandlers ?? DEFAULT_ZMQ_REQUEST_ERROR_HANDLERS;
this.mOurUniqueId = uniqid();
this.Open();
}
private get ResponseTimeout(): number
{
return Config.HeartBeatInterval;
}
public get Endpoint(): string
{
return this.mEndpoint;
}
private AssertRequestProcessed(aRequestId: number, aRequest: TRequestBody): void
{
const lResolver: TRequestResolver | undefined = this.mPendingRequests.get(aRequestId);
if (lResolver)
{
lResolver(
{
ResponseType: ERequestResponse.TIMEOUT,
MessageNonce: aRequestId,
RequestBody: aRequest,
},
);
}
}
private GenerateRequestResult(aMessage: string, aNonce: number): TRequestResponse
{
let lRequestResult: TRequestResponse;
if (this.IsErrorMessage(aMessage))
{
lRequestResult =
{
ResponseType: ERequestResponse.CACHE_ERROR,
Endpoint: this.mEndpoint,
MessageNonce: Number(aNonce),
};
}
else
{
lRequestResult =
{
ResponseType: ERequestResponse.SUCCESS,
Response: aMessage.toString(),
};
}
return lRequestResult;
}
private HandleZMQSendError(aError: any, aRequest: TRequestBody): void
{
if (aError && aError.code && aError.code === "EAGAIN")
{
const lHighWaterMarkWarning: TRequestHwmWarning =
{
Requester: aRequest[ERequestBody.RequesterId],
Nonce: Number(aRequest[ERequestBody.Nonce]),
Message: aRequest[ERequestBody.Message],
};
this.mErrorHandlers.HighWaterMarkWarning(lHighWaterMarkWarning);
}
else
{
throw aError;
}
}
private IsErrorMessage(aMessage: string): boolean
{
return aMessage === RESPONSE_CACHE_EXPIRED;
}
private async ManageRequest(aRequestId: number, aRequest: TRequestBody): Promise<void>
{
const lMaximumSendTime: number = Date.now() + this.mRoundTripMax;
await this.mCancellableDelay.Create(this.ResponseTimeout);
while (this.mPendingRequests.has(aRequestId) && Date.now() < lMaximumSendTime)
{
await this.QueueSend(aRequest);
await this.mCancellableDelay.Create(this.ResponseTimeout);
}
this.AssertRequestProcessed(aRequestId, aRequest);
}
private Open(): void
{
this.mDealer = new zmq.Dealer;
this.mDealer.sendTimeout = 0;
this.mDealer.connect(this.mEndpoint);
this.ResponseHandler();
}
private async ProcessSend(): Promise<void>
{
const lNextSend: TZmqSendRequest | undefined = this.mSendQueue.peek();
if (lNextSend && this.mSafeToSend)
{
this.mSafeToSend = false;
this.mSendQueue.dequeue();
try
{
await this.mDealer.send(lNextSend.Request);
}
catch (aError)
{
this.HandleZMQSendError(aError, lNextSend.Request);
}
lNextSend.Resolve();
this.mSafeToSend = true;
this.ProcessSend();
}
}
private ProcessZmqReceive(nonce: Buffer, msg: Buffer): void
{
const lRequestNonce: number = Number(nonce.toString());
const lMessage: string = msg.toString();
const lMessageCaller: TRequestResolver | undefined = this.mPendingRequests.get(lRequestNonce);
if (lMessageCaller)
{
lMessageCaller(this.GenerateRequestResult(lMessage, lRequestNonce));
this.mPendingRequests.delete(lRequestNonce);
}
}
private QueueSend(aRequest: TRequestBody): Promise<void>
{
let lResolver: () => void;
const lPromise: Promise<void> = new Promise<void>((aResolve: () => void): void =>
{
lResolver = aResolve;
});
this.mSendQueue.enqueue(
{
Request: aRequest,
Resolve: lResolver!,
},
);
this.ProcessSend();
return lPromise;
}
private async ResponseHandler(): Promise<void>
{
for await (const [nonce, msg] of this.mDealer)
{
if (this.mDealer)
{
this.ProcessZmqReceive(nonce, msg);
}
}
}
private async SendRequest(aRequest: TRequestBody): Promise<TRequestResponse>
{
const lRequestId: number = Number(aRequest[ERequestBody.Nonce]);
await this.QueueSend(aRequest); // Can potentially move this inside of ManageRequest loop
this.ManageRequest(lRequestId, aRequest);
return new Promise<TRequestResponse>((aResolve: TRequestResolver): void =>
{
this.mPendingRequests.set(lRequestId, aResolve);
});
}
public Close(): void
{
this.mDealer.linger = 0;
this.mDealer.close();
this.mDealer = undefined!;
this.mCancellableDelay.Clear();
}
public async Send(aData: string): Promise<TRequestResponse>
{
const lRequestId: number = ++this.mRequestNonce;
const lRequest: TRequestBody =
[
this.mOurUniqueId,
lRequestId.toString(),
aData,
];
return this.SendRequest(lRequest);
}
}