From 421d2f4f0da96fcaedd8fdca3ed8398f2db974a5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Oct 2025 13:23:46 +0000 Subject: [PATCH 1/2] Initial plan From 58715aaee425c8f99d43547812cb6c940d292bf3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Oct 2025 13:34:36 +0000 Subject: [PATCH 2/2] Update addok-cluster to v0.10.0 and add graceful shutdown Co-authored-by: jdesboeufs <1231232+jdesboeufs@users.noreply.github.com> --- package-lock.json | 118 +++++++++++++++++++++++++++------------------- package.json | 2 +- server.js | 31 +++++++++++- test/cluster.js | 46 ++++++++++++++++++ 4 files changed, 146 insertions(+), 51 deletions(-) create mode 100644 test/cluster.js diff --git a/package-lock.json b/package-lock.json index 8cf6e91..28daeba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@livingdata/tabular-data-helpers": "^0.0.13", - "addok-cluster": "^0.9.0", + "addok-cluster": "^0.10.0", "addok-geocode-stream": "^0.26.0", "content-disposition": "^0.5.4", "cors": "^2.8.5", @@ -258,7 +258,9 @@ "license": "BSD-3-Clause" }, "node_modules/@ioredis/commands": { - "version": "1.2.0", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.4.0.tgz", + "integrity": "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==", "license": "MIT" }, "node_modules/@isaacs/cliui": { @@ -1221,39 +1223,28 @@ } }, "node_modules/addok-cluster": { - "version": "0.9.0", + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/addok-cluster/-/addok-cluster-0.10.0.tgz", + "integrity": "sha512-jVCHRSFKEOf0CQaoKcRRDkIL60VYkBIijS+xiwOV5yWJFytL3HqIZ+CBRD0m7vNcxJhkEuNeap3PiqaQLudD/Q==", "license": "MIT", "dependencies": { - "debug": "^4.4.1", + "debug": "^4.4.3", "execa": "^8.0.1", "http-errors": "^2.0.0", - "ioredis": "^5.6.1", + "ioredis": "^5.8.1", "lodash-es": "^4.17.21", - "nanoid": "^5.1.5", + "nanoid": "^5.1.6", "python-shell": "^5.0.0", - "supports-color": "^10.0.0" + "supports-color": "^10.2.2" }, "engines": { "node": ">= 20.9" } }, - "node_modules/addok-cluster/node_modules/debug": { - "version": "4.4.1", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/addok-cluster/node_modules/supports-color": { - "version": "10.0.0", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", "license": "MIT", "engines": { "node": ">=18" @@ -1289,6 +1280,25 @@ "node": ">= 20.9" } }, + "node_modules/addok-geocode-stream/node_modules/addok-cluster": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/addok-cluster/-/addok-cluster-0.9.0.tgz", + "integrity": "sha512-+QCOmOt9YVey+aPSRxvID/lLxuex4lVFnEZ+IT2hU8C8mdfcgbyV0ZwCxq6dzTLKFSsFpdBwL0nKtn+IYbIfKA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.1", + "execa": "^8.0.1", + "http-errors": "^2.0.0", + "ioredis": "^5.6.1", + "lodash-es": "^4.17.21", + "nanoid": "^5.1.5", + "python-shell": "^5.0.0", + "supports-color": "^10.0.0" + }, + "engines": { + "node": ">= 20.9" + } + }, "node_modules/addok-geocode-stream/node_modules/iconv-lite": { "version": "0.6.3", "license": "MIT", @@ -1299,6 +1309,18 @@ "node": ">=0.10.0" } }, + "node_modules/addok-geocode-stream/node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/agent-base": { "version": "7.1.3", "dev": true, @@ -1779,22 +1801,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/ava/node_modules/debug": { - "version": "4.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/ava/node_modules/strip-ansi": { "version": "7.1.0", "dev": true, @@ -2409,6 +2415,8 @@ }, "node_modules/cluster-key-slot": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", "license": "Apache-2.0", "engines": { "node": ">=0.10.0" @@ -2752,10 +2760,12 @@ } }, "node_modules/debug": { - "version": "4.3.4", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -2766,10 +2776,6 @@ } } }, - "node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "license": "MIT" - }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -2886,6 +2892,8 @@ }, "node_modules/denque": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", "license": "Apache-2.0", "engines": { "node": ">=0.10" @@ -5516,10 +5524,12 @@ } }, "node_modules/ioredis": { - "version": "5.6.1", + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.8.1.tgz", + "integrity": "sha512-Qho8TgIamqEPdgiMadJwzRMW3TudIg6vpg4YONokGDudy4eqRIJtDbVX72pfLBcWxvbn3qm/40TyGUObdW4tLQ==", "license": "MIT", "dependencies": { - "@ioredis/commands": "^1.1.1", + "@ioredis/commands": "1.4.0", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", @@ -6392,10 +6402,14 @@ }, "node_modules/lodash.defaults": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", "license": "MIT" }, "node_modules/lodash.isarguments": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", "license": "MIT" }, "node_modules/lodash.merge": { @@ -6771,7 +6785,9 @@ } }, "node_modules/nanoid": { - "version": "5.1.5", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz", + "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==", "funding": [ { "type": "github", @@ -7841,6 +7857,8 @@ }, "node_modules/redis-errors": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", "license": "MIT", "engines": { "node": ">=4" @@ -7848,6 +7866,8 @@ }, "node_modules/redis-parser": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", "license": "MIT", "dependencies": { "redis-errors": "^1.0.0" @@ -8838,6 +8858,8 @@ }, "node_modules/standard-as-callback": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", "license": "MIT" }, "node_modules/statuses": { diff --git a/package.json b/package.json index e46adce..2679fe0 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ }, "dependencies": { "@livingdata/tabular-data-helpers": "^0.0.13", - "addok-cluster": "^0.9.0", + "addok-cluster": "^0.10.0", "addok-geocode-stream": "^0.26.0", "content-disposition": "^0.5.4", "cors": "^2.8.5", diff --git a/server.js b/server.js index 1c770b0..b5b31ea 100644 --- a/server.js +++ b/server.js @@ -13,7 +13,13 @@ import routes from './lib/routes.js' const PORT = process.env.PORT || 5000 const app = express() -const cluster = await createCluster() + +const cluster = await createCluster({ + onTerminate(reason) { + console.log(`Cluster terminated: ${reason}`) + process.exit(0) + } +}) app.disable('x-powered-by') @@ -25,6 +31,27 @@ app.use(cors({origin: true})) app.use('/', routes(cluster)) -app.listen(PORT, () => { +const httpServer = app.listen(PORT, () => { console.log(`Start listening on port ${PORT}`) }) + +// Graceful shutdown on SIGTERM and SIGINT +async function handleShutdown(signal) { + console.log(`Received ${signal}, gracefully shutting down...`) + + // Close HTTP server first + httpServer.close(() => { + console.log('Server closed') + }) + + // Then terminate the cluster + try { + await cluster.end() + } catch (error) { + console.error('Error during cluster shutdown:', error) + process.exit(1) + } +} + +process.on('SIGTERM', () => handleShutdown('SIGTERM')) +process.on('SIGINT', () => handleShutdown('SIGINT')) diff --git a/test/cluster.js b/test/cluster.js new file mode 100644 index 0000000..c1742c0 --- /dev/null +++ b/test/cluster.js @@ -0,0 +1,46 @@ +import test from 'ava' + +test('cluster mock / should support end method', async t => { + const cluster = { + geocode() { + return [] + }, + reverse() { + return [] + }, + end() { + return Promise.resolve() + } + } + + t.is(typeof cluster.end, 'function') + await t.notThrowsAsync(async () => { + await cluster.end() + }) +}) + +test('cluster mock / end should be callable when cluster is terminated', async t => { + let terminated = false + + const cluster = { + geocode() { + if (terminated) { + throw new Error('Cluster terminated') + } + + return [] + }, + end() { + terminated = true + return Promise.resolve() + } + } + + await cluster.end() + t.true(terminated) + + await t.throwsAsync( + async () => cluster.geocode({}), + {message: 'Cluster terminated'} + ) +})