@rocicorp/zero 1.11.0-head-86ca5eff-20261014 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/zero/package.js +1 -1
- package/out/zero/package.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts +3 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +4 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +3 -3
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/drain-tracker.d.ts +4 -4
- package/out/zero-cache/src/services/change-streamer/drain-tracker.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/drain-tracker.js +4 -8
- package/out/zero-cache/src/services/change-streamer/drain-tracker.js.map +1 -1
- package/out/zero-cache/src/services/http-service.d.ts +6 -0
- package/out/zero-cache/src/services/http-service.d.ts.map +1 -1
- package/out/zero-cache/src/services/http-service.js +11 -0
- package/out/zero-cache/src/services/http-service.js.map +1 -1
- package/out/zero-cache/src/services/life-cycle.d.ts +3 -2
- package/out/zero-cache/src/services/life-cycle.d.ts.map +1 -1
- package/out/zero-cache/src/services/life-cycle.js +17 -4
- package/out/zero-cache/src/services/life-cycle.js.map +1 -1
- package/out/zero-cache/src/services/replicator/drain-handler.d.ts +8 -0
- package/out/zero-cache/src/services/replicator/drain-handler.d.ts.map +1 -1
- package/out/zero-cache/src/services/replicator/drain-handler.js +38 -4
- package/out/zero-cache/src/services/replicator/drain-handler.js.map +1 -1
- package/out/zero-client/src/client/version.js +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http-service.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/services/http-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AACjD,OAAgB,EAAC,KAAK,eAAe,EAAC,MAAM,SAAS,CAAC;AAGtD,OAAO,EAAC,YAAY,EAAC,MAAM,oBAAoB,CAAC;AAChD,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,cAAc,CAAC;AAE1C,MAAM,MAAM,OAAO,GAAG;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,kBAAkB,EAAE,MAAM,GAAG,SAAS,CAAC;IAGvC,aAAa,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,SAAS,CAAC;CAC9C,CAAC;AAEF;;;;GAIG;AACH,qBAAa,WAAY,YAAW,OAAO;;IACzC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC;IAGnC,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAOxC,YACE,EAAE,EAAE,MAAM,EACV,EAAE,EAAE,UAAU,EACd,IAAI,EAAE,OAAO,EACb,IAAI,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,EAczD;IAGD,SAAS,CAAC,QAAQ,SAAK;IACvB,SAAS,CAAC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAEjC;IAGK,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,CAwB7B;IAEK,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAGzB;IAED;;;;OAIG;IACH,gBAAgB,SAGf;IAEK,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAM1B;CACF"}
|
|
1
|
+
{"version":3,"file":"http-service.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/services/http-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AACjD,OAAgB,EAAC,KAAK,eAAe,EAAC,MAAM,SAAS,CAAC;AAGtD,OAAO,EAAC,YAAY,EAAC,MAAM,oBAAoB,CAAC;AAChD,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,cAAc,CAAC;AAE1C,MAAM,MAAM,OAAO,GAAG;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,kBAAkB,EAAE,MAAM,GAAG,SAAS,CAAC;IAGvC,aAAa,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,SAAS,CAAC;CAC9C,CAAC;AAEF;;;;GAIG;AACH,qBAAa,WAAY,YAAW,OAAO;;IACzC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC;IAGnC,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAOxC,YACE,EAAE,EAAE,MAAM,EACV,EAAE,EAAE,UAAU,EACd,IAAI,EAAE,OAAO,EACb,IAAI,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,EAczD;IAGD,SAAS,CAAC,QAAQ,SAAK;IACvB,SAAS,CAAC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAEjC;IAGK,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,CAwB7B;IAEK,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAGzB;IAED;;;;OAIG;IACH,gBAAgB,SAGf;IAED;;;;OAIG;IACH,aAAa,SAKZ;IAEK,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAM1B;CACF"}
|
|
@@ -65,6 +65,17 @@ var HttpService = class {
|
|
|
65
65
|
this._lc.info?.(`${this.id}: failing health checks to self drain`);
|
|
66
66
|
this.#failHealthChecks = true;
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Stops accepting new connections (e.g. when draining), while existing
|
|
70
|
+
* connections, including websockets, continue to be served until
|
|
71
|
+
* {@link stop} is called.
|
|
72
|
+
*/
|
|
73
|
+
stopAccepting() {
|
|
74
|
+
if (this.#fastify.server.listening) {
|
|
75
|
+
this._lc.info?.(`${this.id}: no longer accepting new connections`);
|
|
76
|
+
this.#fastify.server.close();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
68
79
|
async stop() {
|
|
69
80
|
this._lc.info?.(`${this.id}: no longer accepting connections`);
|
|
70
81
|
this.#heartbeatMonitor?.stop();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http-service.js","names":["#fastify","#port","#heartbeatMonitor","#init","#ready","#failHealthChecks"],"sources":["../../../../../zero-cache/src/services/http-service.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport Fastify, {type FastifyInstance} from 'fastify';\nimport {promiseVoid} from '../../../shared/src/resolved-promises.ts';\nimport {HeartbeatMonitor} from './life-cycle.ts';\nimport {RunningState} from './running-state.ts';\nimport type {Service} from './service.ts';\n\nexport type Options = {\n port: number;\n keepaliveTimeoutMs: number | undefined;\n\n // Wait for the readinessGate to resolve before responding to health checks.\n readinessGate?: Promise<unknown> | undefined;\n};\n\n/**\n * Common functionality for all HttpServices. These include:\n * * Responding to health checks at \"/\"\n * * Tracking optional heartbeats at \"/keepalive\" and draining when they stop.\n */\nexport class HttpService implements Service {\n readonly id: string;\n protected readonly _lc: LogContext;\n readonly #fastify: FastifyInstance;\n readonly #port: number;\n protected readonly _state: RunningState;\n readonly #heartbeatMonitor: HeartbeatMonitor | undefined;\n readonly #init: (fastify: FastifyInstance) => void | Promise<void>;\n\n #ready = false;\n #failHealthChecks = false;\n\n constructor(\n id: string,\n lc: LogContext,\n opts: Options,\n init: (fastify: FastifyInstance) => void | Promise<void>,\n ) {\n const {port, keepaliveTimeoutMs, readinessGate = promiseVoid} = opts;\n this.id = id;\n this._lc = lc.withContext('component', this.id);\n this.#fastify = Fastify();\n this.#port = port;\n this.#init = init;\n this._state = new RunningState(id);\n this.#heartbeatMonitor = keepaliveTimeoutMs\n ? new HeartbeatMonitor(this._lc, keepaliveTimeoutMs)\n : undefined;\n\n void readinessGate.then(() => (this.#ready = true));\n }\n\n // Life-cycle hooks for subclass implementations\n protected _onStart() {}\n protected _onStop(): Promise<void> {\n return promiseVoid;\n }\n // start() is used in unit tests.\n // run() is the lifecycle method called by the ServiceRunner.\n async start(): Promise<string> {\n this.#fastify.get('/', (_req, res) => {\n if (this.#failHealthChecks) {\n res.code(503).send('Draining');\n } else if (this.#ready) {\n res.send('OK');\n }\n });\n this.#fastify.get('/keepalive', ({headers}, res) => {\n this.#heartbeatMonitor?.onHeartbeat(headers);\n if (this.#failHealthChecks) {\n res.code(503).send('Draining');\n } else if (this.#ready) {\n res.send('OK');\n }\n });\n await this.#init(this.#fastify);\n const address = await this.#fastify.listen({\n host: '::',\n port: this.#port,\n });\n this._lc.info?.(`${this.id} listening at ${address}`);\n this._onStart();\n return address;\n }\n\n async run(): Promise<void> {\n await this.start();\n await this._state.stopped();\n }\n\n /**\n * Responds to subsequent health checks (both \"/\" and \"/keepalive\") with a\n * 503, e.g. to signal the container runner to stop routing to, and shut\n * down, a server that is draining itself (see {@link SelfDrain}).\n */\n failHealthChecks() {\n this._lc.info?.(`${this.id}: failing health checks to self drain`);\n this.#failHealthChecks = true;\n }\n\n async stop(): Promise<void> {\n this._lc.info?.(`${this.id}: no longer accepting connections`);\n this.#heartbeatMonitor?.stop();\n this._state.stop(this._lc);\n await this.#fastify.close();\n await this._onStop();\n }\n}\n"],"mappings":";;;;;;;;;;AAoBA,IAAa,cAAb,MAA4C;CAC1C;CACA;CACA;CACA;CACA;CACA;CACA;CAEA,SAAS;CACT,oBAAoB;CAEpB,YACE,IACA,IACA,MACA,MACA;EACA,MAAM,EAAC,MAAM,oBAAoB,gBAAgB,gBAAe;EAChE,KAAK,KAAK;EACV,KAAK,MAAM,GAAG,YAAY,aAAa,KAAK,EAAE;EAC9C,KAAKA,WAAW,QAAQ;EACxB,KAAKC,QAAQ;EACb,KAAKE,QAAQ;EACb,KAAK,SAAS,IAAI,aAAa,EAAE;EACjC,KAAKD,oBAAoB,qBACrB,IAAI,iBAAiB,KAAK,KAAK,kBAAkB,IACjD,KAAA;EAEJ,cAAmB,WAAY,KAAKE,SAAS,IAAK;CACpD;CAGA,WAAqB,CAAC;CACtB,UAAmC;EACjC,OAAO;CACT;CAGA,MAAM,QAAyB;EAC7B,KAAKJ,SAAS,IAAI,MAAM,MAAM,QAAQ;GACpC,IAAI,KAAKK,mBACP,IAAI,KAAK,GAAG,EAAE,KAAK,UAAU;QACxB,IAAI,KAAKD,QACd,IAAI,KAAK,IAAI;EAEjB,CAAC;EACD,KAAKJ,SAAS,IAAI,eAAe,EAAC,WAAU,QAAQ;GAClD,KAAKE,mBAAmB,YAAY,OAAO;GAC3C,IAAI,KAAKG,mBACP,IAAI,KAAK,GAAG,EAAE,KAAK,UAAU;QACxB,IAAI,KAAKD,QACd,IAAI,KAAK,IAAI;EAEjB,CAAC;EACD,MAAM,KAAKD,MAAM,KAAKH,QAAQ;EAC9B,MAAM,UAAU,MAAM,KAAKA,SAAS,OAAO;GACzC,MAAM;GACN,MAAM,KAAKC;EACb,CAAC;EACD,KAAK,IAAI,OAAO,GAAG,KAAK,GAAG,gBAAgB,SAAS;EACpD,KAAK,SAAS;EACd,OAAO;CACT;CAEA,MAAM,MAAqB;EACzB,MAAM,KAAK,MAAM;EACjB,MAAM,KAAK,OAAO,QAAQ;CAC5B;;;;;;CAOA,mBAAmB;EACjB,KAAK,IAAI,OAAO,GAAG,KAAK,GAAG,sCAAsC;EACjE,KAAKI,oBAAoB;CAC3B;CAEA,MAAM,OAAsB;EAC1B,KAAK,IAAI,OAAO,GAAG,KAAK,GAAG,kCAAkC;EAC7D,
|
|
1
|
+
{"version":3,"file":"http-service.js","names":["#fastify","#port","#heartbeatMonitor","#init","#ready","#failHealthChecks"],"sources":["../../../../../zero-cache/src/services/http-service.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport Fastify, {type FastifyInstance} from 'fastify';\nimport {promiseVoid} from '../../../shared/src/resolved-promises.ts';\nimport {HeartbeatMonitor} from './life-cycle.ts';\nimport {RunningState} from './running-state.ts';\nimport type {Service} from './service.ts';\n\nexport type Options = {\n port: number;\n keepaliveTimeoutMs: number | undefined;\n\n // Wait for the readinessGate to resolve before responding to health checks.\n readinessGate?: Promise<unknown> | undefined;\n};\n\n/**\n * Common functionality for all HttpServices. These include:\n * * Responding to health checks at \"/\"\n * * Tracking optional heartbeats at \"/keepalive\" and draining when they stop.\n */\nexport class HttpService implements Service {\n readonly id: string;\n protected readonly _lc: LogContext;\n readonly #fastify: FastifyInstance;\n readonly #port: number;\n protected readonly _state: RunningState;\n readonly #heartbeatMonitor: HeartbeatMonitor | undefined;\n readonly #init: (fastify: FastifyInstance) => void | Promise<void>;\n\n #ready = false;\n #failHealthChecks = false;\n\n constructor(\n id: string,\n lc: LogContext,\n opts: Options,\n init: (fastify: FastifyInstance) => void | Promise<void>,\n ) {\n const {port, keepaliveTimeoutMs, readinessGate = promiseVoid} = opts;\n this.id = id;\n this._lc = lc.withContext('component', this.id);\n this.#fastify = Fastify();\n this.#port = port;\n this.#init = init;\n this._state = new RunningState(id);\n this.#heartbeatMonitor = keepaliveTimeoutMs\n ? new HeartbeatMonitor(this._lc, keepaliveTimeoutMs)\n : undefined;\n\n void readinessGate.then(() => (this.#ready = true));\n }\n\n // Life-cycle hooks for subclass implementations\n protected _onStart() {}\n protected _onStop(): Promise<void> {\n return promiseVoid;\n }\n // start() is used in unit tests.\n // run() is the lifecycle method called by the ServiceRunner.\n async start(): Promise<string> {\n this.#fastify.get('/', (_req, res) => {\n if (this.#failHealthChecks) {\n res.code(503).send('Draining');\n } else if (this.#ready) {\n res.send('OK');\n }\n });\n this.#fastify.get('/keepalive', ({headers}, res) => {\n this.#heartbeatMonitor?.onHeartbeat(headers);\n if (this.#failHealthChecks) {\n res.code(503).send('Draining');\n } else if (this.#ready) {\n res.send('OK');\n }\n });\n await this.#init(this.#fastify);\n const address = await this.#fastify.listen({\n host: '::',\n port: this.#port,\n });\n this._lc.info?.(`${this.id} listening at ${address}`);\n this._onStart();\n return address;\n }\n\n async run(): Promise<void> {\n await this.start();\n await this._state.stopped();\n }\n\n /**\n * Responds to subsequent health checks (both \"/\" and \"/keepalive\") with a\n * 503, e.g. to signal the container runner to stop routing to, and shut\n * down, a server that is draining itself (see {@link SelfDrain}).\n */\n failHealthChecks() {\n this._lc.info?.(`${this.id}: failing health checks to self drain`);\n this.#failHealthChecks = true;\n }\n\n /**\n * Stops accepting new connections (e.g. when draining), while existing\n * connections, including websockets, continue to be served until\n * {@link stop} is called.\n */\n stopAccepting() {\n if (this.#fastify.server.listening) {\n this._lc.info?.(`${this.id}: no longer accepting new connections`);\n this.#fastify.server.close();\n }\n }\n\n async stop(): Promise<void> {\n this._lc.info?.(`${this.id}: no longer accepting connections`);\n this.#heartbeatMonitor?.stop();\n this._state.stop(this._lc);\n await this.#fastify.close();\n await this._onStop();\n }\n}\n"],"mappings":";;;;;;;;;;AAoBA,IAAa,cAAb,MAA4C;CAC1C;CACA;CACA;CACA;CACA;CACA;CACA;CAEA,SAAS;CACT,oBAAoB;CAEpB,YACE,IACA,IACA,MACA,MACA;EACA,MAAM,EAAC,MAAM,oBAAoB,gBAAgB,gBAAe;EAChE,KAAK,KAAK;EACV,KAAK,MAAM,GAAG,YAAY,aAAa,KAAK,EAAE;EAC9C,KAAKA,WAAW,QAAQ;EACxB,KAAKC,QAAQ;EACb,KAAKE,QAAQ;EACb,KAAK,SAAS,IAAI,aAAa,EAAE;EACjC,KAAKD,oBAAoB,qBACrB,IAAI,iBAAiB,KAAK,KAAK,kBAAkB,IACjD,KAAA;EAEJ,cAAmB,WAAY,KAAKE,SAAS,IAAK;CACpD;CAGA,WAAqB,CAAC;CACtB,UAAmC;EACjC,OAAO;CACT;CAGA,MAAM,QAAyB;EAC7B,KAAKJ,SAAS,IAAI,MAAM,MAAM,QAAQ;GACpC,IAAI,KAAKK,mBACP,IAAI,KAAK,GAAG,EAAE,KAAK,UAAU;QACxB,IAAI,KAAKD,QACd,IAAI,KAAK,IAAI;EAEjB,CAAC;EACD,KAAKJ,SAAS,IAAI,eAAe,EAAC,WAAU,QAAQ;GAClD,KAAKE,mBAAmB,YAAY,OAAO;GAC3C,IAAI,KAAKG,mBACP,IAAI,KAAK,GAAG,EAAE,KAAK,UAAU;QACxB,IAAI,KAAKD,QACd,IAAI,KAAK,IAAI;EAEjB,CAAC;EACD,MAAM,KAAKD,MAAM,KAAKH,QAAQ;EAC9B,MAAM,UAAU,MAAM,KAAKA,SAAS,OAAO;GACzC,MAAM;GACN,MAAM,KAAKC;EACb,CAAC;EACD,KAAK,IAAI,OAAO,GAAG,KAAK,GAAG,gBAAgB,SAAS;EACpD,KAAK,SAAS;EACd,OAAO;CACT;CAEA,MAAM,MAAqB;EACzB,MAAM,KAAK,MAAM;EACjB,MAAM,KAAK,OAAO,QAAQ;CAC5B;;;;;;CAOA,mBAAmB;EACjB,KAAK,IAAI,OAAO,GAAG,KAAK,GAAG,sCAAsC;EACjE,KAAKI,oBAAoB;CAC3B;;;;;;CAOA,gBAAgB;EACd,IAAI,KAAKL,SAAS,OAAO,WAAW;GAClC,KAAK,IAAI,OAAO,GAAG,KAAK,GAAG,sCAAsC;GACjE,KAAKA,SAAS,OAAO,MAAM;EAC7B;CACF;CAEA,MAAM,OAAsB;EAC1B,KAAK,IAAI,OAAO,GAAG,KAAK,GAAG,kCAAkC;EAC7D,KAAKE,mBAAmB,KAAK;EAC7B,KAAK,OAAO,KAAK,KAAK,GAAG;EACzB,MAAM,KAAKF,SAAS,MAAM;EAC1B,MAAM,KAAK,QAAQ;CACrB;AACF"}
|
|
@@ -102,8 +102,9 @@ export declare const STOP_TIMEOUT_MS = 60000;
|
|
|
102
102
|
/**
|
|
103
103
|
* Runs the specified services, stopping them on `SIGTERM` or `SIGINT` with
|
|
104
104
|
* an optional {@link SingletonService.drain drain()}, or stopping them
|
|
105
|
-
* without draining for `SIGQUIT
|
|
106
|
-
*
|
|
105
|
+
* without draining for `SIGQUIT`, or when the parent process disconnects.
|
|
106
|
+
* Repeated graceful signals are ignored. If a service stops (or throws) on its
|
|
107
|
+
* own, the remaining services are stopped without draining.
|
|
107
108
|
*
|
|
108
109
|
* @returns a Promise that settles once all services have stopped (or
|
|
109
110
|
* {@link STOP_TIMEOUT_MS} has elapsed), resolving or rejecting
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"life-cycle.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/services/life-cycle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,WAAW,CAAC;AAEnD,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,QAAQ,CAAC;AACzC,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AASjD,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,MAAM,EACZ,MAAM,uBAAuB,CAAC;AAG/B,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,cAAc,CAAC;AAEnD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,UAAU,GAAG,aAAa,GAAG,YAAY,CAAC;AAEtD,eAAO,MAAM,iBAAiB,YAAI,SAAS,EAAE,QAAQ,CAAU,CAAC;AAChE,eAAO,MAAM,iBAAiB,YAAI,SAAS,EAAE,SAAS,CAAU,CAAC;AA8CjE,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,MAAM,QAEzD;AAaD,eAAO,MAAM,8BAA8B,KAAK,CAAC;AAKjD,eAAO,MAAM,+BAA+B,KAAK,CAAC;AAElD;;;GAGG;AACH,qBAAa,cAAc;;IAWzB,YAAY,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,YAAY,EAyC7C;IAEK,QAAQ,kBAKb;IAuBD,aAAa,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,QAsB7D;IAKD,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAwBhE;IAED,YAAY,IAAI,MAAM,EAAE,CAEvB;IAEK,eAAe,kBAEpB;IAED,eAAe,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,QAGzC;CAqFF;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,mBAAoB,YAAW,gBAAgB;;IAC1D,QAAQ,CAAC,EAAE,0BAA0B;IAOrC,YAAY,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,EAe/C;IAED,GAAG,CAAC,CAAC,SAAS,UAAU,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,CAclD;IAED,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CASnB;IAEK,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAQ1B;CAoCF;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B;;;;OAIG;IACH,OAAO,EAAE,MAAM,CAAC;IAEhB;;;;OAIG;IACH,WAAW,EAAE,MAAM,IAAI,CAAC;CACzB,CAAC;AAEF;;;;;;;;;;GAUG;AACH,qBAAa,SAAS;;IASpB,YAAY,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,gBAAgB,EAYrE;IAED,OAAO,SAeN;CACF;AAED;;;GAGG;AACH,eAAO,MAAM,eAAe,QAAS,CAAC;AAEtC
|
|
1
|
+
{"version":3,"file":"life-cycle.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/services/life-cycle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,WAAW,CAAC;AAEnD,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,QAAQ,CAAC;AACzC,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AASjD,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,MAAM,EACZ,MAAM,uBAAuB,CAAC;AAG/B,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,cAAc,CAAC;AAEnD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,UAAU,GAAG,aAAa,GAAG,YAAY,CAAC;AAEtD,eAAO,MAAM,iBAAiB,YAAI,SAAS,EAAE,QAAQ,CAAU,CAAC;AAChE,eAAO,MAAM,iBAAiB,YAAI,SAAS,EAAE,SAAS,CAAU,CAAC;AA8CjE,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,MAAM,QAEzD;AAaD,eAAO,MAAM,8BAA8B,KAAK,CAAC;AAKjD,eAAO,MAAM,+BAA+B,KAAK,CAAC;AAElD;;;GAGG;AACH,qBAAa,cAAc;;IAWzB,YAAY,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,YAAY,EAyC7C;IAEK,QAAQ,kBAKb;IAuBD,aAAa,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,QAsB7D;IAKD,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAwBhE;IAED,YAAY,IAAI,MAAM,EAAE,CAEvB;IAEK,eAAe,kBAEpB;IAED,eAAe,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,QAGzC;CAqFF;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,mBAAoB,YAAW,gBAAgB;;IAC1D,QAAQ,CAAC,EAAE,0BAA0B;IAOrC,YAAY,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,EAe/C;IAED,GAAG,CAAC,CAAC,SAAS,UAAU,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,CAclD;IAED,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CASnB;IAEK,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAQ1B;CAoCF;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B;;;;OAIG;IACH,OAAO,EAAE,MAAM,CAAC;IAEhB;;;;OAIG;IACH,WAAW,EAAE,MAAM,IAAI,CAAC;CACzB,CAAC;AAEF;;;;;;;;;;GAUG;AACH,qBAAa,SAAS;;IASpB,YAAY,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,gBAAgB,EAYrE;IAED,OAAO,SAeN;CACF;AAED;;;GAGG;AACH,eAAO,MAAM,eAAe,QAAS,CAAC;AAEtC;;;;;;;;;;GAUG;AACH,wBAAsB,cAAc,CAClC,EAAE,EAAE,UAAU,EACd,MAAM,EAAE,YAAY,EACpB,GAAG,QAAQ,EAAE,gBAAgB,EAAE,GAC9B,OAAO,CAAC,IAAI,CAAC,CA2Ef;AA8BD,wBAAsB,SAAS,CAC7B,UAAU,EAAE,UAAU,GAAG,CAAC,MAAM,UAAU,CAAC,EAC3C,GAAG,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,iBAoCzB;AAED;;;;;;;;;;GAUG;AACH,qBAAa,gBAAgB;;IAQ3B,YAAY,EAAE,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,EAG/C;IAED,WAAW,CAAC,UAAU,EAAE,mBAAmB,QAgB1C;IAsCD,IAAI,SAKH;CACF"}
|
|
@@ -268,8 +268,9 @@ var STOP_TIMEOUT_MS = 6e4;
|
|
|
268
268
|
/**
|
|
269
269
|
* Runs the specified services, stopping them on `SIGTERM` or `SIGINT` with
|
|
270
270
|
* an optional {@link SingletonService.drain drain()}, or stopping them
|
|
271
|
-
* without draining for `SIGQUIT
|
|
272
|
-
*
|
|
271
|
+
* without draining for `SIGQUIT`, or when the parent process disconnects.
|
|
272
|
+
* Repeated graceful signals are ignored. If a service stops (or throws) on its
|
|
273
|
+
* own, the remaining services are stopped without draining.
|
|
273
274
|
*
|
|
274
275
|
* @returns a Promise that settles once all services have stopped (or
|
|
275
276
|
* {@link STOP_TIMEOUT_MS} has elapsed), resolving or rejecting
|
|
@@ -282,8 +283,15 @@ async function runUntilKilled(lc, parent, ...services) {
|
|
|
282
283
|
const allStopped = resolver();
|
|
283
284
|
let stopping;
|
|
284
285
|
const stopAll = (reason) => stopping ??= stopServices(lc, services, runs, stopped, reason).then(allStopped.resolve);
|
|
285
|
-
|
|
286
|
-
|
|
286
|
+
const GRACEFUL_SIGNALS = GRACEFUL_SHUTDOWN;
|
|
287
|
+
let draining = false;
|
|
288
|
+
const onSignal = async (signal) => {
|
|
289
|
+
if (GRACEFUL_SIGNALS.includes(signal)) {
|
|
290
|
+
if (draining || stopping) {
|
|
291
|
+
lc.info?.(`already shutting down. ignoring ${signal}`);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
draining = true;
|
|
287
295
|
const results = await Promise.allSettled(services.map(async (svc) => {
|
|
288
296
|
if (svc.drain) {
|
|
289
297
|
lc.info?.(`draining ${svc.constructor.name} ${svc.id} (${signal})`);
|
|
@@ -293,6 +301,11 @@ async function runUntilKilled(lc, parent, ...services) {
|
|
|
293
301
|
for (const result of results) if (result.status === "rejected") lc.warn?.(`error while draining (${signal})`, result.reason);
|
|
294
302
|
}
|
|
295
303
|
await stopAll(signal);
|
|
304
|
+
};
|
|
305
|
+
for (const signal of [...GRACEFUL_SHUTDOWN, ...FORCEFUL_SHUTDOWN]) parent.on(signal, () => void onSignal(signal));
|
|
306
|
+
parent.once("disconnect", () => {
|
|
307
|
+
lc.warn?.(`parent process disconnected. stopping without draining`);
|
|
308
|
+
stopAll("disconnect");
|
|
296
309
|
});
|
|
297
310
|
try {
|
|
298
311
|
const svc = await Promise.race([...runs.map((run, i) => run.then(() => services[i])), allStopped.promise]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"life-cycle.js","names":["#lc","#userFacing","#all","#exitImpl","#start","#ready","#startDrain","#kill","#exit","#drainStart","#runningState","#onExit","#initializing","#nextID","#parent","#procs","#done","#onClose","#signaled","#proc","#delayMs","#onRequested","#timer","#requested","#stopInterval","#lastHeartbeat","#checkIntervalTimer","#checkStopInterval","#checkImmediateTimer"],"sources":["../../../../../zero-cache/src/services/life-cycle.ts"],"sourcesContent":["import type {IncomingHttpHeaders} from 'node:http';\nimport {pid} from 'node:process';\nimport type {EventEmitter} from 'stream';\nimport type {LogContext} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport {AbortError} from '../../../shared/src/abort-error.ts';\nimport {logLastChanceSQLiteCorruptionDiagnostics} from '../db/sqlite-corruption.ts';\nimport {\n getOrCreateHistogram,\n LONG_DURATION_HISTOGRAM_BOUNDARIES_S,\n} from '../observability/metrics.ts';\nimport {ConfigurationError} from '../types/configuration-error.ts';\nimport {\n singleProcessMode,\n type Subprocess,\n type Worker,\n} from '../types/processes.ts';\nimport {orTimeout} from '../types/timeout.ts';\nimport {RunningState} from './running-state.ts';\nimport type {SingletonService} from './service.ts';\n\n/**\n * * `user-facing` workers serve external requests and are the first to\n * receive a `SIGTERM` or `SIGINT` signal for graceful shutdown.\n *\n * * `supporting` workers support `user-facing` workers and are sent\n * the `SIGTERM` signal only after all `user-facing` workers have\n * exited.\n *\n * For other kill signals, such as `SIGQUIT` and `SIGABRT`, all workers\n * are stopped without draining. `SIGQUIT` is used to represent an\n * intentional shutdown (for which draining is not beneficial), whereas\n * `SIGABRT` is used for unexpected process exits.\n *\n * A clean exit (i.e. code 0) of a worker of either type that is not the\n * result of a drain is treated as an intentional shutdown of the server.\n */\nexport type WorkerType = 'user-facing' | 'supporting';\n\nexport const GRACEFUL_SHUTDOWN = ['SIGTERM', 'SIGINT'] as const;\nexport const FORCEFUL_SHUTDOWN = ['SIGQUIT', 'SIGABRT'] as const;\n\ntype GracefulShutdownSignal = (typeof GRACEFUL_SHUTDOWN)[number];\n\nfunction workerStartupMetricName(name: string) {\n if (name === 'zero-cache') {\n return undefined;\n }\n if (name.startsWith('change-streamer')) {\n return 'change_streamer';\n }\n if (name.startsWith('replicator')) {\n if (name.includes('(backup)')) {\n return 'backup_replicator';\n }\n if (name.includes('(serving-copy)')) {\n return 'serving_copy_replicator';\n }\n if (name.includes('(serving)')) {\n return 'serving_replicator';\n }\n return 'other';\n }\n if (name.startsWith('syncer')) {\n return 'syncer';\n }\n if (name.startsWith('reaper')) {\n return 'reaper';\n }\n if (name.startsWith('shadow-syncer')) {\n return 'shadow_syncer';\n }\n if (name.startsWith('mutator')) {\n return 'mutator';\n }\n return 'other';\n}\n\nfunction startupDuration() {\n return getOrCreateHistogram('server', 'startup_duration', {\n description: 'Duration from starting zero-cache to its ready signal.',\n unit: 's',\n bucketBoundaries: LONG_DURATION_HISTOGRAM_BOUNDARIES_S,\n });\n}\n\nexport function recordStartupDurationMs(durationMs: number) {\n startupDuration().recordMs(durationMs, {component: 'dispatcher'});\n}\n\nfunction workerStartupDuration() {\n return getOrCreateHistogram('server', 'worker_startup_duration', {\n description: 'Duration from starting a worker to its ready signal.',\n unit: 's',\n bucketBoundaries: LONG_DURATION_HISTOGRAM_BOUNDARIES_S,\n });\n}\n\n// An internal error code used to indicate that a message has already been\n// logged at level ERRROR. When a process exits with this error code, the\n// parent process logs the exit at level WARN instead of ERROR.\nexport const UNHANDLED_EXCEPTION_ERROR_CODE = 13;\n\n// An internal error code used to indicate that the server should exit\n// without draining (e.g. due to a worker exiting cleanly outside of a drain,\n// such as for an auto reset), but the exit is otherwise intentional.\nexport const INTENTIONAL_SHUTDOWN_ERROR_CODE = 14;\n\n/**\n * Handles readiness, termination signals, and coordination of graceful\n * shutdown.\n */\nexport class ProcessManager {\n readonly #lc: LogContext;\n readonly #userFacing = new Set<Subprocess>();\n readonly #all = new Set<Subprocess>();\n readonly #exitImpl: (code: number) => never;\n readonly #start = Date.now();\n readonly #ready: Promise<void>[] = [];\n\n #runningState = new RunningState('process-manager');\n #drainStart = 0;\n\n constructor(lc: LogContext, proc: EventEmitter) {\n this.#lc = lc.withContext('component', 'process-manager');\n\n // Propagate `SIGTERM` and `SIGINT` to all user-facing workers,\n // initiating a graceful shutdown. The parent process will\n // exit once all user-facing workers have exited ...\n for (const signal of GRACEFUL_SHUTDOWN) {\n proc.on(signal, () => this.#startDrain(signal));\n }\n\n // ... which will result in sending `SIGTERM` to the remaining workers.\n proc.on('exit', code =>\n this.#kill(\n this.#all,\n code === 0\n ? 'SIGTERM' // graceful, drained shutdown\n : code === INTENTIONAL_SHUTDOWN_ERROR_CODE\n ? 'SIGQUIT' // intentional abort without drain\n : 'SIGABRT', // unintentional shutdown, alertable error\n ),\n );\n\n // For other (catchable) kill signals, exit with a non-zero error code\n // to send a `SIGQUIT` (intentional shutdown) or `SIGABRT` (unexpected\n // shutdown) to all workers. For these signals, workers are stopped\n // immediately without draining, since there is no merit to slowly draining\n // when supporting workers have stopped.\n //\n // The logic for handling these signals is in `runUntilKilled()`.\n for (const signal of FORCEFUL_SHUTDOWN) {\n proc.on(signal, () =>\n this.#exit(signal === 'SIGQUIT' ? INTENTIONAL_SHUTDOWN_ERROR_CODE : -1),\n );\n }\n\n this.#exitImpl = (code: number) => {\n if (singleProcessMode()) {\n return proc.emit('exit', code) as never; // For unit / integration tests.\n }\n process.exit(code);\n };\n }\n\n async shutdown() {\n if (this.#drainStart === 0) {\n this.#startDrain(GRACEFUL_SHUTDOWN[0]);\n }\n await this.#runningState.stopped();\n }\n\n #exit(code: number) {\n this.#lc.info?.('exiting with code', code);\n this.#runningState.stop(this.#lc);\n void this.#lc.flush().finally(() => this.#exitImpl(code));\n }\n\n #startDrain(signal: GracefulShutdownSignal) {\n if (this.#all.size === 0) {\n // Shutdown if a signal is received before any subprocesses are added.\n this.#lc.info?.(`exiting on ${signal}`);\n this.#exit(0);\n }\n this.#lc.info?.(`initiating drain (${signal})`);\n this.#drainStart = Date.now();\n if (this.#userFacing.size) {\n this.#kill(this.#userFacing, signal);\n } else {\n this.#kill(this.#all, signal);\n }\n }\n\n addSubprocess(proc: Subprocess, type: WorkerType, name: string) {\n if (type === 'user-facing') {\n this.#userFacing.add(proc);\n }\n this.#all.add(proc);\n\n let isOpen = true;\n proc.on('close', (code, signal) => {\n isOpen = false;\n this.#onExit(code, signal, null, type, name, proc);\n });\n\n // As per https://nodejs.org/api/child_process.html#event-error\n // 'error' events can happen when sending a message to a child process\n // fails. This is not really an error when the server is shutting down,\n // so log any post-close errors at 'warn'.\n proc.on('error', err =>\n this.#lc[!isOpen || this.#drainStart > 0 ? 'warn' : 'error']?.(\n `error from ${name} ${proc.pid}`,\n err,\n ),\n );\n }\n\n readonly #initializing = new Map<number, string>();\n #nextID = 0;\n\n addWorker(worker: Worker, type: WorkerType, name: string): Worker {\n this.addSubprocess(worker, type, name);\n\n const start = performance.now();\n const id = ++this.#nextID;\n this.#initializing.set(id, name);\n const {promise, resolve} = resolver();\n this.#ready.push(promise);\n\n worker.onceMessageType('ready', () => {\n const elapsed = performance.now() - start;\n const workerMetricName = workerStartupMetricName(name);\n if (workerMetricName) {\n workerStartupDuration().recordMs(elapsed, {\n worker: workerMetricName,\n type,\n });\n }\n this.#lc.debug?.(`${name} ready (${Date.now() - this.#start} ms)`);\n this.#initializing.delete(id);\n resolve();\n });\n\n return worker;\n }\n\n initializing(): string[] {\n return [...this.#initializing.values()];\n }\n\n async allWorkersReady() {\n await Promise.all(this.#ready);\n }\n\n logErrorAndExit(err: unknown, name: string) {\n // only accessible by the main (i.e. user-facing) process.\n this.#onExit(-1, null, err, 'user-facing', name, undefined);\n }\n\n #onExit(\n code: number,\n sig: NodeJS.Signals | null,\n err: unknown,\n type: WorkerType,\n name: string,\n worker: Subprocess | undefined,\n ) {\n // Remove the worker from maps to avoid attempting to send more signals to it.\n if (worker) {\n this.#userFacing.delete(worker);\n this.#all.delete(worker);\n }\n\n const pid = worker?.pid ?? process.pid;\n\n // A clean exit outside of a drain is an intentional shutdown, e.g. the\n // replication-manager shutting down for protocol-specific instructions\n // (like auto reset), or a configuration error. In this case, a special\n // error code is used to signal that the server should be shut down\n // without draining, but it is otherwise not considered an\n // unexpected/alertable error. The same applies to a supporting worker\n // that exits while user-facing workers are still draining.\n if (\n code === 0 &&\n (this.#drainStart === 0 ||\n (type === 'supporting' && this.#userFacing.size > 0))\n ) {\n code = INTENTIONAL_SHUTDOWN_ERROR_CODE;\n }\n\n if (type === 'supporting') {\n const log =\n code === 0 || code === INTENTIONAL_SHUTDOWN_ERROR_CODE\n ? 'info'\n : 'warn';\n this.#lc[log]?.(`${name} (${pid}) exited with code (${code})`, err ?? '');\n return this.#exit(code);\n }\n\n const log =\n code === 0 || code === INTENTIONAL_SHUTDOWN_ERROR_CODE\n ? 'info'\n : this.#drainStart > 0 || code === UNHANDLED_EXCEPTION_ERROR_CODE\n ? 'warn'\n : 'error';\n this.#lc[log]?.(\n sig\n ? `${name} (${pid}) killed with (${sig})`\n : `${name} (${pid}) exited with code (${code})`,\n err ?? '',\n );\n\n // user-facing workers exited or finished draining.\n if (this.#userFacing.size === 0) {\n this.#lc.info?.(\n this.#drainStart\n ? `all user-facing workers drained (${\n Date.now() - this.#drainStart\n } ms)`\n : `all user-facing workers exited`,\n );\n return this.#exit(this.#drainStart ? 0 : code);\n }\n\n if (this.#drainStart === 0) {\n // If a user-facing worker exits without receiving a drain signal,\n // shutdown the server.\n return this.#exit(code);\n }\n\n return undefined;\n }\n\n #kill(workers: Iterable<Subprocess>, signal: NodeJS.Signals) {\n for (const worker of workers) {\n try {\n worker.kill(signal);\n } catch (e) {\n this.#lc.error?.(e);\n }\n }\n }\n}\n\n/**\n * Runs the supporting subprocesses of a worker (e.g. the litestream process\n * of the backup-replicator) as a {@link SingletonService}.\n *\n * Unlike a {@link ProcessManager}, it does not forward `SIGTERM` or `SIGINT`\n * to its subprocesses. Instead, {@link runUntilKilled} stops it along with the\n * worker's other services, i.e. after they have drained, so that the\n * subprocesses keep supporting them until then. Forceful signals (`SIGQUIT`\n * and `SIGABRT`), on the other hand, are forwarded immediately once the\n * service is running, and the subprocesses are killed if the worker exits\n * before they are stopped.\n *\n * `run()` settles when a subprocess exits before {@link stop()} is called,\n * which stops the worker: a clean exit resolves (and the parent in turn treats\n * the worker's clean exit outside of a drain as an intentional shutdown),\n * while any other exit rejects.\n */\nexport class SupportingProcesses implements SingletonService {\n readonly id = 'supporting-processes';\n readonly #lc: LogContext;\n readonly #parent: EventEmitter;\n readonly #procs = new Map<Subprocess, string>();\n readonly #done = resolver<void>();\n #signaled: NodeJS.Signals | undefined;\n\n constructor(lc: LogContext, parent: EventEmitter) {\n this.#lc = lc.withContext('component', 'supporting-processes');\n this.#parent = parent;\n // run() may never be called if the worker fails before running services.\n this.#done.promise.catch(() => {});\n\n parent.on('exit', code =>\n this.#kill(\n code === 0\n ? 'SIGTERM'\n : code === INTENTIONAL_SHUTDOWN_ERROR_CODE\n ? 'SIGQUIT'\n : 'SIGABRT',\n ),\n );\n }\n\n add<P extends Subprocess>(proc: P, name: string): P {\n this.#procs.set(proc, name);\n proc.on('close', (code: number | null, sig: NodeJS.Signals | null) =>\n this.#onClose(proc, name, code, sig),\n );\n // As with the ProcessManager, 'error' events after a signal (e.g. failing\n // to send a message to an exiting process) are logged at 'warn'.\n proc.on('error', err =>\n this.#lc[this.#signaled ? 'warn' : 'error']?.(\n `error from ${name} ${proc.pid}`,\n err,\n ),\n );\n return proc;\n }\n\n run(): Promise<void> {\n // Signal listeners are only installed when running, i.e. alongside those\n // of runUntilKilled(). Installing them any earlier would consume signals\n // received during worker initialization, which would otherwise terminate\n // the worker (or be handled by the initialization logic).\n for (const signal of FORCEFUL_SHUTDOWN) {\n this.#parent.on(signal, () => this.#kill(signal));\n }\n return this.#done.promise;\n }\n\n async stop(): Promise<void> {\n if (!this.#signaled) {\n this.#kill('SIGTERM');\n }\n if (this.#procs.size === 0) {\n this.#done.resolve();\n }\n await this.#done.promise.catch(() => {});\n }\n\n #onClose(\n proc: Subprocess,\n name: string,\n code: number | null,\n sig: NodeJS.Signals | null,\n ) {\n this.#procs.delete(proc);\n const exit = sig\n ? `${name} (${proc.pid}) killed with (${sig})`\n : `${name} (${proc.pid}) exited with code (${code})`;\n\n if (this.#signaled) {\n this.#lc.info?.(exit);\n if (this.#procs.size === 0) {\n this.#done.resolve();\n }\n } else if (code === 0 || code === INTENTIONAL_SHUTDOWN_ERROR_CODE) {\n this.#lc.info?.(exit);\n this.#done.resolve();\n } else {\n this.#done.reject(new Error(exit));\n }\n }\n\n #kill(signal: NodeJS.Signals) {\n this.#signaled = signal;\n for (const proc of this.#procs.keys()) {\n try {\n proc.kill(signal);\n } catch (e) {\n this.#lc.error?.(e);\n }\n }\n }\n}\n\nexport type SelfDrainOptions = {\n /**\n * The time after a self drain is requested at which the server sends itself\n * a `SIGTERM`, unless one was received sooner (e.g. from the container\n * runner).\n */\n delayMs: number;\n\n /**\n * Called when a self drain is requested. This should fail health checks\n * (both readiness and liveness) so that the container runner stops routing\n * to the server and initiates its normal shutdown process.\n */\n onRequested: () => void;\n};\n\n/**\n * Coordinates a drain that is initiated by the server itself (e.g. by a\n * view-syncer that has been granted permission to restore a new replica),\n * as opposed to one initiated by the container runner with a `SIGTERM`.\n *\n * Health checks start failing immediately, upon which an orchestrator such\n * as Kubernetes stops routing to the server and shuts it down as usual\n * (i.e. running any pre-stop hooks before sending `SIGTERM`). As a backstop,\n * and for environments without liveness checks, the server sends itself a\n * `SIGTERM` after the configured delay.\n */\nexport class SelfDrain {\n readonly #lc: LogContext;\n readonly #proc: EventEmitter;\n readonly #delayMs: number;\n readonly #onRequested: () => void;\n #requested = false;\n #signaled = false;\n #timer: ReturnType<typeof setTimeout> | undefined;\n\n constructor(lc: LogContext, proc: EventEmitter, opts: SelfDrainOptions) {\n this.#lc = lc.withContext('component', 'self-drain');\n this.#proc = proc;\n this.#delayMs = opts.delayMs;\n this.#onRequested = opts.onRequested;\n\n for (const signal of GRACEFUL_SHUTDOWN) {\n proc.once(signal, () => {\n this.#signaled = true;\n clearTimeout(this.#timer);\n });\n }\n }\n\n request() {\n if (this.#requested || this.#signaled) {\n return; // Already draining.\n }\n this.#requested = true;\n this.#lc.info?.(\n `self drain requested. sending SIGTERM in ${this.#delayMs} ms`,\n );\n this.#onRequested();\n this.#timer = setTimeout(() => {\n if (!this.#signaled) {\n this.#lc.info?.(`initiating self drain`);\n this.#proc.emit('SIGTERM', 'SIGTERM');\n }\n }, this.#delayMs);\n }\n}\n\n/**\n * The maximum time that {@link runUntilKilled} waits for its services to stop\n * (i.e. for their `run()` to settle) after stopping them.\n */\nexport const STOP_TIMEOUT_MS = 60_000;\n\n/**\n * Runs the specified services, stopping them on `SIGTERM` or `SIGINT` with\n * an optional {@link SingletonService.drain drain()}, or stopping them\n * without draining for `SIGQUIT`. If a service stops (or throws) on its own,\n * the remaining services are stopped without draining.\n *\n * @returns a Promise that settles once all services have stopped (or\n * {@link STOP_TIMEOUT_MS} has elapsed), resolving or rejecting\n * with the outcome of the first service to stop.\n */\nexport async function runUntilKilled(\n lc: LogContext,\n parent: EventEmitter,\n ...services: SingletonService[]\n): Promise<void> {\n if (services.length === 0) {\n return;\n }\n const stopped = new Set<SingletonService>();\n const runs = services.map(svc => svc.run().finally(() => stopped.add(svc)));\n\n const allStopped = resolver<void>();\n let stopping: Promise<void> | undefined;\n const stopAll = (reason: string) =>\n (stopping ??= stopServices(lc, services, runs, stopped, reason).then(\n allStopped.resolve,\n ));\n\n for (const signal of [...GRACEFUL_SHUTDOWN, ...FORCEFUL_SHUTDOWN]) {\n parent.once(signal, async () => {\n const GRACEFUL_SIGNALS = GRACEFUL_SHUTDOWN as readonly NodeJS.Signals[];\n\n if (GRACEFUL_SIGNALS.includes(signal)) {\n // Drain all services before stopping any of them. Stopping a service\n // resolves its run() and thus ends runUntilKilled(), so a service\n // without a drain() must not be stopped while others are draining.\n const results = await Promise.allSettled(\n services.map(async svc => {\n if (svc.drain) {\n lc.info?.(\n `draining ${svc.constructor.name} ${svc.id} (${signal})`,\n );\n await svc.drain();\n }\n }),\n );\n for (const result of results) {\n if (result.status === 'rejected') {\n lc.warn?.(`error while draining (${signal})`, result.reason);\n }\n }\n }\n await stopAll(signal);\n });\n }\n\n try {\n // Run all services and resolve when any of them stops, or when stopping\n // them (after a signal) has timed out.\n const svc = await Promise.race([\n ...runs.map((run, i) => run.then(() => services[i])),\n allStopped.promise,\n ]);\n if (svc) {\n lc.info?.(`${svc.constructor.name} (${svc.id}) stopped`);\n }\n } catch (e) {\n lc.error?.(`exiting on error`, e);\n throw e;\n } finally {\n await stopAll('stopped');\n }\n}\n\nasync function stopServices(\n lc: LogContext,\n services: SingletonService[],\n runs: Promise<void>[],\n stopped: Set<SingletonService>,\n reason: string,\n) {\n for (const svc of services) {\n if (!stopped.has(svc)) {\n lc.info?.(`stopping ${svc.constructor.name} ${svc.id} (${reason})`);\n void svc\n .stop()\n .catch(e =>\n lc.warn?.(`error stopping ${svc.constructor.name} ${svc.id}`, e),\n );\n }\n }\n if (\n (await orTimeout(Promise.allSettled(runs), STOP_TIMEOUT_MS)) === 'timed-out'\n ) {\n const running = services.filter(svc => !stopped.has(svc));\n lc.warn?.(\n `services did not stop within ${STOP_TIMEOUT_MS} ms: ` +\n running.map(svc => `${svc.constructor.name} ${svc.id}`).join(', '),\n );\n }\n}\n\nexport async function exitAfter(\n logContext: LogContext | (() => LogContext),\n run: () => Promise<void>,\n) {\n const getLogContext =\n typeof logContext === 'function' ? logContext : () => logContext;\n try {\n await run();\n const lc = getLogContext();\n lc.info?.(`pid ${pid} exiting normally`);\n process.exit(0);\n } catch (e) {\n const lc = getLogContext();\n if (e instanceof ConfigurationError) {\n lc.error?.(`exiting with configuration error: ${String(e)}`, e);\n process.exit(0);\n }\n if (e instanceof AbortError) {\n // AbortErrors signal an intentional shutdown or restart (e.g. SIGTERM\n // during a rollout, AutoResetSignal) and should not raise alerts. The\n // non-zero exit code is kept so that restart semantics are unchanged.\n lc.warn?.(`exiting after abort: ${String(e)}`, e);\n } else {\n lc.error?.(`exiting with error: ${String(e)}`, e);\n }\n try {\n logLastChanceSQLiteCorruptionDiagnostics(lc, e);\n } catch (diagnosticError) {\n lc.error?.('SQLite corruption last-chance diagnostic failed', {\n error: diagnosticError,\n });\n }\n try {\n await lc.flush();\n } finally {\n process.exit(-1);\n }\n }\n}\n\n/**\n * The HeartbeatMonitor monitors the cadence heartbeats (e.g. \"/keepalive\"\n * health checks made to HttpServices) that signal that the server\n * should continue processing requests. When a configurable `stopInterval`\n * elapses without receiving these heartbeats, the monitor initiates a\n * graceful shutdown of the server. This works with common load balancing\n * frameworks such as AWS Elastic Load Balancing.\n *\n * The HeartbeatMonitor is **opt-in** in that it only kicks in after it\n * starts receiving keepalives.\n */\nexport class HeartbeatMonitor {\n readonly #stopInterval: number;\n\n #lc: LogContext;\n #checkIntervalTimer: NodeJS.Timeout | undefined;\n #checkImmediateTimer: NodeJS.Immediate | undefined;\n #lastHeartbeat = 0;\n\n constructor(lc: LogContext, stopInterval: number) {\n this.#lc = lc;\n this.#stopInterval = stopInterval;\n }\n\n onHeartbeat(reqHeaders: IncomingHttpHeaders) {\n this.#lastHeartbeat = Date.now();\n if (this.#checkIntervalTimer === undefined) {\n this.#lc.info?.(\n `starting heartbeat monitor at ${\n this.#stopInterval / 1000\n } second interval`,\n reqHeaders,\n );\n // e.g. check every 5 seconds to see if it's been over 20 seconds\n // since the last heartbeat.\n this.#checkIntervalTimer = setInterval(\n this.#checkStopInterval,\n this.#stopInterval / 4,\n );\n }\n }\n\n #checkStopInterval = () => {\n // In the Node.js event loop, timers like setInterval and setTimeout\n // run *before* I/O events coming from network sockets or file reads/writes.\n // When this process gets starved of CPU resources for long periods of time,\n // for example when other processes are monopolizing all available cores,\n // pathological behavior can emerge:\n // - keepalive network request comes in, but is queued in Node internals waiting\n // for time on the event loop\n // - CPU is starved/monopolized by other processes for longer than the time\n // configured via this.#stopInterval\n // - When CPU becomes available and the event loop wakes up, this stop interval\n // check is run *before* the keepalive request is processed. The value of\n // this.#lastHeartbeat is now very stale, and erroneously triggers a shutdown\n // even though keepalive requests were about to be processed and update\n // this.#lastHeartbeat. Downtime ensues.\n //\n // To avoid this, we push the check out to a phase of the event loop *after*\n // I/O events are processed, using setImmediate():\n // https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick#setimmediate-vs-settimeout\n //\n // This ensures we see a value for this.#lastHeartbeat that reflects\n // any keepalive requests that came in during the current event loop turn.\n this.#checkImmediateTimer = setImmediate(() => {\n this.#checkImmediateTimer = undefined;\n const timeSinceLastHeartbeat = Date.now() - this.#lastHeartbeat;\n if (timeSinceLastHeartbeat >= this.#stopInterval) {\n this.#lc.info?.(\n `last heartbeat received ${\n timeSinceLastHeartbeat / 1000\n } seconds ago. draining.`,\n );\n process.kill(process.pid, GRACEFUL_SHUTDOWN[0]);\n }\n });\n };\n\n stop() {\n clearTimeout(this.#checkIntervalTimer);\n if (this.#checkImmediateTimer) {\n clearImmediate(this.#checkImmediateTimer);\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"life-cycle.js","names":["#lc","#userFacing","#all","#exitImpl","#start","#ready","#startDrain","#kill","#exit","#drainStart","#runningState","#onExit","#initializing","#nextID","#parent","#procs","#done","#onClose","#signaled","#proc","#delayMs","#onRequested","#timer","#requested","#stopInterval","#lastHeartbeat","#checkIntervalTimer","#checkStopInterval","#checkImmediateTimer"],"sources":["../../../../../zero-cache/src/services/life-cycle.ts"],"sourcesContent":["import type {IncomingHttpHeaders} from 'node:http';\nimport {pid} from 'node:process';\nimport type {EventEmitter} from 'stream';\nimport type {LogContext} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport {AbortError} from '../../../shared/src/abort-error.ts';\nimport {logLastChanceSQLiteCorruptionDiagnostics} from '../db/sqlite-corruption.ts';\nimport {\n getOrCreateHistogram,\n LONG_DURATION_HISTOGRAM_BOUNDARIES_S,\n} from '../observability/metrics.ts';\nimport {ConfigurationError} from '../types/configuration-error.ts';\nimport {\n singleProcessMode,\n type Subprocess,\n type Worker,\n} from '../types/processes.ts';\nimport {orTimeout} from '../types/timeout.ts';\nimport {RunningState} from './running-state.ts';\nimport type {SingletonService} from './service.ts';\n\n/**\n * * `user-facing` workers serve external requests and are the first to\n * receive a `SIGTERM` or `SIGINT` signal for graceful shutdown.\n *\n * * `supporting` workers support `user-facing` workers and are sent\n * the `SIGTERM` signal only after all `user-facing` workers have\n * exited.\n *\n * For other kill signals, such as `SIGQUIT` and `SIGABRT`, all workers\n * are stopped without draining. `SIGQUIT` is used to represent an\n * intentional shutdown (for which draining is not beneficial), whereas\n * `SIGABRT` is used for unexpected process exits.\n *\n * A clean exit (i.e. code 0) of a worker of either type that is not the\n * result of a drain is treated as an intentional shutdown of the server.\n */\nexport type WorkerType = 'user-facing' | 'supporting';\n\nexport const GRACEFUL_SHUTDOWN = ['SIGTERM', 'SIGINT'] as const;\nexport const FORCEFUL_SHUTDOWN = ['SIGQUIT', 'SIGABRT'] as const;\n\ntype GracefulShutdownSignal = (typeof GRACEFUL_SHUTDOWN)[number];\n\nfunction workerStartupMetricName(name: string) {\n if (name === 'zero-cache') {\n return undefined;\n }\n if (name.startsWith('change-streamer')) {\n return 'change_streamer';\n }\n if (name.startsWith('replicator')) {\n if (name.includes('(backup)')) {\n return 'backup_replicator';\n }\n if (name.includes('(serving-copy)')) {\n return 'serving_copy_replicator';\n }\n if (name.includes('(serving)')) {\n return 'serving_replicator';\n }\n return 'other';\n }\n if (name.startsWith('syncer')) {\n return 'syncer';\n }\n if (name.startsWith('reaper')) {\n return 'reaper';\n }\n if (name.startsWith('shadow-syncer')) {\n return 'shadow_syncer';\n }\n if (name.startsWith('mutator')) {\n return 'mutator';\n }\n return 'other';\n}\n\nfunction startupDuration() {\n return getOrCreateHistogram('server', 'startup_duration', {\n description: 'Duration from starting zero-cache to its ready signal.',\n unit: 's',\n bucketBoundaries: LONG_DURATION_HISTOGRAM_BOUNDARIES_S,\n });\n}\n\nexport function recordStartupDurationMs(durationMs: number) {\n startupDuration().recordMs(durationMs, {component: 'dispatcher'});\n}\n\nfunction workerStartupDuration() {\n return getOrCreateHistogram('server', 'worker_startup_duration', {\n description: 'Duration from starting a worker to its ready signal.',\n unit: 's',\n bucketBoundaries: LONG_DURATION_HISTOGRAM_BOUNDARIES_S,\n });\n}\n\n// An internal error code used to indicate that a message has already been\n// logged at level ERRROR. When a process exits with this error code, the\n// parent process logs the exit at level WARN instead of ERROR.\nexport const UNHANDLED_EXCEPTION_ERROR_CODE = 13;\n\n// An internal error code used to indicate that the server should exit\n// without draining (e.g. due to a worker exiting cleanly outside of a drain,\n// such as for an auto reset), but the exit is otherwise intentional.\nexport const INTENTIONAL_SHUTDOWN_ERROR_CODE = 14;\n\n/**\n * Handles readiness, termination signals, and coordination of graceful\n * shutdown.\n */\nexport class ProcessManager {\n readonly #lc: LogContext;\n readonly #userFacing = new Set<Subprocess>();\n readonly #all = new Set<Subprocess>();\n readonly #exitImpl: (code: number) => never;\n readonly #start = Date.now();\n readonly #ready: Promise<void>[] = [];\n\n #runningState = new RunningState('process-manager');\n #drainStart = 0;\n\n constructor(lc: LogContext, proc: EventEmitter) {\n this.#lc = lc.withContext('component', 'process-manager');\n\n // Propagate `SIGTERM` and `SIGINT` to all user-facing workers,\n // initiating a graceful shutdown. The parent process will\n // exit once all user-facing workers have exited ...\n for (const signal of GRACEFUL_SHUTDOWN) {\n proc.on(signal, () => this.#startDrain(signal));\n }\n\n // ... which will result in sending `SIGTERM` to the remaining workers.\n proc.on('exit', code =>\n this.#kill(\n this.#all,\n code === 0\n ? 'SIGTERM' // graceful, drained shutdown\n : code === INTENTIONAL_SHUTDOWN_ERROR_CODE\n ? 'SIGQUIT' // intentional abort without drain\n : 'SIGABRT', // unintentional shutdown, alertable error\n ),\n );\n\n // For other (catchable) kill signals, exit with a non-zero error code\n // to send a `SIGQUIT` (intentional shutdown) or `SIGABRT` (unexpected\n // shutdown) to all workers. For these signals, workers are stopped\n // immediately without draining, since there is no merit to slowly draining\n // when supporting workers have stopped.\n //\n // The logic for handling these signals is in `runUntilKilled()`.\n for (const signal of FORCEFUL_SHUTDOWN) {\n proc.on(signal, () =>\n this.#exit(signal === 'SIGQUIT' ? INTENTIONAL_SHUTDOWN_ERROR_CODE : -1),\n );\n }\n\n this.#exitImpl = (code: number) => {\n if (singleProcessMode()) {\n return proc.emit('exit', code) as never; // For unit / integration tests.\n }\n process.exit(code);\n };\n }\n\n async shutdown() {\n if (this.#drainStart === 0) {\n this.#startDrain(GRACEFUL_SHUTDOWN[0]);\n }\n await this.#runningState.stopped();\n }\n\n #exit(code: number) {\n this.#lc.info?.('exiting with code', code);\n this.#runningState.stop(this.#lc);\n void this.#lc.flush().finally(() => this.#exitImpl(code));\n }\n\n #startDrain(signal: GracefulShutdownSignal) {\n if (this.#all.size === 0) {\n // Shutdown if a signal is received before any subprocesses are added.\n this.#lc.info?.(`exiting on ${signal}`);\n this.#exit(0);\n }\n this.#lc.info?.(`initiating drain (${signal})`);\n this.#drainStart = Date.now();\n if (this.#userFacing.size) {\n this.#kill(this.#userFacing, signal);\n } else {\n this.#kill(this.#all, signal);\n }\n }\n\n addSubprocess(proc: Subprocess, type: WorkerType, name: string) {\n if (type === 'user-facing') {\n this.#userFacing.add(proc);\n }\n this.#all.add(proc);\n\n let isOpen = true;\n proc.on('close', (code, signal) => {\n isOpen = false;\n this.#onExit(code, signal, null, type, name, proc);\n });\n\n // As per https://nodejs.org/api/child_process.html#event-error\n // 'error' events can happen when sending a message to a child process\n // fails. This is not really an error when the server is shutting down,\n // so log any post-close errors at 'warn'.\n proc.on('error', err =>\n this.#lc[!isOpen || this.#drainStart > 0 ? 'warn' : 'error']?.(\n `error from ${name} ${proc.pid}`,\n err,\n ),\n );\n }\n\n readonly #initializing = new Map<number, string>();\n #nextID = 0;\n\n addWorker(worker: Worker, type: WorkerType, name: string): Worker {\n this.addSubprocess(worker, type, name);\n\n const start = performance.now();\n const id = ++this.#nextID;\n this.#initializing.set(id, name);\n const {promise, resolve} = resolver();\n this.#ready.push(promise);\n\n worker.onceMessageType('ready', () => {\n const elapsed = performance.now() - start;\n const workerMetricName = workerStartupMetricName(name);\n if (workerMetricName) {\n workerStartupDuration().recordMs(elapsed, {\n worker: workerMetricName,\n type,\n });\n }\n this.#lc.debug?.(`${name} ready (${Date.now() - this.#start} ms)`);\n this.#initializing.delete(id);\n resolve();\n });\n\n return worker;\n }\n\n initializing(): string[] {\n return [...this.#initializing.values()];\n }\n\n async allWorkersReady() {\n await Promise.all(this.#ready);\n }\n\n logErrorAndExit(err: unknown, name: string) {\n // only accessible by the main (i.e. user-facing) process.\n this.#onExit(-1, null, err, 'user-facing', name, undefined);\n }\n\n #onExit(\n code: number,\n sig: NodeJS.Signals | null,\n err: unknown,\n type: WorkerType,\n name: string,\n worker: Subprocess | undefined,\n ) {\n // Remove the worker from maps to avoid attempting to send more signals to it.\n if (worker) {\n this.#userFacing.delete(worker);\n this.#all.delete(worker);\n }\n\n const pid = worker?.pid ?? process.pid;\n\n // A clean exit outside of a drain is an intentional shutdown, e.g. the\n // replication-manager shutting down for protocol-specific instructions\n // (like auto reset), or a configuration error. In this case, a special\n // error code is used to signal that the server should be shut down\n // without draining, but it is otherwise not considered an\n // unexpected/alertable error. The same applies to a supporting worker\n // that exits while user-facing workers are still draining.\n if (\n code === 0 &&\n (this.#drainStart === 0 ||\n (type === 'supporting' && this.#userFacing.size > 0))\n ) {\n code = INTENTIONAL_SHUTDOWN_ERROR_CODE;\n }\n\n if (type === 'supporting') {\n const log =\n code === 0 || code === INTENTIONAL_SHUTDOWN_ERROR_CODE\n ? 'info'\n : 'warn';\n this.#lc[log]?.(`${name} (${pid}) exited with code (${code})`, err ?? '');\n return this.#exit(code);\n }\n\n const log =\n code === 0 || code === INTENTIONAL_SHUTDOWN_ERROR_CODE\n ? 'info'\n : this.#drainStart > 0 || code === UNHANDLED_EXCEPTION_ERROR_CODE\n ? 'warn'\n : 'error';\n this.#lc[log]?.(\n sig\n ? `${name} (${pid}) killed with (${sig})`\n : `${name} (${pid}) exited with code (${code})`,\n err ?? '',\n );\n\n // user-facing workers exited or finished draining.\n if (this.#userFacing.size === 0) {\n this.#lc.info?.(\n this.#drainStart\n ? `all user-facing workers drained (${\n Date.now() - this.#drainStart\n } ms)`\n : `all user-facing workers exited`,\n );\n return this.#exit(this.#drainStart ? 0 : code);\n }\n\n if (this.#drainStart === 0) {\n // If a user-facing worker exits without receiving a drain signal,\n // shutdown the server.\n return this.#exit(code);\n }\n\n return undefined;\n }\n\n #kill(workers: Iterable<Subprocess>, signal: NodeJS.Signals) {\n for (const worker of workers) {\n try {\n worker.kill(signal);\n } catch (e) {\n this.#lc.error?.(e);\n }\n }\n }\n}\n\n/**\n * Runs the supporting subprocesses of a worker (e.g. the litestream process\n * of the backup-replicator) as a {@link SingletonService}.\n *\n * Unlike a {@link ProcessManager}, it does not forward `SIGTERM` or `SIGINT`\n * to its subprocesses. Instead, {@link runUntilKilled} stops it along with the\n * worker's other services, i.e. after they have drained, so that the\n * subprocesses keep supporting them until then. Forceful signals (`SIGQUIT`\n * and `SIGABRT`), on the other hand, are forwarded immediately once the\n * service is running, and the subprocesses are killed if the worker exits\n * before they are stopped.\n *\n * `run()` settles when a subprocess exits before {@link stop()} is called,\n * which stops the worker: a clean exit resolves (and the parent in turn treats\n * the worker's clean exit outside of a drain as an intentional shutdown),\n * while any other exit rejects.\n */\nexport class SupportingProcesses implements SingletonService {\n readonly id = 'supporting-processes';\n readonly #lc: LogContext;\n readonly #parent: EventEmitter;\n readonly #procs = new Map<Subprocess, string>();\n readonly #done = resolver<void>();\n #signaled: NodeJS.Signals | undefined;\n\n constructor(lc: LogContext, parent: EventEmitter) {\n this.#lc = lc.withContext('component', 'supporting-processes');\n this.#parent = parent;\n // run() may never be called if the worker fails before running services.\n this.#done.promise.catch(() => {});\n\n parent.on('exit', code =>\n this.#kill(\n code === 0\n ? 'SIGTERM'\n : code === INTENTIONAL_SHUTDOWN_ERROR_CODE\n ? 'SIGQUIT'\n : 'SIGABRT',\n ),\n );\n }\n\n add<P extends Subprocess>(proc: P, name: string): P {\n this.#procs.set(proc, name);\n proc.on('close', (code: number | null, sig: NodeJS.Signals | null) =>\n this.#onClose(proc, name, code, sig),\n );\n // As with the ProcessManager, 'error' events after a signal (e.g. failing\n // to send a message to an exiting process) are logged at 'warn'.\n proc.on('error', err =>\n this.#lc[this.#signaled ? 'warn' : 'error']?.(\n `error from ${name} ${proc.pid}`,\n err,\n ),\n );\n return proc;\n }\n\n run(): Promise<void> {\n // Signal listeners are only installed when running, i.e. alongside those\n // of runUntilKilled(). Installing them any earlier would consume signals\n // received during worker initialization, which would otherwise terminate\n // the worker (or be handled by the initialization logic).\n for (const signal of FORCEFUL_SHUTDOWN) {\n this.#parent.on(signal, () => this.#kill(signal));\n }\n return this.#done.promise;\n }\n\n async stop(): Promise<void> {\n if (!this.#signaled) {\n this.#kill('SIGTERM');\n }\n if (this.#procs.size === 0) {\n this.#done.resolve();\n }\n await this.#done.promise.catch(() => {});\n }\n\n #onClose(\n proc: Subprocess,\n name: string,\n code: number | null,\n sig: NodeJS.Signals | null,\n ) {\n this.#procs.delete(proc);\n const exit = sig\n ? `${name} (${proc.pid}) killed with (${sig})`\n : `${name} (${proc.pid}) exited with code (${code})`;\n\n if (this.#signaled) {\n this.#lc.info?.(exit);\n if (this.#procs.size === 0) {\n this.#done.resolve();\n }\n } else if (code === 0 || code === INTENTIONAL_SHUTDOWN_ERROR_CODE) {\n this.#lc.info?.(exit);\n this.#done.resolve();\n } else {\n this.#done.reject(new Error(exit));\n }\n }\n\n #kill(signal: NodeJS.Signals) {\n this.#signaled = signal;\n for (const proc of this.#procs.keys()) {\n try {\n proc.kill(signal);\n } catch (e) {\n this.#lc.error?.(e);\n }\n }\n }\n}\n\nexport type SelfDrainOptions = {\n /**\n * The time after a self drain is requested at which the server sends itself\n * a `SIGTERM`, unless one was received sooner (e.g. from the container\n * runner).\n */\n delayMs: number;\n\n /**\n * Called when a self drain is requested. This should fail health checks\n * (both readiness and liveness) so that the container runner stops routing\n * to the server and initiates its normal shutdown process.\n */\n onRequested: () => void;\n};\n\n/**\n * Coordinates a drain that is initiated by the server itself (e.g. by a\n * view-syncer that has been granted permission to restore a new replica),\n * as opposed to one initiated by the container runner with a `SIGTERM`.\n *\n * Health checks start failing immediately, upon which an orchestrator such\n * as Kubernetes stops routing to the server and shuts it down as usual\n * (i.e. running any pre-stop hooks before sending `SIGTERM`). As a backstop,\n * and for environments without liveness checks, the server sends itself a\n * `SIGTERM` after the configured delay.\n */\nexport class SelfDrain {\n readonly #lc: LogContext;\n readonly #proc: EventEmitter;\n readonly #delayMs: number;\n readonly #onRequested: () => void;\n #requested = false;\n #signaled = false;\n #timer: ReturnType<typeof setTimeout> | undefined;\n\n constructor(lc: LogContext, proc: EventEmitter, opts: SelfDrainOptions) {\n this.#lc = lc.withContext('component', 'self-drain');\n this.#proc = proc;\n this.#delayMs = opts.delayMs;\n this.#onRequested = opts.onRequested;\n\n for (const signal of GRACEFUL_SHUTDOWN) {\n proc.once(signal, () => {\n this.#signaled = true;\n clearTimeout(this.#timer);\n });\n }\n }\n\n request() {\n if (this.#requested || this.#signaled) {\n return; // Already draining.\n }\n this.#requested = true;\n this.#lc.info?.(\n `self drain requested. sending SIGTERM in ${this.#delayMs} ms`,\n );\n this.#onRequested();\n this.#timer = setTimeout(() => {\n if (!this.#signaled) {\n this.#lc.info?.(`initiating self drain`);\n this.#proc.emit('SIGTERM', 'SIGTERM');\n }\n }, this.#delayMs);\n }\n}\n\n/**\n * The maximum time that {@link runUntilKilled} waits for its services to stop\n * (i.e. for their `run()` to settle) after stopping them.\n */\nexport const STOP_TIMEOUT_MS = 60_000;\n\n/**\n * Runs the specified services, stopping them on `SIGTERM` or `SIGINT` with\n * an optional {@link SingletonService.drain drain()}, or stopping them\n * without draining for `SIGQUIT`, or when the parent process disconnects.\n * Repeated graceful signals are ignored. If a service stops (or throws) on its\n * own, the remaining services are stopped without draining.\n *\n * @returns a Promise that settles once all services have stopped (or\n * {@link STOP_TIMEOUT_MS} has elapsed), resolving or rejecting\n * with the outcome of the first service to stop.\n */\nexport async function runUntilKilled(\n lc: LogContext,\n parent: EventEmitter,\n ...services: SingletonService[]\n): Promise<void> {\n if (services.length === 0) {\n return;\n }\n const stopped = new Set<SingletonService>();\n const runs = services.map(svc => svc.run().finally(() => stopped.add(svc)));\n\n const allStopped = resolver<void>();\n let stopping: Promise<void> | undefined;\n const stopAll = (reason: string) =>\n (stopping ??= stopServices(lc, services, runs, stopped, reason).then(\n allStopped.resolve,\n ));\n\n const GRACEFUL_SIGNALS = GRACEFUL_SHUTDOWN as readonly NodeJS.Signals[];\n let draining = false;\n const onSignal = async (signal: NodeJS.Signals) => {\n if (GRACEFUL_SIGNALS.includes(signal)) {\n if (draining || stopping) {\n // e.g. A SIGINT from both the terminal and a wrapping process.\n lc.info?.(`already shutting down. ignoring ${signal}`);\n return;\n }\n draining = true;\n // Drain all services before stopping any of them. Stopping a service\n // resolves its run() and thus ends runUntilKilled(), so a service\n // without a drain() must not be stopped while others are draining.\n const results = await Promise.allSettled(\n services.map(async svc => {\n if (svc.drain) {\n lc.info?.(`draining ${svc.constructor.name} ${svc.id} (${signal})`);\n await svc.drain();\n }\n }),\n );\n for (const result of results) {\n if (result.status === 'rejected') {\n lc.warn?.(`error while draining (${signal})`, result.reason);\n }\n }\n }\n // A forceful signal stops the services without waiting for (or after)\n // a drain.\n await stopAll(signal);\n };\n // The listeners remain installed for the lifetime of the process, as a\n // signal without a listener would terminate the process (e.g. a repeated\n // SIGINT during a drain).\n for (const signal of [...GRACEFUL_SHUTDOWN, ...FORCEFUL_SHUTDOWN]) {\n parent.on(signal, () => void onSignal(signal));\n }\n // A forked worker whose parent has exited (e.g. was killed by a signal,\n // in which case it cannot signal its workers) stops without draining, so\n // that it (and in turn its own subprocesses) is not orphaned.\n parent.once('disconnect', () => {\n lc.warn?.(`parent process disconnected. stopping without draining`);\n void stopAll('disconnect');\n });\n\n try {\n // Run all services and resolve when any of them stops, or when stopping\n // them (after a signal) has timed out.\n const svc = await Promise.race([\n ...runs.map((run, i) => run.then(() => services[i])),\n allStopped.promise,\n ]);\n if (svc) {\n lc.info?.(`${svc.constructor.name} (${svc.id}) stopped`);\n }\n } catch (e) {\n lc.error?.(`exiting on error`, e);\n throw e;\n } finally {\n await stopAll('stopped');\n }\n}\n\nasync function stopServices(\n lc: LogContext,\n services: SingletonService[],\n runs: Promise<void>[],\n stopped: Set<SingletonService>,\n reason: string,\n) {\n for (const svc of services) {\n if (!stopped.has(svc)) {\n lc.info?.(`stopping ${svc.constructor.name} ${svc.id} (${reason})`);\n void svc\n .stop()\n .catch(e =>\n lc.warn?.(`error stopping ${svc.constructor.name} ${svc.id}`, e),\n );\n }\n }\n if (\n (await orTimeout(Promise.allSettled(runs), STOP_TIMEOUT_MS)) === 'timed-out'\n ) {\n const running = services.filter(svc => !stopped.has(svc));\n lc.warn?.(\n `services did not stop within ${STOP_TIMEOUT_MS} ms: ` +\n running.map(svc => `${svc.constructor.name} ${svc.id}`).join(', '),\n );\n }\n}\n\nexport async function exitAfter(\n logContext: LogContext | (() => LogContext),\n run: () => Promise<void>,\n) {\n const getLogContext =\n typeof logContext === 'function' ? logContext : () => logContext;\n try {\n await run();\n const lc = getLogContext();\n lc.info?.(`pid ${pid} exiting normally`);\n process.exit(0);\n } catch (e) {\n const lc = getLogContext();\n if (e instanceof ConfigurationError) {\n lc.error?.(`exiting with configuration error: ${String(e)}`, e);\n process.exit(0);\n }\n if (e instanceof AbortError) {\n // AbortErrors signal an intentional shutdown or restart (e.g. SIGTERM\n // during a rollout, AutoResetSignal) and should not raise alerts. The\n // non-zero exit code is kept so that restart semantics are unchanged.\n lc.warn?.(`exiting after abort: ${String(e)}`, e);\n } else {\n lc.error?.(`exiting with error: ${String(e)}`, e);\n }\n try {\n logLastChanceSQLiteCorruptionDiagnostics(lc, e);\n } catch (diagnosticError) {\n lc.error?.('SQLite corruption last-chance diagnostic failed', {\n error: diagnosticError,\n });\n }\n try {\n await lc.flush();\n } finally {\n process.exit(-1);\n }\n }\n}\n\n/**\n * The HeartbeatMonitor monitors the cadence heartbeats (e.g. \"/keepalive\"\n * health checks made to HttpServices) that signal that the server\n * should continue processing requests. When a configurable `stopInterval`\n * elapses without receiving these heartbeats, the monitor initiates a\n * graceful shutdown of the server. This works with common load balancing\n * frameworks such as AWS Elastic Load Balancing.\n *\n * The HeartbeatMonitor is **opt-in** in that it only kicks in after it\n * starts receiving keepalives.\n */\nexport class HeartbeatMonitor {\n readonly #stopInterval: number;\n\n #lc: LogContext;\n #checkIntervalTimer: NodeJS.Timeout | undefined;\n #checkImmediateTimer: NodeJS.Immediate | undefined;\n #lastHeartbeat = 0;\n\n constructor(lc: LogContext, stopInterval: number) {\n this.#lc = lc;\n this.#stopInterval = stopInterval;\n }\n\n onHeartbeat(reqHeaders: IncomingHttpHeaders) {\n this.#lastHeartbeat = Date.now();\n if (this.#checkIntervalTimer === undefined) {\n this.#lc.info?.(\n `starting heartbeat monitor at ${\n this.#stopInterval / 1000\n } second interval`,\n reqHeaders,\n );\n // e.g. check every 5 seconds to see if it's been over 20 seconds\n // since the last heartbeat.\n this.#checkIntervalTimer = setInterval(\n this.#checkStopInterval,\n this.#stopInterval / 4,\n );\n }\n }\n\n #checkStopInterval = () => {\n // In the Node.js event loop, timers like setInterval and setTimeout\n // run *before* I/O events coming from network sockets or file reads/writes.\n // When this process gets starved of CPU resources for long periods of time,\n // for example when other processes are monopolizing all available cores,\n // pathological behavior can emerge:\n // - keepalive network request comes in, but is queued in Node internals waiting\n // for time on the event loop\n // - CPU is starved/monopolized by other processes for longer than the time\n // configured via this.#stopInterval\n // - When CPU becomes available and the event loop wakes up, this stop interval\n // check is run *before* the keepalive request is processed. The value of\n // this.#lastHeartbeat is now very stale, and erroneously triggers a shutdown\n // even though keepalive requests were about to be processed and update\n // this.#lastHeartbeat. Downtime ensues.\n //\n // To avoid this, we push the check out to a phase of the event loop *after*\n // I/O events are processed, using setImmediate():\n // https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick#setimmediate-vs-settimeout\n //\n // This ensures we see a value for this.#lastHeartbeat that reflects\n // any keepalive requests that came in during the current event loop turn.\n this.#checkImmediateTimer = setImmediate(() => {\n this.#checkImmediateTimer = undefined;\n const timeSinceLastHeartbeat = Date.now() - this.#lastHeartbeat;\n if (timeSinceLastHeartbeat >= this.#stopInterval) {\n this.#lc.info?.(\n `last heartbeat received ${\n timeSinceLastHeartbeat / 1000\n } seconds ago. draining.`,\n );\n process.kill(process.pid, GRACEFUL_SHUTDOWN[0]);\n }\n });\n };\n\n stop() {\n clearTimeout(this.#checkIntervalTimer);\n if (this.#checkImmediateTimer) {\n clearImmediate(this.#checkImmediateTimer);\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -18,6 +18,8 @@ export type SwitchoverOptions = {
|
|
|
18
18
|
replicaFile: string;
|
|
19
19
|
/** For testing. */
|
|
20
20
|
timeoutMs?: number | undefined;
|
|
21
|
+
/** For testing. */
|
|
22
|
+
searchTimeoutMs?: number | undefined;
|
|
21
23
|
};
|
|
22
24
|
export type Downstream = Source<Sized<SubscribeDownstream>>;
|
|
23
25
|
/**
|
|
@@ -46,6 +48,12 @@ export type Downstream = Source<Sized<SubscribeDownstream>>;
|
|
|
46
48
|
* * Anything else (e.g. an error, or a `draining` replication-manager):
|
|
47
49
|
* The switchover is retried later.
|
|
48
50
|
*
|
|
51
|
+
* If no other replication-manager accepts or holds a switchover subscription
|
|
52
|
+
* within {@link DEFAULT_SWITCHOVER_SEARCH_TIMEOUT_MS} (e.g. because the
|
|
53
|
+
* draining one is the only one), the current subscription is closed so that
|
|
54
|
+
* the draining replication-manager can shut down, and the consumer reconnects
|
|
55
|
+
* on its own.
|
|
56
|
+
*
|
|
49
57
|
* The current subscription continues to be consumed throughout. A
|
|
50
58
|
* switchover is scoped to the subscription from which it switches: If that
|
|
51
59
|
* subscription ends first (e.g. the replication-manager shuts down), the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"drain-handler.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/replicator/drain-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAEjD,OAAO,EAAW,KAAK,KAAK,EAAE,KAAK,MAAM,EAAC,MAAM,wBAAwB,CAAC;AACzE,OAAO,KAAK,EACV,cAAc,EAEd,iBAAiB,EAClB,MAAM,uCAAuC,CAAC;AAC/C,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,iCAAiC,CAAC;AACzE,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"drain-handler.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/replicator/drain-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAEjD,OAAO,EAAW,KAAK,KAAK,EAAE,KAAK,MAAM,EAAC,MAAM,wBAAwB,CAAC;AACzE,OAAO,KAAK,EACV,cAAc,EAEd,iBAAiB,EAClB,MAAM,uCAAuC,CAAC;AAC/C,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,iCAAiC,CAAC;AACzE,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,qBAAqB,CAAC;AAqBtD,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;;;;OAKG;IACH,gBAAgB,EAAE,MAAM,IAAI,CAAC;IAE7B;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB,mBAAmB;IACnB,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAE/B,mBAAmB;IACnB,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACtC,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,mBAAmB,CAAC,CAAC,CAAC;AAS5D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,qBAAa,YAAY;;IAYvB;;;OAGG;IACH,YACE,EAAE,EAAE,UAAU,EACd,cAAc,EAAE,cAAc,EAC9B,KAAK,EAAE,YAAY,EACnB,OAAO,EAAE,iBAAiB,EAC1B,gBAAgB,EAAE,MAAM,iBAAiB,EAO1C;IAED;;;OAGG;IACH,qBAAqB,CAAC,OAAO,EAAE,UAAU,QAaxC;IAED,IAAI,iBAAiB,IAAI,OAAO,CAE/B;IAED;;;OAGG;IACH,iBAAiB,IAAI,UAAU,GAAG,SAAS,CAI1C;CA+JF"}
|
|
@@ -5,6 +5,18 @@ var DEFAULT_SWITCHOVER_TIMEOUT_MS = 1e4;
|
|
|
5
5
|
var MIN_SWITCHOVER_RETRY_DELAY_MS = 1e3;
|
|
6
6
|
var MAX_SWITCHOVER_RETRY_DELAY_MS = 3e4;
|
|
7
7
|
/**
|
|
8
|
+
* Bounds the time to search for another replication-manager to switch over
|
|
9
|
+
* to, after which the current subscription is closed so that the draining
|
|
10
|
+
* replication-manager can shut down (e.g. when it is the only one). The time
|
|
11
|
+
* during which a subscription is held by another replication-manager (i.e.
|
|
12
|
+
* waiting for permission to restore) does not count towards it.
|
|
13
|
+
*
|
|
14
|
+
* This is well below the default termination grace period of container
|
|
15
|
+
* runners (30 seconds in Kubernetes and ECS), so that the draining
|
|
16
|
+
* replication-manager can shut down cleanly when there is no other.
|
|
17
|
+
*/
|
|
18
|
+
var DEFAULT_SWITCHOVER_SEARCH_TIMEOUT_MS = 2e4;
|
|
19
|
+
/**
|
|
8
20
|
* Handles the switchover of a serving replica's subscription from a draining
|
|
9
21
|
* replication-manager to another one, without interrupting the current
|
|
10
22
|
* subscription.
|
|
@@ -30,6 +42,12 @@ var MAX_SWITCHOVER_RETRY_DELAY_MS = 3e4;
|
|
|
30
42
|
* * Anything else (e.g. an error, or a `draining` replication-manager):
|
|
31
43
|
* The switchover is retried later.
|
|
32
44
|
*
|
|
45
|
+
* If no other replication-manager accepts or holds a switchover subscription
|
|
46
|
+
* within {@link DEFAULT_SWITCHOVER_SEARCH_TIMEOUT_MS} (e.g. because the
|
|
47
|
+
* draining one is the only one), the current subscription is closed so that
|
|
48
|
+
* the draining replication-manager can shut down, and the consumer reconnects
|
|
49
|
+
* on its own.
|
|
50
|
+
*
|
|
33
51
|
* The current subscription continues to be consumed throughout. A
|
|
34
52
|
* switchover is scoped to the subscription from which it switches: If that
|
|
35
53
|
* subscription ends first (e.g. the replication-manager shuts down), the
|
|
@@ -82,11 +100,27 @@ var DrainHandler = class {
|
|
|
82
100
|
return !current.signal.aborted && this.#state.shouldRun();
|
|
83
101
|
}
|
|
84
102
|
async #switchover(current) {
|
|
103
|
+
const searchTimeoutMs = this.#options.searchTimeoutMs ?? DEFAULT_SWITCHOVER_SEARCH_TIMEOUT_MS;
|
|
85
104
|
let failures = 0;
|
|
105
|
+
let searchStart = Date.now();
|
|
86
106
|
while (this.#isActive(current)) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
107
|
+
const outcome = await this.#attempt(current);
|
|
108
|
+
if (outcome === "switched" || outcome === "restoring") return;
|
|
109
|
+
if (outcome === "released") {
|
|
110
|
+
failures = 0;
|
|
111
|
+
searchStart = Date.now();
|
|
112
|
+
}
|
|
113
|
+
const remaining = searchStart + searchTimeoutMs - Date.now();
|
|
114
|
+
if (remaining > 0) {
|
|
115
|
+
const delay = Math.min(MIN_SWITCHOVER_RETRY_DELAY_MS * 2 ** failures++, MAX_SWITCHOVER_RETRY_DELAY_MS);
|
|
116
|
+
await this.#state.sleep(Math.min(delay, remaining));
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (this.#isActive(current)) {
|
|
120
|
+
this.#lc.warn?.(`no other replication-manager found within ${searchTimeoutMs} ms. disconnecting from the draining replication-manager`);
|
|
121
|
+
current.cancel();
|
|
122
|
+
}
|
|
123
|
+
return;
|
|
90
124
|
}
|
|
91
125
|
}
|
|
92
126
|
async #attempt(current) {
|
|
@@ -118,7 +152,7 @@ var DrainHandler = class {
|
|
|
118
152
|
if (restore) {
|
|
119
153
|
if (await this.#holdForRestore(current, rest, restore)) return "restoring";
|
|
120
154
|
lc.info?.(`held switchover subscription closed. will retry`);
|
|
121
|
-
return "
|
|
155
|
+
return "released";
|
|
122
156
|
}
|
|
123
157
|
if (!draining) {
|
|
124
158
|
lc.info?.(`switched over to a new subscription at ${ctx.watermark}`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"drain-handler.js","names":["#lc","#changeStreamer","#state","#options","#committedContext","#restoreGranted","#draining","#isActive","#switchover","#next","#attempt","#holdForRestore"],"sources":["../../../../../../zero-cache/src/services/replicator/drain-handler.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport {readHead, type Sized, type Source} from '../../types/streams.ts';\nimport type {\n ChangeStreamer,\n Status,\n SubscriberContext,\n} from '../change-streamer/change-streamer.ts';\nimport type {SubscribeDownstream} from '../change-streamer/subscribe.ts';\nimport type {RunningState} from '../running-state.ts';\n\n// Bounds the time to wait for a switchover subscription to be established\n// and to receive its first message.\nconst DEFAULT_SWITCHOVER_TIMEOUT_MS = 10_000;\nconst MIN_SWITCHOVER_RETRY_DELAY_MS = 1_000;\nconst MAX_SWITCHOVER_RETRY_DELAY_MS = 30_000;\n\nexport type SwitchoverOptions = {\n /**\n * Called when a replication-manager with a newer replica version grants\n * permission to restore its replica. This should initiate a graceful\n * restart of the server (during which the current subscription continues\n * to be served).\n */\n onRestoreGranted: () => void;\n\n /**\n * The replica file, from which the state of the last committed\n * transaction is read for switchover subscriptions.\n */\n replicaFile: string;\n\n /** For testing. */\n timeoutMs?: number | undefined;\n};\n\nexport type Downstream = Source<Sized<SubscribeDownstream>>;\n\ntype AttemptOutcome = 'switched' | 'restoring' | 'retry';\n\n/**\n * Handles the switchover of a serving replica's subscription from a draining\n * replication-manager to another one, without interrupting the current\n * subscription.\n *\n * Upon {@link onChangeStreamerDrain}, the handler repeatedly (with backoff)\n * opens a new `switchover` subscription, at the watermark of the replica's\n * last committed transaction, to whichever replication-manager the\n * connection is routed to. The first message on the new subscription\n * determines the outcome:\n * * `status`: The subscription was accepted (i.e. the replica is compatible).\n * The current subscription is cancelled (aborting any transaction in\n * progress), and the new one is made available via\n * {@link getNextConnection}. Note that the new subscription may start with\n * transactions that were committed from the current subscription in the\n * meantime, which the consumer must skip.\n * * `status` with `restore: queued`: The replication-manager has a newer\n * replica version, and is holding the subscription until it grants\n * permission to restore. The held subscription is monitored for the grant.\n * * `status` with `restore: granted`: Permission to restore is granted, and\n * {@link SwitchoverOptions.onRestoreGranted} is called. The granted\n * subscription is kept open until the process exits, so that the\n * replication-manager can track the grant until the restore begins.\n * * Anything else (e.g. an error, or a `draining` replication-manager):\n * The switchover is retried later.\n *\n * The current subscription continues to be consumed throughout. A\n * switchover is scoped to the subscription from which it switches: If that\n * subscription ends first (e.g. the replication-manager shuts down), the\n * switchover is abandoned, including any held (but not granted)\n * subscription, as the consumer reconnects on its own.\n */\nexport class DrainHandler {\n readonly #lc: LogContext;\n readonly #changeStreamer: ChangeStreamer;\n readonly #state: RunningState;\n readonly #options: SwitchoverOptions;\n readonly #committedContext: () => SubscriberContext;\n\n // The subscription currently being switched from.\n #draining: Downstream | undefined;\n #restoreGranted = false;\n #next: Downstream | undefined;\n\n /**\n * @param committedContext Returns the subscriber context as of the\n * replica's last committed transaction.\n */\n constructor(\n lc: LogContext,\n changeStreamer: ChangeStreamer,\n state: RunningState,\n options: SwitchoverOptions,\n committedContext: () => SubscriberContext,\n ) {\n this.#lc = lc.withContext('component', 'drain-handler');\n this.#changeStreamer = changeStreamer;\n this.#state = state;\n this.#options = options;\n this.#committedContext = committedContext;\n }\n\n /**\n * Called when the change-streamer of the `current` subscription is\n * draining.\n */\n onChangeStreamerDrain(current: Downstream) {\n if (\n this.#restoreGranted ||\n this.#draining === current ||\n !this.#isActive(current)\n ) {\n return;\n }\n this.#draining = current;\n this.#lc.info?.(\n `replication-manager is draining. switching over to another`,\n );\n void this.#switchover(current);\n }\n\n get hasNextConnection(): boolean {\n return this.#next !== undefined;\n }\n\n /**\n * Returns (and clears) the subscription that replaces the (now cancelled)\n * current one, if a switchover succeeded.\n */\n getNextConnection(): Downstream | undefined {\n const next = this.#next;\n this.#next = undefined;\n return next;\n }\n\n #isActive(current: Downstream) {\n return !current.signal.aborted && this.#state.shouldRun();\n }\n\n async #switchover(current: Downstream) {\n let failures = 0;\n while (this.#isActive(current)) {\n const outcome = await this.#attempt(current);\n if (outcome !== 'retry') {\n return;\n }\n const delay = Math.min(\n MIN_SWITCHOVER_RETRY_DELAY_MS * 2 ** failures++,\n MAX_SWITCHOVER_RETRY_DELAY_MS,\n );\n await this.#state.sleep(delay);\n }\n }\n\n async #attempt(current: Downstream): Promise<AttemptOutcome> {\n const lc = this.#lc;\n const timeoutMs = this.#options.timeoutMs ?? DEFAULT_SWITCHOVER_TIMEOUT_MS;\n let candidate: Downstream | undefined;\n let timer: ReturnType<typeof setTimeout> | undefined;\n let timedOut = false;\n try {\n const ctx = this.#committedContext();\n lc.info?.(`attempting switchover at ${ctx.watermark}`);\n const timeout = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n timedOut = true;\n reject(new Error(`timed out after ${timeoutMs} ms`));\n }, timeoutMs);\n });\n const subscribing = this.#changeStreamer.subscribe({\n ...ctx,\n switchover: true,\n });\n // Clean up a connection that is established after a timeout.\n void subscribing.then(\n sub => (timedOut ? sub.cancel() : undefined),\n () => {},\n );\n candidate = await Promise.race([subscribing, timeout]);\n const {head, rest} = readHead(candidate);\n const first = await Promise.race([head.next(), timeout]);\n const msg = first.done ? undefined : first.value.data;\n\n if (!this.#isActive(current)) {\n // e.g. The current subscription failed in the meantime.\n } else if (msg?.[0] === 'status') {\n const {restore, draining} = msg[1];\n if (restore) {\n const granted = await this.#holdForRestore(current, rest, restore);\n if (granted) {\n return 'restoring';\n }\n lc.info?.(`held switchover subscription closed. will retry`);\n return 'retry';\n }\n if (!draining) {\n lc.info?.(`switched over to a new subscription at ${ctx.watermark}`);\n this.#next = rest;\n current.cancel();\n return 'switched';\n }\n }\n lc.warn?.(`switchover rejected. continuing current subscription`, msg);\n } catch (e) {\n lc.warn?.(`switchover failed. continuing current subscription`, e);\n } finally {\n clearTimeout(timer);\n }\n candidate?.cancel();\n return 'retry';\n }\n\n /**\n * Keeps the held (or granted) subscription open, and initiates a graceful\n * restart when permission to restore is granted. A held subscription that\n * has yet to be granted is cancelled if the `current` subscription ends.\n *\n * @returns `true` once permission is granted, or `false` if the held\n * subscription closes before then.\n */\n #holdForRestore(\n current: Downstream,\n held: Downstream,\n restore: NonNullable<Status['restore']>,\n ): Promise<boolean> {\n const lc = this.#lc;\n const result = resolver<boolean>();\n const unregister = this.#state.cancelOnStop(held);\n const onCurrentEnded = () => {\n if (!this.#restoreGranted) {\n lc.info?.(`abandoning held switchover subscription`);\n held.cancel();\n }\n };\n current.signal.addEventListener('abort', onCurrentEnded, {once: true});\n\n const onRestore = (r: NonNullable<Status['restore']>) => {\n if (r.state === 'queued') {\n lc.info?.(\n `waiting for permission to restore a new replica (position ${r.position})`,\n );\n } else if (!this.#restoreGranted) {\n lc.info?.(\n `granted permission to restore a new replica. restarting gracefully`,\n );\n this.#restoreGranted = true;\n this.#options.onRestoreGranted();\n result.resolve(true);\n }\n };\n onRestore(restore);\n void (async () => {\n try {\n // Note: The loop must not be exited early, as that would close the\n // connection.\n for await (const {data: msg} of held) {\n if (msg[0] === 'status' && msg[1].restore) {\n onRestore(msg[1].restore);\n }\n }\n } catch (e) {\n lc.warn?.(`held switchover subscription failed`, e);\n } finally {\n unregister();\n current.signal.removeEventListener('abort', onCurrentEnded);\n result.resolve(false);\n }\n })();\n return result.promise;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"drain-handler.js","names":["#lc","#changeStreamer","#state","#options","#committedContext","#restoreGranted","#draining","#isActive","#switchover","#next","#attempt","#holdForRestore"],"sources":["../../../../../../zero-cache/src/services/replicator/drain-handler.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport {readHead, type Sized, type Source} from '../../types/streams.ts';\nimport type {\n ChangeStreamer,\n Status,\n SubscriberContext,\n} from '../change-streamer/change-streamer.ts';\nimport type {SubscribeDownstream} from '../change-streamer/subscribe.ts';\nimport type {RunningState} from '../running-state.ts';\n\n// Bounds the time to wait for a switchover subscription to be established\n// and to receive its first message.\nconst DEFAULT_SWITCHOVER_TIMEOUT_MS = 10_000;\nconst MIN_SWITCHOVER_RETRY_DELAY_MS = 1_000;\nconst MAX_SWITCHOVER_RETRY_DELAY_MS = 30_000;\n\n/**\n * Bounds the time to search for another replication-manager to switch over\n * to, after which the current subscription is closed so that the draining\n * replication-manager can shut down (e.g. when it is the only one). The time\n * during which a subscription is held by another replication-manager (i.e.\n * waiting for permission to restore) does not count towards it.\n *\n * This is well below the default termination grace period of container\n * runners (30 seconds in Kubernetes and ECS), so that the draining\n * replication-manager can shut down cleanly when there is no other.\n */\nconst DEFAULT_SWITCHOVER_SEARCH_TIMEOUT_MS = 20_000;\n\nexport type SwitchoverOptions = {\n /**\n * Called when a replication-manager with a newer replica version grants\n * permission to restore its replica. This should initiate a graceful\n * restart of the server (during which the current subscription continues\n * to be served).\n */\n onRestoreGranted: () => void;\n\n /**\n * The replica file, from which the state of the last committed\n * transaction is read for switchover subscriptions.\n */\n replicaFile: string;\n\n /** For testing. */\n timeoutMs?: number | undefined;\n\n /** For testing. */\n searchTimeoutMs?: number | undefined;\n};\n\nexport type Downstream = Source<Sized<SubscribeDownstream>>;\n\ntype AttemptOutcome =\n | 'switched'\n | 'restoring'\n // The subscription was held for restore, but closed before a grant.\n | 'released'\n | 'retry';\n\n/**\n * Handles the switchover of a serving replica's subscription from a draining\n * replication-manager to another one, without interrupting the current\n * subscription.\n *\n * Upon {@link onChangeStreamerDrain}, the handler repeatedly (with backoff)\n * opens a new `switchover` subscription, at the watermark of the replica's\n * last committed transaction, to whichever replication-manager the\n * connection is routed to. The first message on the new subscription\n * determines the outcome:\n * * `status`: The subscription was accepted (i.e. the replica is compatible).\n * The current subscription is cancelled (aborting any transaction in\n * progress), and the new one is made available via\n * {@link getNextConnection}. Note that the new subscription may start with\n * transactions that were committed from the current subscription in the\n * meantime, which the consumer must skip.\n * * `status` with `restore: queued`: The replication-manager has a newer\n * replica version, and is holding the subscription until it grants\n * permission to restore. The held subscription is monitored for the grant.\n * * `status` with `restore: granted`: Permission to restore is granted, and\n * {@link SwitchoverOptions.onRestoreGranted} is called. The granted\n * subscription is kept open until the process exits, so that the\n * replication-manager can track the grant until the restore begins.\n * * Anything else (e.g. an error, or a `draining` replication-manager):\n * The switchover is retried later.\n *\n * If no other replication-manager accepts or holds a switchover subscription\n * within {@link DEFAULT_SWITCHOVER_SEARCH_TIMEOUT_MS} (e.g. because the\n * draining one is the only one), the current subscription is closed so that\n * the draining replication-manager can shut down, and the consumer reconnects\n * on its own.\n *\n * The current subscription continues to be consumed throughout. A\n * switchover is scoped to the subscription from which it switches: If that\n * subscription ends first (e.g. the replication-manager shuts down), the\n * switchover is abandoned, including any held (but not granted)\n * subscription, as the consumer reconnects on its own.\n */\nexport class DrainHandler {\n readonly #lc: LogContext;\n readonly #changeStreamer: ChangeStreamer;\n readonly #state: RunningState;\n readonly #options: SwitchoverOptions;\n readonly #committedContext: () => SubscriberContext;\n\n // The subscription currently being switched from.\n #draining: Downstream | undefined;\n #restoreGranted = false;\n #next: Downstream | undefined;\n\n /**\n * @param committedContext Returns the subscriber context as of the\n * replica's last committed transaction.\n */\n constructor(\n lc: LogContext,\n changeStreamer: ChangeStreamer,\n state: RunningState,\n options: SwitchoverOptions,\n committedContext: () => SubscriberContext,\n ) {\n this.#lc = lc.withContext('component', 'drain-handler');\n this.#changeStreamer = changeStreamer;\n this.#state = state;\n this.#options = options;\n this.#committedContext = committedContext;\n }\n\n /**\n * Called when the change-streamer of the `current` subscription is\n * draining.\n */\n onChangeStreamerDrain(current: Downstream) {\n if (\n this.#restoreGranted ||\n this.#draining === current ||\n !this.#isActive(current)\n ) {\n return;\n }\n this.#draining = current;\n this.#lc.info?.(\n `replication-manager is draining. switching over to another`,\n );\n void this.#switchover(current);\n }\n\n get hasNextConnection(): boolean {\n return this.#next !== undefined;\n }\n\n /**\n * Returns (and clears) the subscription that replaces the (now cancelled)\n * current one, if a switchover succeeded.\n */\n getNextConnection(): Downstream | undefined {\n const next = this.#next;\n this.#next = undefined;\n return next;\n }\n\n #isActive(current: Downstream) {\n return !current.signal.aborted && this.#state.shouldRun();\n }\n\n async #switchover(current: Downstream) {\n const searchTimeoutMs =\n this.#options.searchTimeoutMs ?? DEFAULT_SWITCHOVER_SEARCH_TIMEOUT_MS;\n let failures = 0;\n let searchStart = Date.now();\n while (this.#isActive(current)) {\n const outcome = await this.#attempt(current);\n if (outcome === 'switched' || outcome === 'restoring') {\n return;\n }\n if (outcome === 'released') {\n // Time spent held by another replication-manager does not count\n // towards the search.\n failures = 0;\n searchStart = Date.now();\n }\n const remaining = searchStart + searchTimeoutMs - Date.now();\n if (remaining > 0) {\n const delay = Math.min(\n MIN_SWITCHOVER_RETRY_DELAY_MS * 2 ** failures++,\n MAX_SWITCHOVER_RETRY_DELAY_MS,\n );\n await this.#state.sleep(Math.min(delay, remaining));\n continue;\n }\n // The search has timed out.\n if (this.#isActive(current)) {\n this.#lc.warn?.(\n `no other replication-manager found within ${searchTimeoutMs} ms. ` +\n `disconnecting from the draining replication-manager`,\n );\n current.cancel();\n }\n return;\n }\n }\n\n async #attempt(current: Downstream): Promise<AttemptOutcome> {\n const lc = this.#lc;\n const timeoutMs = this.#options.timeoutMs ?? DEFAULT_SWITCHOVER_TIMEOUT_MS;\n let candidate: Downstream | undefined;\n let timer: ReturnType<typeof setTimeout> | undefined;\n let timedOut = false;\n try {\n const ctx = this.#committedContext();\n lc.info?.(`attempting switchover at ${ctx.watermark}`);\n const timeout = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n timedOut = true;\n reject(new Error(`timed out after ${timeoutMs} ms`));\n }, timeoutMs);\n });\n const subscribing = this.#changeStreamer.subscribe({\n ...ctx,\n switchover: true,\n });\n // Clean up a connection that is established after a timeout.\n void subscribing.then(\n sub => (timedOut ? sub.cancel() : undefined),\n () => {},\n );\n candidate = await Promise.race([subscribing, timeout]);\n const {head, rest} = readHead(candidate);\n const first = await Promise.race([head.next(), timeout]);\n const msg = first.done ? undefined : first.value.data;\n\n if (!this.#isActive(current)) {\n // e.g. The current subscription failed in the meantime.\n } else if (msg?.[0] === 'status') {\n const {restore, draining} = msg[1];\n if (restore) {\n const granted = await this.#holdForRestore(current, rest, restore);\n if (granted) {\n return 'restoring';\n }\n lc.info?.(`held switchover subscription closed. will retry`);\n return 'released';\n }\n if (!draining) {\n lc.info?.(`switched over to a new subscription at ${ctx.watermark}`);\n this.#next = rest;\n current.cancel();\n return 'switched';\n }\n }\n lc.warn?.(`switchover rejected. continuing current subscription`, msg);\n } catch (e) {\n lc.warn?.(`switchover failed. continuing current subscription`, e);\n } finally {\n clearTimeout(timer);\n }\n candidate?.cancel();\n return 'retry';\n }\n\n /**\n * Keeps the held (or granted) subscription open, and initiates a graceful\n * restart when permission to restore is granted. A held subscription that\n * has yet to be granted is cancelled if the `current` subscription ends.\n *\n * @returns `true` once permission is granted, or `false` if the held\n * subscription closes before then.\n */\n #holdForRestore(\n current: Downstream,\n held: Downstream,\n restore: NonNullable<Status['restore']>,\n ): Promise<boolean> {\n const lc = this.#lc;\n const result = resolver<boolean>();\n const unregister = this.#state.cancelOnStop(held);\n const onCurrentEnded = () => {\n if (!this.#restoreGranted) {\n lc.info?.(`abandoning held switchover subscription`);\n held.cancel();\n }\n };\n current.signal.addEventListener('abort', onCurrentEnded, {once: true});\n\n const onRestore = (r: NonNullable<Status['restore']>) => {\n if (r.state === 'queued') {\n lc.info?.(\n `waiting for permission to restore a new replica (position ${r.position})`,\n );\n } else if (!this.#restoreGranted) {\n lc.info?.(\n `granted permission to restore a new replica. restarting gracefully`,\n );\n this.#restoreGranted = true;\n this.#options.onRestoreGranted();\n result.resolve(true);\n }\n };\n onRestore(restore);\n void (async () => {\n try {\n // Note: The loop must not be exited early, as that would close the\n // connection.\n for await (const {data: msg} of held) {\n if (msg[0] === 'status' && msg[1].restore) {\n onRestore(msg[1].restore);\n }\n }\n } catch (e) {\n lc.warn?.(`held switchover subscription failed`, e);\n } finally {\n unregister();\n current.signal.removeEventListener('abort', onCurrentEnded);\n result.resolve(false);\n }\n })();\n return result.promise;\n }\n}\n"],"mappings":"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"}
|