UNPKG

@sha1n/fungus

Version:

A dependency based service graph controller library

191 lines (190 loc) 7.43 kB
"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;