openvibers/openvibe.sources Commits
Move Sources' shutdown onto openvibe-sdk/service gracefulStop (#3)
Changes
@@@ -13,7 +13,7 @@"express": "^4.21.2","fast-xml-parser": "^4.5.3","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.76.0",-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.23.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.3.1","pg": "^8.23.0"},@@@ -1059,9 +1059,9 @@}},"node_modules/openvibe-sdk": {-- "version": "0.23.1",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.23.1",-- "integrity": "sha512-6u1Ym8u+nSKyq+uJjel4/3QDQ9HOjpQzFa8oMasorXQhgHUlDEQ7X6F6Xf1zZTuuVdMVkxOeqJqOnQbraktosg==",++ "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"@@@ -28,7 +28,7 @@"express": "^4.21.2","fast-xml-parser": "^4.5.3","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.76.0",-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.23.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.3.1","pg": "^8.23.0"},@@@ -19,6 +19,7 @@ const { createIngest } = require('./ingest');const { createScheduler } = require('./scheduler');const { createOutbox, createRelay } = require('./events/outbox');const { jwksClient } = require('openvibe-sdk/auth');++const { gracefulStop } = require('openvibe-sdk/service');const { createAuth } = require('./auth');const { createApp } = require('./app');@@@ -82,13 +83,10 @@ async function start({ config, db: givenDb = null, now = () => Date.now(), fetchif (require.main === module) {require('dotenv').config();start().then((handles) => {-- const shutdown = (sig) => {-- console.log(`[sources] ${sig}: shutting down`);-- handles.close().then(() => process.exit(0), () => process.exit(1));-- setTimeout(() => process.exit(1), 20000).unref();-- };-- process.on('SIGTERM', () => shutdown('SIGTERM'));-- process.on('SIGINT', () => shutdown('SIGINT'));++ // SIGTERM/SIGINT (openvibe-sdk/service, docs/service.md's handles family, Sources 20 s): requests in++ // flight get 15 s, then handles.close() (the prune timer and JWKS refresher stopped, the scheduler,++ // relay and server stopped, the database closed; a rejection exits 1); past 20 s the process exits 1.++ gracefulStop({ name: 'Sources', server: handles.server, handles, drainMs: 15000, deadlineMs: 20000 });}).catch((err) => {console.error(`[sources] failed to start: ${err.stack || err}`);process.exit(1);nnew file mode 100644@@@ -0,0 +1,56 @@++'use strict';++/**++ * openvibe-sdk/service in Sources (plan T1, the handles family): the SIGTERM/SIGINT shutdown is the kit's++ * gracefulStop, not a hand-written handler. It drains the server for 15 s, then closes the handles (the++ * prune timer and JWKS refresher stopped, the scheduler, relay and server stopped, the database closed; a++ * rejection exits 1); past 20 s the process exits 1. The static half reads server/index.js; the behavioural++ * half drives gracefulStop with the same options Sources 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\), 20000\)/, 'the old 20 s exit-1 timer is gone');++});++++t("the one stop names Sources, passes the handles and uses drainMs 15000 / deadlineMs 20000", () => {++ const call = source.slice(source.indexOf('gracefulStop('), source.indexOf('gracefulStop(') + 200);++ assert.match(call, /name: 'Sources'/);++ assert.match(call, /\bhandles\b/);++ assert.match(call, /drainMs: 15000/);++ assert.match(call, /deadlineMs: 20000/);++ assert.doesNotMatch(call, /deadlineExitCode/, 'the default exit 1 past the deadline 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: 'Sources', server, handles, drainMs: 15000, deadlineMs: 20000, 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",
"fast-xml-parser": "^4.5.3",
"fast-xml-parser": "^4.5.3",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.76.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.76.0",
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.23.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.3.1",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.3.1",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
@@ -1059,9 +1059,9 @@
@@ -1059,9 +1059,9 @@
}
}
},
},
"node_modules/openvibe-sdk": {
"node_modules/openvibe-sdk": {
- "version": "0.23.1",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.23.1",
- "integrity": "sha512-6u1Ym8u+nSKyq+uJjel4/3QDQ9HOjpQzFa8oMasorXQhgHUlDEQ7X6F6Xf1zZTuuVdMVkxOeqJqOnQbraktosg==",
+ "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"
@@ -28,7 +28,7 @@
@@ -28,7 +28,7 @@
"express": "^4.21.2",
"express": "^4.21.2",
"fast-xml-parser": "^4.5.3",
"fast-xml-parser": "^4.5.3",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.76.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.76.0",
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.23.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.3.1",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.3.1",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
@@ -19,6 +19,7 @@ const { createIngest } = require('./ingest');
@@ -19,6 +19,7 @@ const { createIngest } = require('./ingest');
const { createScheduler } = require('./scheduler');
const { createScheduler } = require('./scheduler');
const { createOutbox, createRelay } = require('./events/outbox');
const { createOutbox, createRelay } = require('./events/outbox');
const { jwksClient } = require('openvibe-sdk/auth');
const { jwksClient } = require('openvibe-sdk/auth');
+const { gracefulStop } = require('openvibe-sdk/service');
const { createAuth } = require('./auth');
const { createAuth } = require('./auth');
const { createApp } = require('./app');
const { createApp } = require('./app');
@@ -82,13 +83,10 @@ async function start({ config, db: givenDb = null, now = () => Date.now(), fetch
@@ -82,13 +83,10 @@ async function start({ config, db: givenDb = null, now = () => Date.now(), fetch
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(`[sources] ${sig}: shutting down`);
- handles.close().then(() => process.exit(0), () => process.exit(1));
- setTimeout(() => process.exit(1), 20000).unref();
- };
- process.on('SIGTERM', () => shutdown('SIGTERM'));
- process.on('SIGINT', () => shutdown('SIGINT'));
+ // SIGTERM/SIGINT (openvibe-sdk/service, docs/service.md's handles family, Sources 20 s): requests in
+ // flight get 15 s, then handles.close() (the prune timer and JWKS refresher stopped, the scheduler,
+ // relay and server stopped, the database closed; a rejection exits 1); past 20 s the process exits 1.
+ gracefulStop({ name: 'Sources', server: handles.server, handles, drainMs: 15000, deadlineMs: 20000 });
}).catch((err) => {
}).catch((err) => {
console.error(`[sources] failed to start: ${err.stack || err}`);
console.error(`[sources] failed to start: ${err.stack || err}`);
process.exit(1);
process.exit(1);
new file mode 100644
new file mode 100644
@@ -0,0 +1,56 @@
@@ -0,0 +1,56 @@
+'use strict';
+/**
+ * openvibe-sdk/service in Sources (plan T1, the handles family): the SIGTERM/SIGINT shutdown is the kit's
+ * gracefulStop, not a hand-written handler. It drains the server for 15 s, then closes the handles (the
+ * prune timer and JWKS refresher stopped, the scheduler, relay and server stopped, the database closed; a
+ * rejection exits 1); past 20 s the process exits 1. The static half reads server/index.js; the behavioural
+ * half drives gracefulStop with the same options Sources 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\), 20000\)/, 'the old 20 s exit-1 timer is gone');
+});
+
+t("the one stop names Sources, passes the handles and uses drainMs 15000 / deadlineMs 20000", () => {
+ const call = source.slice(source.indexOf('gracefulStop('), source.indexOf('gracefulStop(') + 200);
+ assert.match(call, /name: 'Sources'/);
+ assert.match(call, /\bhandles\b/);
+ assert.match(call, /drainMs: 15000/);
+ assert.match(call, /deadlineMs: 20000/);
+ assert.doesNotMatch(call, /deadlineExitCode/, 'the default exit 1 past the deadline 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: 'Sources', server, handles, drainMs: 15000, deadlineMs: 20000, 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();