From 6b28770942638d7d621848a69ec11f566777f768 Mon Sep 17 00:00:00 2001 From: David Ludwig Date: Thu, 10 Mar 2022 21:47:22 -0600 Subject: [PATCH] Basic microservice class and unit tests --- packages/microservice/src/Microservice.ts | 172 +++++++++++++++++ packages/microservice/src/index.ts | 2 + packages/microservice/src/schema.ts | 21 +++ .../test/unit/MicroService.spec.ts | 8 - .../test/unit/Microservice.spec.ts | 176 ++++++++++++++++++ 5 files changed, 371 insertions(+), 8 deletions(-) create mode 100644 packages/microservice/src/Microservice.ts create mode 100644 packages/microservice/src/schema.ts delete mode 100644 packages/microservice/test/unit/MicroService.spec.ts create mode 100644 packages/microservice/test/unit/Microservice.spec.ts diff --git a/packages/microservice/src/Microservice.ts b/packages/microservice/src/Microservice.ts new file mode 100644 index 0000000..0ecba53 --- /dev/null +++ b/packages/microservice/src/Microservice.ts @@ -0,0 +1,172 @@ +import assert from "assert"; +import { EventEmitter } from "events"; +import { MicroserviceState } from "./schema"; + +/** + * The InternalService constructor type + */ +// type InternalServiceConstructor< +// T extends Microservice = Microservice +// > = new (microservice: T) => InternalService; + +/** + * Declare EventEmitter types + */ +interface Events { + "boot": () => void, + "start": () => void, + "ready": () => void, + "shutdown": () => void, + "finished": () => void +} + +/** + * Torrent IPC events + */ +export declare interface Microservice { + on(event: U, listener: Events[U]): this, + emit(event: U, ...args: Parameters): boolean +} + + /** + * The main application class + */ +export class Microservice extends EventEmitter +{ + /** + * The exec promise used to wait for quit event + */ + #execPromise?: Promise; + + /** + * A handler function to quit the microservice application + */ + #quitHandler?: (value: number | PromiseLike) => void; + + + /** + * The current state of the microservice + */ + #state: MicroserviceState = MicroserviceState.Idle; + + // Microservice Management --------------------------------------------------------------------- + + /** + * Invoke the boot phase of the microservice + */ + protected async boot() { + process.on("SIGINT", this.quit.bind(this)); + this.setState(MicroserviceState.Booting); + this.emit("boot"); + return true; + } + + /** + * Invoke the start phase of the microservice + */ + protected async start() { + this.setState(MicroserviceState.Starting); + this.emit("start"); + return true; + } + + /** + * Invoke the run phase of the microservice + */ + protected async run() { + this.setState(MicroserviceState.Running); + this.emit("ready"); + assert(this.#execPromise !== undefined); + let exitCode = await this.#execPromise; + return exitCode; + } + + /** + * Invoke the shutdown phase of the microservice + */ + protected async shutdown() { + process.off("SIGINT", this.quit.bind(this)); + this.setState(MicroserviceState.Quitting); + this.emit("shutdown"); + return true; + } + + /** + * Run the application + */ + public async exec() { + // Exit if not in an idling state + if (this.state() != MicroserviceState.Idle) { + console.error("Cannot exec an already-started microservice"); + return 1; + } + // Run the microservice application + let exitCode = await (async () => { + // Create the microservice execution promise to listen for quit events + let hasQuit = false; + this.#execPromise = new Promise(resolve => this.#quitHandler = (exitCode) => { + resolve(exitCode); + hasQuit = true; + }); + + // Boot the microservice and internal services + if (!await this.boot()) { // no need to check for hasQuit + console.error("Failed to boot the microservice"); + return 1; + } + // Start the internal services + if (!hasQuit && !await this.start()) { + console.error("Failed to start the microservice"); + return 2; + } + // If the application has not quit, we can run the app + let exitCode: number; + if (!hasQuit) { + exitCode = await this.run(); + } else { + exitCode = await this.#execPromise; + } + + // Shutdown the microservice + if (!await this.shutdown()) { + console.error("Failed to shutdown the microservice"); + return 3; + } + return exitCode; + })(); + this.setState(MicroserviceState.Finished); + return exitCode; + } + + /** + * Quit the application + */ + public quit(code: number = 0) { + if (this.state() == MicroserviceState.Idle) { + this.setState(MicroserviceState.Finished); + return; + } + if (this.state() > MicroserviceState.Running || this.#quitHandler === undefined) { + return; + } + this.#quitHandler(code); + } + + // Accessors ----------------------------------------------------------------------------------- + + /** + * Get the current state of the microservice + */ + public state() { + return this.#state; + } + + // Mutators ------------------------------------------------------------------------------------ + + /** + * Set the current state of the microservice + */ + protected setState(state: MicroserviceState) { + this.#state = state; + } +}; diff --git a/packages/microservice/src/index.ts b/packages/microservice/src/index.ts index e69de29..f4a8ce4 100644 --- a/packages/microservice/src/index.ts +++ b/packages/microservice/src/index.ts @@ -0,0 +1,2 @@ +export * from "./schema"; +export * from "./Microservice"; diff --git a/packages/microservice/src/schema.ts b/packages/microservice/src/schema.ts new file mode 100644 index 0000000..02f7ecf --- /dev/null +++ b/packages/microservice/src/schema.ts @@ -0,0 +1,21 @@ +/** + * Microservice states + */ + export enum MicroserviceState { + Idle, + Booting, + Starting, + Running, + Quitting, + Finished +} + +/** + * Microservice exit codes + */ +export enum ExitCode { + Ok, + BootError, + StartupError, + ShutdownError +} diff --git a/packages/microservice/test/unit/MicroService.spec.ts b/packages/microservice/test/unit/MicroService.spec.ts deleted file mode 100644 index 9aa86a7..0000000 --- a/packages/microservice/test/unit/MicroService.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { expect } from "chai"; -import "mocha"; - -describe("Test", () => { - it("Should be this", () => { - expect(true).to.equal(true); - }) -}); diff --git a/packages/microservice/test/unit/Microservice.spec.ts b/packages/microservice/test/unit/Microservice.spec.ts new file mode 100644 index 0000000..ed8a03a --- /dev/null +++ b/packages/microservice/test/unit/Microservice.spec.ts @@ -0,0 +1,176 @@ +import { expect } from "chai"; +import "mocha"; + +import { + Microservice, + MicroserviceState +} from "../../src"; + +class StateControlledMicroservice extends Microservice { + + public readonly quitState: MicroserviceState; + public hasBooted = false; + public hasStarted = false; + public hasRun = false; + public shutdownCalls = 0; + + public constructor(quitState: MicroserviceState) { + super(); + this.quitState = quitState; + } + + protected override async boot() { + let result = await super.boot(); + if (this.quitState === MicroserviceState.Booting) { + this.quit(25); + } else { + this.hasBooted = true; + } + return result; + } + + protected override async start() { + let result = await super.start(); + if (this.quitState === MicroserviceState.Starting) { + this.quit(50); + } else { + this.hasStarted = true; + } + return result; + } + + protected override async run() { + this.hasRun = true; + return await super.run(); + } + + protected override async shutdown() { + this.shutdownCalls++; + let result = super.shutdown(); + if (this.quitState === MicroserviceState.Quitting) { + this.quit(100); + } + return result; + } +} + +describe("Microservice", () => { + describe("Basic state management", () => { + describe("Initial state", () => { + let microservice = new Microservice(); + it("should be idle", () => { + expect(microservice.state()).to.equal(MicroserviceState.Idle); + }); + }); + + describe("Running state", () => { + let microservice = new Microservice(); + it("should be running", async () => { + let cb = new Promise(fulfill => microservice.on("ready", fulfill)); + microservice.exec(); + await cb; + expect(microservice.state()).to.equal(MicroserviceState.Running); + }); + }); + + // describe("Finished State", () => { + // let microservice = new Microservice(); + // let cb = microservice.exec(); + // microservice.quit(); + // it("Should be in a finished state", async () => { + // await cb; + // expect(microservice.state()).to.equal(MicroserviceState.Finished); + // }); + // }); + }); + + describe("Quit state control flow", () => { + describe("Quit before executing process", async () => { + let microservice = new Microservice(); + microservice.quit(); + it("should be in finished state", () => { + expect(microservice.state()).to.equal(MicroserviceState.Finished); + }); + }); + describe("Quit during boot state", () => { + let microservice = new StateControlledMicroservice(MicroserviceState.Booting); + it("should have the correct exit code", async () => { + let exitCode = await microservice.exec(); + expect(exitCode).to.equal(25); + }); + it("should be in finished state", () => { + expect(microservice.state()).to.equal(MicroserviceState.Finished); + }); + it("should not have booted", () => { + expect(microservice.hasBooted).to.be.false; + }); + it("should not have started", () => { + expect(microservice.hasStarted).to.be.false; + }); + it("should have shutdown", () => { + expect(microservice.shutdownCalls).to.equal(1); + }); + }); + describe("Quit during starting state", () => { + let microservice = new StateControlledMicroservice(MicroserviceState.Starting); + it("should have the correct exit code", async () => { + let exitCode = await microservice.exec(); + expect(exitCode).to.equal(50); + }); + it("should be in finished state", () => { + expect(microservice.state()).to.equal(MicroserviceState.Finished); + }); + it("should have booted", () => { + expect(microservice.hasBooted).to.be.true; + }); + it("should not have started", () => { + expect(microservice.hasStarted).to.be.false; + }); + it("should have shutdown", () => { + expect(microservice.shutdownCalls).to.equal(1); + }); + }); + describe("Quit during running state", () => { + let microservice = new StateControlledMicroservice(MicroserviceState.Running); + let runningPromise = microservice.exec(); + setImmediate(() => microservice.quit(75)); + it("should have the correct exit code", async () => { + let exitCode = await runningPromise; + expect(exitCode).to.equal(75); + }); + it("should be in finished state", () => { + expect(microservice.state()).to.equal(MicroserviceState.Finished); + }); + it("should have booted", () => { + expect(microservice.hasBooted).to.be.true; + }); + it("should have started", () => { + expect(microservice.hasStarted).to.be.true; + }); + it("should have shutdown", () => { + expect(microservice.shutdownCalls).to.equal(1); + }); + }); + describe("Quit during quitting state", () => { + let microservice = new StateControlledMicroservice(MicroserviceState.Quitting); + microservice.once("ready", microservice.quit); + let runningPromise = microservice.exec(); + it("should have the correct exit code", async () => { + let exitCode = await runningPromise; + expect(exitCode).to.equal(0); + }); + it("should be in finished state", () => { + expect(microservice.state()).to.equal(MicroserviceState.Finished); + }); + it("should have booted", () => { + expect(microservice.hasBooted).to.be.true; + }); + it("should have started", () => { + expect(microservice.hasStarted).to.be.true; + }); + it("should have shutdown only once", () => { + expect(microservice.shutdownCalls).to.equal(1); + }); + }); + }); +});