openvibers/openvibe.search Commits

Move Search's shutdown onto openvibe-sdk/service gracefulStop (#4)

OP OpenVibers committed 2 Oct 2026 · 4d6e48c2e6 · 4 files, +66 −12

checks run by GitHub CI (test) · CI ✓ 3/3 · review SHIP by DeepSeek harness

https://github.com/OpenVibers/OpenVibe.Search/pull/4

Changes

ddiff --git a/package-lock.json b/package-lock.json
iindex b90f2bb..d4248d1 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -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"
ddiff --git a/package.json b/package.json
iindex a0f1544..cdd33e3 100644
---- a/package.json
++++ b/package.json
@@@ -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"
},
ddiff --git a/server/index.js b/server/index.js
iindex 5587b69..4cec4a3 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -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(), fetch
if (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);
ddiff --git a/test/service-kit.test.js b/test/service-kit.test.js
nnew file mode 100644
iindex 0000000..2b0bbaa
---- /dev/null
++++ b/test/service-kit.test.js
@@@ -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();