openvibers/openvibe.events Commits
Move Events' shutdown onto openvibe-sdk/service gracefulStop (#2)
Changes
@@@ -13,7 +13,7 @@"express": "^4.21.2","iovalkey": "^0.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.81.0",-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.1",++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.2.0","pg": "^8.23.0"},@@@ -1118,9 +1118,9 @@}},"node_modules/openvibe-sdk": {-- "version": "0.25.1",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.1",-- "integrity": "sha512-c6K1riU7xT60qEd0YFn4QDPjWXTglo0LcE4cU8TSrmRTJL7TPAT7/iqQjS2UACt2zmQ1soNM8gfIZf7v9sVdZg==",++ "version": "0.26.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",++ "integrity": "sha512-+RK89ZG4WAKIRh89yr3ieIvnyJVNSOI+dsbBqGNcr9UHsJStdxtM75zSDq8cUNpBPQ1X6ElatZrjU9S3s0aEkg==","license": "MIT","engines": {"node": ">=20"@@@ -25,7 +25,7 @@"express": "^4.21.2","iovalkey": "^0.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.81.0",-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.1",++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.2.0","pg": "^8.23.0"},@@@ -18,6 +18,7 @@ const { createRealtime } = require('./realtime');const { createApp } = require('./app');const { createMetrics } = require('./metrics');const { createGuardedPost } = require('./egress');++const { gracefulStop } = require('openvibe-sdk/service');async function start({config, db: givenDb = null, clock = { now: () => Date.now() }, fetchImpl = globalThis.fetch, deliveryFetch = fetchImpl,@@@ -128,13 +129,10 @@ async function start({if (require.main === module) {require('dotenv').config();start().then((handles) => {-- const shutdown = (sig) => {-- console.log(`[events] ${sig}: shutting down`);-- handles.close().then(() => process.exit(0), () => process.exit(1));-- setTimeout(() => process.exit(1), 10000).unref();-- };-- process.on('SIGTERM', () => shutdown('SIGTERM'));-- process.on('SIGINT', () => shutdown('SIGINT'));++ // SIGTERM/SIGINT (openvibe-sdk/service, docs/service.md's handles family): requests in flight get 8 s,++ // then handles.close() (the timers stopped, the carriers/realtime/worker stopped, the server closed, the++ // database closed; a rejection exits 1); past 10 s the process exits 1.++ gracefulStop({ name: 'events', server: handles.server, handles, drainMs: 8000, deadlineMs: 10000 });}).catch((err) => {console.error(`[events] failed to start: ${err.stack || err}`);process.exit(1);nnew file mode 100644@@@ -0,0 +1,57 @@++'use strict';++/**++ * openvibe-sdk/service in Events (plan T1, the handles family): the SIGTERM/SIGINT shutdown is the kit's++ * gracefulStop, not a hand-written handler. It drains the server for 8 s, then closes the handles (the++ * timers, carriers, realtime, worker, server, valkey and database; a rejection exits 1); past 10 s the++ * process exits 1. The static half reads server/index.js; the behavioural half drives gracefulStop with++ * the same options Events passes, exits stubbed.++ */++const assert = require('assert');++const fs = require('fs');++const path = require('path');++const http = require('http');++const { gracefulStop } = require('openvibe-sdk/service');++const { suite } = require('./helpers');++++const t = suite('service-kit');++const quiet = { log() {}, warn() {}, error() {} };++const source = fs.readFileSync(path.join(__dirname, '../server/index.js'), 'utf8');++++t("server/index.js imports gracefulStop from openvibe-sdk/service and owns no signal handler", () => {++ assert.match(source, /const \{ gracefulStop \} = require\('openvibe-sdk\/service'\)/);++ assert.doesNotMatch(source, /process\.on\('SIGTERM'/);++ assert.doesNotMatch(source, /process\.on\('SIGINT'/);++ assert.doesNotMatch(source, /setTimeout\(\(\) => process\.exit\(1\), 10000\)/, 'the old 10 s exit-1 timer is gone');++});++++t("the one stop names events, passes the handles and uses drainMs 8000 / deadlineMs 10000", () => {++ const call = source.slice(source.indexOf('gracefulStop('), source.indexOf('gracefulStop(') + 200);++ assert.match(call, /name: 'events'/);++ assert.match(call, /\bhandles\b/);++ assert.match(call, /drainMs: 8000/);++ assert.match(call, /deadlineMs: 10000/);++ assert.doesNotMatch(call, /deadlineExitCode/, 'the default exit 1 past the deadline is kept');++ assert.match(source, /server\.keepAliveTimeout = 65000/, 'the keep-alive timeout is kept');++});++++t('the stop drains the server, then closes the handles; a failing close exits 1; a second call is a no-op', async () => {++ for (const fails of [false, true]) {++ const server = http.createServer((_req, res) => res.end('ok'));++ await new Promise((r) => server.listen(0, '127.0.0.1', r));++ const order = [];++ const handles = { close: async () => { order.push(server.listening ? 'close while listening' : 'close'); if (fails) throw new Error('db'); } };++ let exits = 0;++ let exited = null;++ const kit = gracefulStop({ name: 'events', server, handles, drainMs: 8000, deadlineMs: 10000, signals: false, exit: (c) => { exited = c; exits++; }, log: quiet });++ const [code, again] = await Promise.all([kit.stop('SIGTERM'), kit.stop('SIGINT')]);++ assert.deepStrictEqual(order, ['close'], 'the handles close after the server stopped listening');++ assert.strictEqual(code, fails ? 1 : 0);++ assert.strictEqual(again, code, 'a second signal changes nothing');++ assert.strictEqual(exited, code);++ assert.strictEqual(exits, 1, 'exit is called once');++ assert.strictEqual(kit.stopping(), true);++ assert.strictEqual(server.listening, false);++ }++});++++t.run();
@@ -13,7 +13,7 @@
@@ -13,7 +13,7 @@
"express": "^4.21.2",
"express": "^4.21.2",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.81.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.81.0",
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.1",
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.2.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.2.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
@@ -1118,9 +1118,9 @@
@@ -1118,9 +1118,9 @@
}
}
},
},
"node_modules/openvibe-sdk": {
"node_modules/openvibe-sdk": {
- "version": "0.25.1",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.1",
- "integrity": "sha512-c6K1riU7xT60qEd0YFn4QDPjWXTglo0LcE4cU8TSrmRTJL7TPAT7/iqQjS2UACt2zmQ1soNM8gfIZf7v9sVdZg==",
+ "version": "0.26.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
+ "integrity": "sha512-+RK89ZG4WAKIRh89yr3ieIvnyJVNSOI+dsbBqGNcr9UHsJStdxtM75zSDq8cUNpBPQ1X6ElatZrjU9S3s0aEkg==",
"license": "MIT",
"license": "MIT",
"engines": {
"engines": {
"node": ">=20"
"node": ">=20"
@@ -25,7 +25,7 @@
@@ -25,7 +25,7 @@
"express": "^4.21.2",
"express": "^4.21.2",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.81.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.81.0",
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.1",
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.2.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.2.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
@@ -18,6 +18,7 @@ const { createRealtime } = require('./realtime');
@@ -18,6 +18,7 @@ const { createRealtime } = require('./realtime');
const { createApp } = require('./app');
const { createApp } = require('./app');
const { createMetrics } = require('./metrics');
const { createMetrics } = require('./metrics');
const { createGuardedPost } = require('./egress');
const { createGuardedPost } = require('./egress');
+const { gracefulStop } = require('openvibe-sdk/service');
async function start({
async function start({
config, db: givenDb = null, clock = { now: () => Date.now() }, fetchImpl = globalThis.fetch, deliveryFetch = fetchImpl,
config, db: givenDb = null, clock = { now: () => Date.now() }, fetchImpl = globalThis.fetch, deliveryFetch = fetchImpl,
@@ -128,13 +129,10 @@ async function start({
@@ -128,13 +129,10 @@ async function start({
if (require.main === module) {
if (require.main === module) {
require('dotenv').config();
require('dotenv').config();
start().then((handles) => {
start().then((handles) => {
- const shutdown = (sig) => {
- console.log(`[events] ${sig}: shutting down`);
- handles.close().then(() => process.exit(0), () => process.exit(1));
- setTimeout(() => process.exit(1), 10000).unref();
- };
- process.on('SIGTERM', () => shutdown('SIGTERM'));
- process.on('SIGINT', () => shutdown('SIGINT'));
+ // SIGTERM/SIGINT (openvibe-sdk/service, docs/service.md's handles family): requests in flight get 8 s,
+ // then handles.close() (the timers stopped, the carriers/realtime/worker stopped, the server closed, the
+ // database closed; a rejection exits 1); past 10 s the process exits 1.
+ gracefulStop({ name: 'events', server: handles.server, handles, drainMs: 8000, deadlineMs: 10000 });
}).catch((err) => {
}).catch((err) => {
console.error(`[events] failed to start: ${err.stack || err}`);
console.error(`[events] failed to start: ${err.stack || err}`);
process.exit(1);
process.exit(1);
new file mode 100644
new file mode 100644
@@ -0,0 +1,57 @@
@@ -0,0 +1,57 @@
+'use strict';
+/**
+ * openvibe-sdk/service in Events (plan T1, the handles family): the SIGTERM/SIGINT shutdown is the kit's
+ * gracefulStop, not a hand-written handler. It drains the server for 8 s, then closes the handles (the
+ * timers, carriers, realtime, worker, server, valkey and database; a rejection exits 1); past 10 s the
+ * process exits 1. The static half reads server/index.js; the behavioural half drives gracefulStop with
+ * the same options Events passes, exits stubbed.
+ */
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const http = require('http');
+const { gracefulStop } = require('openvibe-sdk/service');
+const { suite } = require('./helpers');
+
+const t = suite('service-kit');
+const quiet = { log() {}, warn() {}, error() {} };
+const source = fs.readFileSync(path.join(__dirname, '../server/index.js'), 'utf8');
+
+t("server/index.js imports gracefulStop from openvibe-sdk/service and owns no signal handler", () => {
+ assert.match(source, /const \{ gracefulStop \} = require\('openvibe-sdk\/service'\)/);
+ assert.doesNotMatch(source, /process\.on\('SIGTERM'/);
+ assert.doesNotMatch(source, /process\.on\('SIGINT'/);
+ assert.doesNotMatch(source, /setTimeout\(\(\) => process\.exit\(1\), 10000\)/, 'the old 10 s exit-1 timer is gone');
+});
+
+t("the one stop names events, passes the handles and uses drainMs 8000 / deadlineMs 10000", () => {
+ const call = source.slice(source.indexOf('gracefulStop('), source.indexOf('gracefulStop(') + 200);
+ assert.match(call, /name: 'events'/);
+ assert.match(call, /\bhandles\b/);
+ assert.match(call, /drainMs: 8000/);
+ assert.match(call, /deadlineMs: 10000/);
+ assert.doesNotMatch(call, /deadlineExitCode/, 'the default exit 1 past the deadline is kept');
+ assert.match(source, /server\.keepAliveTimeout = 65000/, 'the keep-alive timeout is kept');
+});
+
+t('the stop drains the server, then closes the handles; a failing close exits 1; a second call is a no-op', async () => {
+ for (const fails of [false, true]) {
+ const server = http.createServer((_req, res) => res.end('ok'));
+ await new Promise((r) => server.listen(0, '127.0.0.1', r));
+ const order = [];
+ const handles = { close: async () => { order.push(server.listening ? 'close while listening' : 'close'); if (fails) throw new Error('db'); } };
+ let exits = 0;
+ let exited = null;
+ const kit = gracefulStop({ name: 'events', server, handles, drainMs: 8000, deadlineMs: 10000, signals: false, exit: (c) => { exited = c; exits++; }, log: quiet });
+ const [code, again] = await Promise.all([kit.stop('SIGTERM'), kit.stop('SIGINT')]);
+ assert.deepStrictEqual(order, ['close'], 'the handles close after the server stopped listening');
+ assert.strictEqual(code, fails ? 1 : 0);
+ assert.strictEqual(again, code, 'a second signal changes nothing');
+ assert.strictEqual(exited, code);
+ assert.strictEqual(exits, 1, 'exit is called once');
+ assert.strictEqual(kit.stopping(), true);
+ assert.strictEqual(server.listening, false);
+ }
+});
+
+t.run();