Skip to content

Commit 4b0938c

Browse files
fix(core): handle graceful shutdown when server fails to start (#3281)
* fix(core): handle graceful shutdown when server fails to start * docs(billing): add module README with quota system guide * fix(core): address review feedback on graceful shutdown - Extract hard-coded timeout into FORCE_SHUTDOWN_TIMEOUT_MS constant - Add JSDoc for shutdown function with @param and @returns - Log error in catch block instead of swallowing silently
1 parent 6df1249 commit 4b0938c

3 files changed

Lines changed: 98 additions & 14 deletions

File tree

lib/app.js

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -127,23 +127,38 @@ const start = async () => {
127127
}
128128
};
129129

130-
// Shut down the server
130+
const FORCE_SHUTDOWN_TIMEOUT_MS = 5000;
131+
132+
/**
133+
* Gracefully shut down the server, disconnecting services before exiting.
134+
* If the server promise rejects (e.g. failed to start), logs the error and exits.
135+
* A forced exit is triggered after {@link FORCE_SHUTDOWN_TIMEOUT_MS} ms if shutdown hangs.
136+
*
137+
* @param {Promise<{http: import('http').Server}>} server - Promise returned by {@link start}
138+
* @returns {Promise<void>} Resolves when shutdown is initiated (process exits via callback)
139+
*/
131140
const shutdown = async (server) => {
141+
// Force exit if graceful shutdown hangs
142+
const forceTimeout = setTimeout(() => {
143+
console.error(chalk.red('Forced shutdown (timeout)'));
144+
process.exit(1);
145+
}, FORCE_SHUTDOWN_TIMEOUT_MS);
146+
forceTimeout.unref();
147+
132148
try {
133-
server.then(async (value) => {
134-
await mongooseService.disconnect();
135-
// add sequelize
136-
value.http.close((err) => {
137-
console.info(chalk.yellow('Server closed'));
138-
if (err) {
139-
console.info(chalk.red('Error on server close.', err));
140-
process.exitCode = 1;
141-
}
142-
process.exit();
143-
});
149+
const value = await server;
150+
await mongooseService.disconnect();
151+
value.http.close((err) => {
152+
console.info(chalk.yellow('Server closed'));
153+
if (err) {
154+
console.info(chalk.red('Error on server close.', err));
155+
process.exitCode = 1;
156+
}
157+
process.exit();
144158
});
145-
} catch (e) {
146-
throw new Error(e);
159+
} catch (err) {
160+
console.error(chalk.red('Shutdown error: server never started or shutdown failed'), err);
161+
process.exit(1);
147162
}
148163
};
149164

modules/billing/README.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Billing Module
2+
3+
Stripe-based billing with per-plan quota management.
4+
5+
## Quota System
6+
7+
### Configuration
8+
9+
Each downstream project defines its quotas in the billing config:
10+
11+
```js
12+
// config/defaults/development.config.js
13+
billing: {
14+
quotas: {
15+
free: { documents: { create: 10, export: 50 } },
16+
starter: { documents: { create: 100, export: 500 } },
17+
pro: { documents: { create: Infinity, export: Infinity } },
18+
},
19+
}
20+
```
21+
22+
### Middleware — `requireQuota(resource, action)`
23+
24+
Enforces per-plan limits on routes. Returns 429 when quota exceeded.
25+
26+
```js
27+
import requireQuota from '../billing/middlewares/billing.requireQuota.js';
28+
29+
app.route('/api/documents')
30+
.post(passport.authenticate('jwt', { session: false }),
31+
organization.resolveOrganization,
32+
requireQuota('documents', 'create'),
33+
documents.create);
34+
```
35+
36+
### Tracking Usage — `BillingUsageService`
37+
38+
Increment counters after successful operations:
39+
40+
```js
41+
import BillingUsageService from '../billing/services/billing.usage.service.js';
42+
43+
// After creating a document
44+
await BillingUsageService.increment(organizationId, 'documents.create', 1);
45+
```
46+
47+
### Usage Endpoint
48+
49+
`GET /api/billing/usage` — returns current usage, limits, and plan for the authenticated org.
50+
51+
### Events
52+
53+
Listen for plan changes in downstream modules:
54+
55+
```js
56+
import { billingEvents } from '../billing/lib/events.js';
57+
58+
billingEvents.on('plan.changed', ({ organizationId, previousPlan, newPlan, isDowngrade }) => {
59+
if (isDowngrade) {
60+
// Handle downgrade (e.g. disable premium features)
61+
}
62+
});
63+
```

modules/billing/config/billing.development.config.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ const config = {
22
billing: {
33
// Plans available for subscriptions — extend as needed
44
plans: ['free', 'starter', 'pro', 'enterprise'],
5+
// Quotas — downstream projects override these per plan:
6+
// quotas: {
7+
// free: { documents: { create: 10, export: 50 } },
8+
// starter: { documents: { create: 100, export: 500 } },
9+
// pro: { documents: { create: Infinity, export: Infinity } },
10+
// },
511
// Stripe subscription statuses — see https://docs.stripe.com/api/subscriptions/object#subscription_object-status
612
statuses: [
713
'incomplete',

0 commit comments

Comments
 (0)