@sha1n/fungus
Version:
A dependency based service graph controller library
191 lines (190 loc) • 7.43 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createEnvironment = exports.InternalRuntimeContext = void 0;
const uuid_1 = require("uuid");
const dagraph_1 = __importDefault(require("@sha1n/dagraph"));
const logger_1 = require("./logger");
const ServiceController_1 = require("./ServiceController");
class InternalRuntimeContext {
constructor(name) {
this.name = name;
this._catalog = new Map();
}
get catalog() {
return this._catalog;
}
register(metadata) {
this._catalog.set(metadata.id, metadata);
}
unregister(id) {
this._catalog.delete(id);
}
}
exports.InternalRuntimeContext = InternalRuntimeContext;
class StoppedEnv {
constructor(specs, name) {
this.specs = specs;
this.logger = (0, logger_1.createLogger)(name);
this.ctx = new InternalRuntimeContext(name);
this.servicesGraph = this.init();
}
start() {
return __awaiter(this, void 0, void 0, function* () {
const outCtx = yield this.doStart(this.servicesGraph, this.ctx);
return [outCtx, this.startedEnv()];
});
}
stop() {
return __awaiter(this, void 0, void 0, function* () {
return Promise.reject(new Error('Not started'));
});
}
doStart(serviceGraph, ctx) {
return __awaiter(this, void 0, void 0, function* () {
this.logger.info('starting up...');
const allStartedPromise = new Promise((resolve, reject) => {
const services = serviceGraph.getServices();
const onError = (error) => {
ctx.shuttingDown = true;
reject(error);
};
for (const service of services) {
service.prependOnceListener('error', onError);
service.prependOnceListener('started', (metadata, ctx) => {
// This is critical to avoid handling errors that occur after startup
service.removeListener('error', onError);
ctx.register(metadata);
if (ctx.catalog.size === services.length) {
resolve(ctx);
}
});
}
Promise.allSettled(serviceGraph.getBootstrapServices().map(s => s.start(ctx)));
});
try {
const result = yield allStartedPromise;
return result;
}
catch (e) {
yield this.startedEnv().stop().catch(this.logger.error);
throw e;
}
});
}
startedEnv() {
return new StartedEnv(this.servicesGraph, this.specs, this.ctx.name);
}
init() {
const servicesGraph = new ServiceGraph();
const register = (service, dependencies) => {
this.logger.info('registering service %s', service.id);
servicesGraph.addService(service);
dependencies === null || dependencies === void 0 ? void 0 : dependencies.forEach(dep => {
servicesGraph.addDependency(service, dep);
});
};
for (const spec of this.specs) {
register(spec.service, spec.dependsOn);
}
return servicesGraph;
}
}
class StartedEnv {
constructor(servicesGraph, specs, name) {
this.servicesGraph = servicesGraph;
this.specs = specs;
this.logger = (0, logger_1.createLogger)(name);
this.ctx = new InternalRuntimeContext(name);
}
start() {
return __awaiter(this, void 0, void 0, function* () {
return Promise.reject(new Error('Already started'));
});
}
stop() {
return __awaiter(this, void 0, void 0, function* () {
yield this.doStop(this.ctx);
return new StoppedEnv(this.specs, this.ctx.name);
});
}
doStop(ctx) {
return __awaiter(this, void 0, void 0, function* () {
this.logger.info('stopping...');
// The teardown algorithm traverses all the services in reverse topological order
// to ensure that we stop as clean as possible, even if something fails
const errors = [];
for (const service of this.servicesGraph.getTeardownServices()) {
yield service
.stop(ctx)
.catch(e => {
errors.push(e);
return Promise.resolve();
})
.finally(() => {
ctx.unregister(service.id);
});
}
if (errors.length > 0) {
throw new Error(errors.map(e => e.message).join('\n'));
}
});
}
}
class ServiceGraph {
constructor() {
this.logger = (0, logger_1.createLogger)('srv-graph');
this.graph = (0, dagraph_1.default)();
}
addService(service) {
this.graph.addNode(this.getOrCreateControllerFor(service));
}
addDependency(service, dependency) {
const srvController = this.getOrCreateControllerFor(service);
const depController = this.getOrCreateControllerFor(dependency);
this.logger.info('adding dependency: %s depends on %s', service.id, depController.id);
this.graph.addEdge(depController, srvController);
// This is required in order to allow the controller to start once all deps are started.
srvController.addDependency(depController);
}
getServices() {
return [...this.graph.nodes()];
}
getBootstrapServices() {
return [...this.graph.roots()];
}
getTeardownServices() {
return [...this.graph.reverse().topologicalSort()];
}
getOrCreateControllerFor(service) {
return this.graph.getNode(service.id) || new ServiceController_1.ServiceController(service);
}
}
function createEnvironment(specs, options) {
const name = (options === null || options === void 0 ? void 0 : options.name) || `env-${(0, uuid_1.v4)()}`;
const stoppedEnv = new StoppedEnv(specs, name);
let env = stoppedEnv;
return {
name,
start: () => __awaiter(this, void 0, void 0, function* () {
const [ctx, startedEnv] = yield env.start();
env = startedEnv;
return ctx;
}),
stop: () => __awaiter(this, void 0, void 0, function* () {
env = yield env.stop();
})
};
}
exports.createEnvironment = createEnvironment;