Skip to content

Commit eacb5ec

Browse files
committed
perf: gracefully shutdown the infinite loop
1 parent 5ef0d39 commit eacb5ec

File tree

7 files changed

+142
-82
lines changed

7 files changed

+142
-82
lines changed

src/graphql/operations/options.ts

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { capture } from '@snapshot-labs/snapshot-sentry';
2-
import snapshot from '@snapshot-labs/snapshot.js';
31
import log from '../../helpers/log';
2+
import { createLoop } from '../../helpers/loop';
43
import db from '../../helpers/mysql';
54

65
const RUN_INTERVAL = 120e3;
@@ -11,23 +10,11 @@ export default async function () {
1110
return options;
1211
}
1312

14-
async function loadOptions() {
15-
options = await db.queryAsync('SELECT s.* FROM options s');
16-
}
17-
18-
async function run() {
19-
while (true) {
20-
try {
21-
log.info('[options] Start options refresh');
22-
await loadOptions();
23-
log.info(`[options] ${options.length} options reloaded`);
24-
log.info('[options] End options refresh');
25-
} catch (e: any) {
26-
capture(e);
27-
log.error(`[options] failed to refresh options, ${JSON.stringify(e)}`);
28-
}
29-
await snapshot.utils.sleep(RUN_INTERVAL);
13+
createLoop({
14+
name: 'options',
15+
interval: RUN_INTERVAL,
16+
task: async () => {
17+
options = await db.queryAsync('SELECT s.* FROM options s');
18+
log.info(`[options] ${options.length} options reloaded`);
3019
}
31-
}
32-
33-
run();
20+
});

src/helpers/loop.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { capture } from '@snapshot-labs/snapshot-sentry';
2+
import snapshot from '@snapshot-labs/snapshot.js';
3+
import log from './log';
4+
import {
5+
getShutdownStatus,
6+
registerStopFunction,
7+
ShutdownFunction
8+
} from './shutdown';
9+
10+
interface LoopOptions {
11+
name: string;
12+
interval: number;
13+
task: () => Promise<void>;
14+
maxConsecutiveFailsBeforeCapture?: number;
15+
}
16+
17+
export const createLoop = (options: LoopOptions): ShutdownFunction => {
18+
const {
19+
name,
20+
interval,
21+
task,
22+
maxConsecutiveFailsBeforeCapture = 1
23+
} = options;
24+
let isRunning = false;
25+
let consecutiveFailsCount = 0;
26+
27+
const run = async () => {
28+
isRunning = true;
29+
while (!getShutdownStatus() && isRunning) {
30+
try {
31+
log.info(`[${name}] Start ${name} refresh`);
32+
await task();
33+
consecutiveFailsCount = 0; // Reset on success
34+
log.info(`[${name}] End ${name} refresh`);
35+
} catch (e: any) {
36+
consecutiveFailsCount++;
37+
38+
if (consecutiveFailsCount >= maxConsecutiveFailsBeforeCapture) {
39+
capture(e);
40+
}
41+
log.error(`[${name}] failed to refresh, ${JSON.stringify(e)}`);
42+
}
43+
await snapshot.utils.sleep(interval);
44+
}
45+
};
46+
47+
const stop = async () => {
48+
isRunning = false;
49+
};
50+
51+
// Register stop function and start the loop
52+
registerStopFunction(stop);
53+
run();
54+
55+
return stop;
56+
};

src/helpers/mysql.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import mysql from 'mysql';
44
import Connection from 'mysql/lib/Connection';
55
import Pool from 'mysql/lib/Pool';
66
import log from './log';
7+
import { registerStopFunction } from './shutdown';
78

89
const connectionLimit = parseInt(process.env.CONNECTION_LIMIT || '25');
910
log.info(`[mysql] connection limit ${connectionLimit}`);
@@ -37,15 +38,24 @@ const sequencerDB = mysql.createPool(sequencerConfig);
3738

3839
bluebird.promisifyAll([Pool, Connection]);
3940

40-
export const closeDatabase = (): Promise<void> => {
41+
const createCloseFunction = (pool: Pool, name: string) => (): Promise<void> => {
4142
return new Promise(resolve => {
42-
hubDB.end(() => {
43-
sequencerDB.end(() => {
44-
console.log('[mysql] Database connection pools closed');
45-
resolve();
46-
});
43+
pool.end((err: Error | null) => {
44+
if (err) {
45+
log.error(`[mysql] Error closing ${name}:`, err);
46+
} else {
47+
log.info(`[mysql] ${name} connection pool closed`);
48+
}
49+
resolve();
4750
});
4851
});
4952
};
5053

54+
const closeHubDB = createCloseFunction(hubDB, 'hubDB');
55+
const closeSequencerDB = createCloseFunction(sequencerDB, 'sequencerDB');
56+
57+
// Register each database shutdown individually with the shutdown helper
58+
registerStopFunction(closeHubDB);
59+
registerStopFunction(closeSequencerDB);
60+
5161
export { hubDB as default, sequencerDB };

src/helpers/shutdown.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import log from './log';
2+
3+
export type ShutdownFunction = () => Promise<void>;
4+
5+
let isShuttingDown = false;
6+
const stopFunctions: ShutdownFunction[] = [];
7+
8+
export const getShutdownStatus = () => isShuttingDown;
9+
10+
export const registerStopFunction = (fn: ShutdownFunction) => {
11+
stopFunctions.push(fn);
12+
};
13+
14+
export const initiateShutdown = async () => {
15+
isShuttingDown = true;
16+
17+
// Execute all stop functions with individual error handling
18+
const results = await Promise.allSettled(stopFunctions.map(fn => fn()));
19+
20+
// Log any failures but don't throw
21+
results.forEach((result, index) => {
22+
if (result.status === 'rejected') {
23+
log.error(`Stop function ${index} failed:`, result.reason);
24+
}
25+
});
26+
};

src/helpers/spaces.ts

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import { capture } from '@snapshot-labs/snapshot-sentry';
2-
import snapshot from '@snapshot-labs/snapshot.js';
31
import networks from '@snapshot-labs/snapshot.js/src/networks.json';
42
import { uniq } from 'lodash';
3+
import { createLoop } from './loop';
54
import log from './log';
65
import db from './mysql';
76

@@ -329,20 +328,12 @@ export async function getSpace(id: string) {
329328
};
330329
}
331330

332-
export default async function run() {
333-
while (true) {
334-
try {
335-
log.info('[spaces] Start spaces refresh');
336-
337-
await loadSpaces();
338-
await loadSpacesMetrics();
339-
340-
sortSpaces();
341-
log.info('[spaces] End spaces refresh');
342-
} catch (e: any) {
343-
capture(e);
344-
log.error(`[spaces] failed to load spaces, ${JSON.stringify(e)}`);
345-
}
346-
await snapshot.utils.sleep(RUN_INTERVAL);
331+
export default createLoop({
332+
name: 'spaces',
333+
interval: RUN_INTERVAL,
334+
task: async () => {
335+
await loadSpaces();
336+
await loadSpacesMetrics();
337+
sortSpaces();
347338
}
348-
}
339+
});

src/helpers/strategies.ts

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { URL } from 'url';
22
import { capture } from '@snapshot-labs/snapshot-sentry';
33
import snapshot from '@snapshot-labs/snapshot.js';
4-
import log from './log';
4+
import { createLoop } from './loop';
55
import { spacesMetadata } from './spaces';
66

77
export let strategies: any[] = [];
@@ -14,8 +14,6 @@ const scoreApiURL: URL = new URL(
1414
scoreApiURL.pathname = '/api/strategies';
1515
const uri = scoreApiURL.toString();
1616

17-
let consecutiveFailsCount = 0;
18-
1917
async function loadStrategies() {
2018
const res = await snapshot.utils.getJSON(uri);
2119

@@ -57,23 +55,11 @@ async function loadStrategies() {
5755
);
5856
}
5957

60-
async function run() {
61-
while (true) {
62-
try {
63-
log.info('[strategies] Start strategies refresh');
64-
await loadStrategies();
65-
consecutiveFailsCount = 0;
66-
log.info('[strategies] End strategies refresh');
67-
} catch (e: any) {
68-
consecutiveFailsCount++;
69-
70-
if (consecutiveFailsCount >= 3) {
71-
capture(e);
72-
}
73-
log.error(`[strategies] failed to load ${JSON.stringify(e)}`);
74-
}
75-
await snapshot.utils.sleep(RUN_INTERVAL);
76-
}
77-
}
78-
79-
run();
58+
createLoop({
59+
name: 'strategies',
60+
interval: RUN_INTERVAL,
61+
task: async () => {
62+
await loadStrategies();
63+
},
64+
maxConsecutiveFailsBeforeCapture: 3
65+
});

src/index.ts

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import graphql from './graphql';
88
import { checkKeycard } from './helpers/keycard';
99
import log from './helpers/log';
1010
import initMetrics from './helpers/metrics';
11-
import { closeDatabase } from './helpers/mysql';
1211
import rateLimit from './helpers/rateLimit';
12+
import { initiateShutdown } from './helpers/shutdown';
1313
import refreshSpacesCache from './helpers/spaces';
1414
import './helpers/strategies';
1515

@@ -38,20 +38,24 @@ const server = app.listen(PORT, () =>
3838
);
3939

4040
const gracefulShutdown = async (signal: string) => {
41-
console.log(`Received ${signal}. Starting graceful shutdown...`);
42-
43-
server.close(async () => {
44-
console.log('Express server closed.');
45-
46-
try {
47-
await closeDatabase();
48-
console.log('Graceful shutdown completed.');
49-
process.exit(0);
50-
} catch (error) {
51-
console.error('Error during shutdown:', error);
52-
process.exit(1);
53-
}
41+
log.info(`Received ${signal}. Starting graceful shutdown...`);
42+
43+
// Stop all background processes (loops + database)
44+
await initiateShutdown();
45+
log.info('Background processes stopped.');
46+
47+
// Close Express server
48+
server.close(() => {
49+
log.info('Express server closed.');
50+
log.info('Graceful shutdown completed.');
51+
process.exit(0);
5452
});
53+
54+
// Fallback timeout for the entire shutdown process
55+
setTimeout(() => {
56+
log.error('Graceful shutdown timeout exceeded, forcing exit');
57+
process.exit(1);
58+
}, 15000); // 15 seconds total shutdown timeout
5559
};
5660

5761
process.on('SIGINT', () => gracefulShutdown('SIGINT'));

0 commit comments

Comments
 (0)