openvibers/openvibe.search Commits
Move Search's shutdown onto openvibe-sdk/service gracefulStop (#4)
Changes
@@@ -14,7 +14,7 @@"dotenv": "^16.4.7","express": "^4.21.2","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.2.0","pg": "^8.23.0"},@@@ -1040,9 +1040,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"@@@ -26,7 +26,7 @@"dotenv": "^16.4.7","express": "^4.21.2","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.2.0","pg": "^8.23.0"},@@@ -13,6 +13,7 @@ const { createEngine } = require('./engine/pg');const { createStore } = require('./store');const { createOutbox, createRelay } = require('./events/outbox');const { jwksClient } = require('openvibe-sdk/auth');++const { gracefulStop } = require('openvibe-sdk/service');const { createAuth } = require('./auth');const { createPurgeQueue, createPurger } = require('./purge');const { createSavedSearches } = require('./saved');@@@ -79,13 +80,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(`[search] ${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 prune timer and JWKS refresher stopped, the relay and purger stopped, the++ // server closed, the database closed; a rejection exits 1); past 10 s the process exits 1.++ gracefulStop({ name: 'search', server: handles.server, handles, drainMs: 8000, deadlineMs: 10000 });}).catch((err) => {console.error(`[search] failed to start: ${err.stack || err}`);process.exit(1);nnew file mode 100644@@@ -0,0 +1,56 @@++'use strict';++/**++ * openvibe-sdk/service in Search (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++ * prune timer and JWKS refresher stopped, the relay and purger stopped, the server closed, the database++ * closed; 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 Search 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 search, passes the handles and uses drainMs 8000 / deadlineMs 10000", () => {++ const call = source.slice(source.indexOf('gracefulStop('), source.indexOf('gracefulStop(') + 200);++ assert.match(call, /name: 'search'/);++ 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');++});++++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: 'search', 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();
@@ -14,7 +14,7 @@
@@ -14,7 +14,7 @@
"dotenv": "^16.4.7",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"express": "^4.21.2",
"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.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"
},
},
@@ -1040,9 +1040,9 @@
@@ -1040,9 +1040,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"
@@ -26,7 +26,7 @@
@@ -26,7 +26,7 @@
"dotenv": "^16.4.7",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"express": "^4.21.2",
"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.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"
},
},
@@ -13,6 +13,7 @@ const { createEngine } = require('./engine/pg');
@@ -13,6 +13,7 @@ const { createEngine } = require('./engine/pg');
const { createStore } = require('./store');
const { createStore } = require('./store');
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 { createPurgeQueue, createPurger } = require('./purge');
const { createPurgeQueue, createPurger } = require('./purge');
const { createSavedSearches } = require('./saved');
const { createSavedSearches } = require('./saved');
@@ -79,13 +80,10 @@ async function start({ config, db: givenDb = null, now = () => Date.now(), fetch
@@ -79,13 +80,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(`[search] ${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 prune timer and JWKS refresher stopped, the relay and purger stopped, the
+ // server closed, the database closed; a rejection exits 1); past 10 s the process exits 1.
+ gracefulStop({ name: 'search', server: handles.server, handles, drainMs: 8000, deadlineMs: 10000 });
}).catch((err) => {
}).catch((err) => {
console.error(`[search] failed to start: ${err.stack || err}`);
console.error(`[search] 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 Search (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
+ * prune timer and JWKS refresher stopped, the relay and purger stopped, the server closed, the database
+ * closed; 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 Search 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 search, passes the handles and uses drainMs 8000 / deadlineMs 10000", () => {
+ const call = source.slice(source.indexOf('gracefulStop('), source.indexOf('gracefulStop(') + 200);
+ assert.match(call, /name: 'search'/);
+ 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');
+});
+
+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: 'search', 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();