UNPKG

reliable-zeromq

Version:

A collection of reliable zeromq messaging constructs

329 lines (269 loc) 10.7 kB
/* tslint:disable: no-string-literal */ import type { TestInterface } from "ava"; import anyTest, { ExecutionContext } from "ava"; import * as Sinon from "sinon"; import { ImportMock, MockManager } from "ts-mock-imports"; import * as zmq from "zeromq"; import Config from "../../Src/Config"; import { TPublisherHwmWarning } from "../../Src/Errors"; import JSONBigInt from "../../Src/Utils/JSONBigInt"; import { EMessageType, EPublishMessage, PUBLISHER_CACHE_EXPIRED, ZMQPublisher } from "../../Src/ZMQPublisher"; import * as ZMQResponse from "../../Src/ZMQResponse"; import { TSubscriptionEndpoints } from "../../Src/ZMQSubscriber/ZMQSubscriber"; import { YieldToEventLoop } from "../Helpers/AsyncTools"; import { DUMMY_ENDPOINTS } from "../Helpers/DummyEndpoints.data"; type TTestContext = { PublisherMock: MockManager<zmq.Publisher>; ResponderMock: MockManager<ZMQResponse.ZMQResponse>; TestData: any[]; Endpoints: TSubscriptionEndpoints; }; const test: TestInterface<TTestContext> = anyTest as TestInterface<TTestContext> ; test.before((t: ExecutionContext<TTestContext>): void => { // Unnecessary }); test.beforeEach((t: ExecutionContext<TTestContext>): void => { const lResponderMock: MockManager<ZMQResponse.ZMQResponse> = ImportMock.mockClass<ZMQResponse.ZMQResponse>(ZMQResponse, "ZMQResponse"); const lPublisherMock: MockManager<zmq.Publisher> = ImportMock.mockClass<zmq.Publisher>(zmq, "Publisher"); t.context = { PublisherMock: lPublisherMock, ResponderMock: lResponderMock, TestData: [ { a: 100n, b: 20n, // JSONBigInt will parse "20n" to 20n, known issue c: 0.5, d: [ 5n, "myFunc()", ], }, ], Endpoints: DUMMY_ENDPOINTS.STATUS_UPDATES, }; }); test.afterEach((t: ExecutionContext<TTestContext>): void => { Sinon.restore(); ImportMock.restore(); }); test.serial("Start, Publish, Respond, Close", async(t: ExecutionContext<TTestContext>): Promise<void> => { const clock: Sinon.SinonFakeTimers = Sinon.useFakeTimers(); const lZmqPublisher: MockManager<zmq.Publisher> = t.context.PublisherMock; const lPublisher: ZMQPublisher = new ZMQPublisher(t.context.Endpoints); lZmqPublisher.mock("bind", Promise.resolve()); await lPublisher.Open(); t.is(lPublisher.Endpoint, DUMMY_ENDPOINTS.STATUS_UPDATES.PublisherAddress); const lSendMock: Sinon.SinonStub = lZmqPublisher.mock("send", Promise.resolve()); lPublisher.Publish("myTopicA", "myFirstMessage"); t.is(lSendMock.callCount, 1); t.deepEqual(lSendMock.getCall(0).args[0], ["myTopicA", EMessageType.PUBLISH, "0", "myFirstMessage"]); t.is(lPublisher["mMessageCaches"].size, 1); t.is(lPublisher["mMessageCaches"].get("myTopicA")!.size, 1); t.is(lPublisher["mTopicDetails"].size, 1); lPublisher.Publish("myTopicA", JSONBigInt.Stringify(t.context.TestData)); await YieldToEventLoop(); t.is(lSendMock.callCount, 2); t.deepEqual( lSendMock.getCall(1).args[0], ["myTopicA", EMessageType.PUBLISH, "1", JSONBigInt.Stringify(t.context.TestData)], ); t.is(lPublisher["mMessageCaches"].size, 1); t.is(lPublisher["mMessageCaches"].get("myTopicA")!.size, 2); t.is(lPublisher["mTopicDetails"].size, 1); const lRecoveryRequest: [string, ...number[]] = [ "myTopicA", 0, 1, ]; const lRecoveryResponse: string = await lPublisher["HandleRequest"](JSONBigInt.Stringify(lRecoveryRequest)); const lExpectedRecoveryResponse: string[][] = [ lSendMock.getCall(0).args[0], lSendMock.getCall(1).args[0], ]; t.deepEqual(JSONBigInt.Parse(lRecoveryResponse), lExpectedRecoveryResponse); const lInvalidRecoveryRequest: [number, number] = [ -1, // Non-existant nonce 0, ]; const lInvalidRecoveryResponse: string = await lPublisher["HandleRequest"](JSONBigInt.Stringify(lInvalidRecoveryRequest)); t.deepEqual(JSONBigInt.Parse(lInvalidRecoveryResponse), []); const lTestData: [string, string][] = [ ["newTopicA", "myMessageA"], ["newTopicA", "myMessageB"], ["newTopicA", "myMessageC"], ["newTopic1", "myMessage~"], ["newTopic1", "myMessage~"], ]; lPublisher.Publish("newTopicA", "myMessageA"); lPublisher.Publish("newTopicA", "myMessageB"); lPublisher.Publish("newTopicA", "myMessageC"); lPublisher.Publish("newTopic1", "myMessage~"); lPublisher.Publish("newTopic1", "myMessage~"); await YieldToEventLoop(); for (let i: number = 0; i < 5; ++i) { t.is(lSendMock.getCall(i + 2).args[0][EPublishMessage.Topic], lTestData[i][0]); t.is(lSendMock.getCall(i + 2).args[0][EPublishMessage.Message], lTestData[i][1]); } const lSecondRecoveryRequest: [string, ...number[]] = [ "newTopicA", 0, 1, 2, ]; const lThirdRecoveryRequest: [string, ...number[]] = [ "newTopic1", 0, 1, ]; const lSecondRecoveryResponse: string = await lPublisher["HandleRequest"](JSONBigInt.Stringify(lSecondRecoveryRequest)); const lThirdRecoveryResponse: string = await lPublisher["HandleRequest"](JSONBigInt.Stringify(lThirdRecoveryRequest)); const lExpectedSecondResponse: string[][] = [ lSendMock.getCall(2).args[0], lSendMock.getCall(3).args[0], lSendMock.getCall(4).args[0], ]; const lExpectedThirdResponse: string[][] = [ lSendMock.getCall(5).args[0], lSendMock.getCall(6).args[0], ]; t.deepEqual(JSONBigInt.Parse(lSecondRecoveryResponse), lExpectedSecondResponse); t.deepEqual(JSONBigInt.Parse(lThirdRecoveryResponse), lExpectedThirdResponse); t.is(lSendMock.callCount, 7); t.is(lPublisher["mTopicDetails"].size, 3); clock.tick(Config.HeartBeatInterval); await YieldToEventLoop(); const lHeartbeats: string[][] = [ lSendMock.getCall(7).args[0], lSendMock.getCall(8).args[0], lSendMock.getCall(9).args[0], ]; const lExpectedHeartbeats: string[][] = [ ["myTopicA", EMessageType.HEARTBEAT, "1", ""], ["newTopicA", EMessageType.HEARTBEAT, "2", ""], ["newTopic1", EMessageType.HEARTBEAT, "1", ""], ]; t.deepEqual(lHeartbeats, lExpectedHeartbeats); t.is(lSendMock.callCount, 10); clock.tick(Config.HeartBeatInterval); await YieldToEventLoop(); t.is(lSendMock.callCount, 13); lPublisher.Close(); }); test.serial("Errors & Warns", async(t: ExecutionContext<TTestContext>) => { const clock: Sinon.SinonFakeTimers = Sinon.useFakeTimers(); const lZmqPublisher: MockManager<zmq.Publisher> = t.context.PublisherMock; // Set Config for Test: Config.MaximumLatency = 1000; Config.HeartBeatInterval = 500; const lSendMock: Sinon.SinonStub = lZmqPublisher.mock("send", Promise.resolve()); lZmqPublisher.mock("bind", Promise.resolve()); const lWarnings: TPublisherHwmWarning[] = []; const lPublisher: ZMQPublisher = new ZMQPublisher(t.context.Endpoints); const lCustomPublisher: ZMQPublisher = new ZMQPublisher( t.context.Endpoints, { HighWaterMarkWarning: (aWarning: TPublisherHwmWarning): void => { lWarnings.push(aWarning); }, }, ); await lCustomPublisher.Open(); await lPublisher.Open(); lPublisher.Publish("myTopicA", "myFirstMessage"); t.is(lSendMock.callCount, 1); t.deepEqual(lSendMock.getCall(0).args[0], ["myTopicA", EMessageType.PUBLISH, "0", "myFirstMessage"]); const lFirstRecoveryRequest: [string, ...number[]] = [ "myTopicA", 0, 1, ]; const lFirstRecoveryResponse: string[][] = JSONBigInt.Parse( await lPublisher["HandleRequest"](JSONBigInt.Stringify(lFirstRecoveryRequest)), ); const lFirstExpectedResponse: string[][] = [ lSendMock.getCall(0).args[0], [PUBLISHER_CACHE_EXPIRED], ]; t.deepEqual(lFirstRecoveryResponse, lFirstExpectedResponse); lPublisher.Publish("myTopicA", "mySecondMessage"); // Message 1 & 2 published on timestamp: 0 clock.tick(501); // const EXPIRY_BUFFER: number = 500; lPublisher.Publish("myTopicA", "myThirdMessage"); // Message 3 & 4 published on timestamp: 1 lPublisher.Publish("myTopicA", "myFourthMessage"); // Expire messages 1 & 2 clock.tick(Config.MaximumLatency * 3); await YieldToEventLoop(); const lSecondRecoveryRequest: [string, ...number[]] = [ "myTopicA", 0, 1, 2, 3, ]; const lSecondRecoveryResponse: string[][] = JSONBigInt.Parse( await lPublisher["HandleRequest"](JSONBigInt.Stringify(lSecondRecoveryRequest)), ); const lSecondExpectedResponse: string[][] = [ [PUBLISHER_CACHE_EXPIRED], // 0 [PUBLISHER_CACHE_EXPIRED], // 1 lSendMock.getCall(3).args[0], // 2 lSendMock.getCall(4).args[0], // 3 ]; t.deepEqual(lSecondRecoveryResponse, lSecondExpectedResponse); lSendMock.returns(Promise.reject( { code: "EAGAIN", }, )); // Test HWMWarning clock.tick(100); t.is(lWarnings.length, 0); t.is(lSendMock.callCount, 10); lPublisher.Publish("myTopicA", "myFifthMessage"); await YieldToEventLoop(); t.is(lWarnings.length, 0); t.is(lSendMock.callCount, 11); lCustomPublisher.Publish("myTopicB", "myFirstMessage"); await YieldToEventLoop(); t.is(lWarnings.length, 1); t.is(lSendMock.callCount, 12); t.deepEqual(lWarnings[0], { Topic: "myTopicB", Nonce: 0, Message: "myFirstMessage" }); clock.tick(Config.HeartBeatInterval - 101); // Trigger old timeout clock.tick(Config.HeartBeatInterval); // Trigger new timeout await YieldToEventLoop(); t.is(lWarnings.length, 2); t.is(lSendMock.callCount, 14); t.deepEqual(lWarnings[1], { Topic: "myTopicB", Nonce: 0, Message: "" }); lSendMock.returns(Promise.resolve()); clock.tick(Config.HeartBeatInterval); await YieldToEventLoop(); const lActual: string[][] = [lSendMock.getCall(12).args[0], lSendMock.getCall(13).args[0]].sort(); const lExpected: string[][] = [["myTopicA", "HEARTBEAT", "4", ""], ["myTopicB", "HEARTBEAT", "0", ""]]; t.deepEqual(lActual, lExpected); lPublisher.Close(); await YieldToEventLoop(); // Reset Config Config.SetGlobalConfig(); });