UNPKG

@sha1n/fungus

Version:

A dependency based service graph controller library

178 lines (177 loc) 6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.InternalRuntimeContext = void 0; exports.createEnvironment = createEnvironment; const uuid_1 = require("uuid"); const dagraph_1 = __importDefault(require("@sha1n/dagraph")); const logger_1 = require("./logger"); const ServiceController_1 = require("./ServiceController"); class InternalRuntimeContext { name; _catalog = new Map(); shuttingDown; constructor(name) { this.name = name; } get catalog() { return this._catalog; } register(metadata) { this._catalog.set(metadata.id, metadata); } unregister(id) { this._catalog.delete(id); } } exports.InternalRuntimeContext = InternalRuntimeContext; class StoppedEnv { specs; logger; ctx; servicesGraph; constructor(specs, name) { this.specs = specs; this.logger = (0, logger_1.createLogger)(name); this.ctx = new InternalRuntimeContext(name); this.servicesGraph = this.init(); } async start() { const outCtx = await this.doStart(this.servicesGraph, this.ctx); return [outCtx, this.startedEnv()]; } async stop() { return Promise.reject(new Error('Not started')); } async doStart(serviceGraph, ctx) { 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); } }); } void Promise.allSettled(serviceGraph.getBootstrapServices().map(s => s.start(ctx))); }); try { const result = await allStartedPromise; return result; } catch (e) { await 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?.forEach(dep => { servicesGraph.addDependency(service, dep); }); }; for (const spec of this.specs) { register(spec.service, spec.dependsOn); } return servicesGraph; } } class StartedEnv { servicesGraph; specs; logger; ctx; constructor(servicesGraph, specs, name) { this.servicesGraph = servicesGraph; this.specs = specs; this.logger = (0, logger_1.createLogger)(name); this.ctx = new InternalRuntimeContext(name); } async start() { return Promise.reject(new Error('Already started')); } async stop() { await this.doStop(this.ctx); return new StoppedEnv(this.specs, this.ctx.name); } async doStop(ctx) { 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()) { await 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 { logger = (0, logger_1.createLogger)('srv-graph'); 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?.name || `env-${(0, uuid_1.v4)()}`; const stoppedEnv = new StoppedEnv(specs, name); let env = stoppedEnv; return { name, start: async () => { const [ctx, startedEnv] = await env.start(); env = startedEnv; return ctx; }, stop: async () => { env = await env.stop(); } }; }