Browse Source

Basic microservice class and unit tests

master
David Ludwig 3 years ago
parent
commit
6b28770942
5 changed files with 371 additions and 8 deletions
  1. +172
    -0
      packages/microservice/src/Microservice.ts
  2. +2
    -0
      packages/microservice/src/index.ts
  3. +21
    -0
      packages/microservice/src/schema.ts
  4. +0
    -8
      packages/microservice/test/unit/MicroService.spec.ts
  5. +176
    -0
      packages/microservice/test/unit/Microservice.spec.ts

+ 172
- 0
packages/microservice/src/Microservice.ts View File

@ -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<T>;
/**
* Declare EventEmitter types
*/
interface Events {
"boot": () => void,
"start": () => void,
"ready": () => void,
"shutdown": () => void,
"finished": () => void
}
/**
* Torrent IPC events
*/
export declare interface Microservice {
on<U extends keyof Events>(event: U, listener: Events[U]): this,
emit<U extends keyof Events>(event: U, ...args: Parameters<Events[U]>): boolean
}
/**
* The main application class
*/
export class Microservice extends EventEmitter
{
/**
* The exec promise used to wait for quit event
*/
#execPromise?: Promise<number>;
/**
* A handler function to quit the microservice application
*/
#quitHandler?: (value: number | PromiseLike<number>) => 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<number>(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;
}
};

+ 2
- 0
packages/microservice/src/index.ts View File

@ -0,0 +1,2 @@
export * from "./schema";
export * from "./Microservice";

+ 21
- 0
packages/microservice/src/schema.ts View File

@ -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
}

+ 0
- 8
packages/microservice/test/unit/MicroService.spec.ts View File

@ -1,8 +0,0 @@
import { expect } from "chai";
import "mocha";
describe("Test", () => {
it("Should be this", () => {
expect(true).to.equal(true);
})
});

+ 176
- 0
packages/microservice/test/unit/Microservice.spec.ts View File

@ -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<void>(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);
});
});
});
});

Loading…
Cancel
Save