Skip to content

Commit 38e5df1

Browse files
authored
Merge pull request #161 from jujaga/chore/maintenance
General Maintenance updates
2 parents 4bed47f + b1058a3 commit 38e5df1

15 files changed

Lines changed: 2923 additions & 3095 deletions

File tree

app/app.js

100755100644
Lines changed: 116 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
const compression = require('compression');
22
const config = require('config');
33
const express = require('express');
4+
const fs = require('fs');
45
const log = require('npmlog');
56
const morgan = require('morgan');
67
const path = require('path');
78
const Problem = require('api-problem');
89
const querystring = require('querystring');
10+
const Writable = require('stream').Writable;
911

1012
const keycloak = require('./src/components/keycloak');
1113
const v1Router = require('./src/routes/v1');
@@ -18,8 +20,10 @@ const state = {
1820
connections: {
1921
data: false
2022
},
23+
ready: false,
2124
shutdown: false
2225
};
26+
let probeId;
2327

2428
const app = express();
2529
app.use(compression());
@@ -30,20 +34,52 @@ app.use(express.urlencoded({ extended: true }));
3034
log.level = config.get('server.logLevel');
3135
log.addLevel('debug', 1500, { fg: 'cyan' });
3236

37+
let logFileStream;
38+
let teeStream;
39+
if (config.has('server.logFile')) {
40+
// Write to logFile in append mode
41+
logFileStream = fs.createWriteStream(config.get('server.logFile'), { flags: 'a' });
42+
teeStream = new Writable({
43+
objectMode: true,
44+
write: (data, _, done) => {
45+
process.stdout.write(data);
46+
logFileStream.write(data);
47+
done();
48+
}
49+
});
50+
log.disableColor();
51+
log.stream = teeStream;
52+
}
53+
3354
// Skip if running tests
3455
if (process.env.NODE_ENV !== 'test') {
3556
const morganOpts = {
3657
// Skip logging kube-probe requests
3758
skip: (req) => req.headers['user-agent'] && req.headers['user-agent'].includes('kube-probe')
3859
};
60+
if (config.has('server.logFile')) {
61+
morganOpts.stream = teeStream;
62+
}
3963
// Add Morgan endpoint logging
4064
app.use(morgan(config.get('server.morganFormat'), morganOpts));
65+
// Initialize connections and exit if unsuccessful
4166
initializeConnections();
4267
}
4368

4469
// Use Keycloak OIDC Middleware
4570
app.use(keycloak.middleware());
4671

72+
// Block requests until service is ready
73+
app.use((_req, res, next) => {
74+
if (state.shutdown) {
75+
new Problem(503, { details: 'Server is shutting down' }).send(res);
76+
} else if (!state.ready) {
77+
new Problem(503, { details: 'Server is not ready' }).send(res);
78+
} else {
79+
next();
80+
}
81+
});
82+
4783
// Frontend configuration endpoint
4884
apiRouter.use('/config', (_req, res, next) => {
4985
try {
@@ -78,6 +114,11 @@ app.use(staticFilesPath, express.static(path.join(__dirname, 'frontend/dist')));
78114
// Handle 500
79115
// eslint-disable-next-line no-unused-vars
80116
app.use((err, _req, res, _next) => {
117+
// Attempt to reset DB connection
118+
if (!state.shutdown) {
119+
dataConnection.resetConnection();
120+
}
121+
81122
if (err.stack) {
82123
log.error(err.stack);
83124
}
@@ -112,81 +153,100 @@ process.on('unhandledRejection', err => {
112153
}
113154
});
114155

156+
// Graceful shutdown support
157+
process.on('SIGTERM', shutdown);
158+
process.on('SIGINT', shutdown);
159+
process.on('SIGUSR1', shutdown);
160+
process.on('SIGUSR2', shutdown);
161+
process.on('exit', () => {
162+
log.info('Exiting...');
163+
});
164+
115165
/**
116166
* @function shutdown
117-
* Begins shutting down this application. It will hard exit after 3 seconds.
167+
* Shuts down this application after at least 3 seconds.
118168
*/
119169
function shutdown() {
120-
if (!state.shutdown) {
121-
log.info('Received kill signal. Shutting down...');
122-
state.shutdown = true;
123-
// Wait 3 seconds before hard exiting
124-
setTimeout(() => process.exit(), 3000);
125-
}
170+
log.info('Received kill signal. Shutting down...');
171+
// Wait 3 seconds before starting cleanup
172+
if (!state.shutdown) setTimeout(cleanup, 3000);
126173
}
127174

128-
process.on('SIGTERM', shutdown);
129-
process.on('SIGINT', shutdown);
175+
/**
176+
* @function cleanup
177+
* Cleans up connections in this application.
178+
*/
179+
function cleanup() {
180+
log.info('Service no longer accepting traffic');
181+
state.shutdown = true;
182+
183+
log.info('Cleaning up...');
184+
clearInterval(probeId);
185+
186+
dataConnection.close(() => process.exit());
187+
188+
// Wait 10 seconds max before hard exiting
189+
setTimeout(() => process.exit(), 10000);
190+
}
130191

131192
/**
132193
* @function initializeConnections
133-
* Initializes the database, queue and email connections
194+
* Initializes the database connections
134195
* This will force the application to exit if it fails
135196
*/
136197
function initializeConnections() {
137198
// Initialize connections and exit if unsuccessful
138-
try {
139-
const tasks = [
140-
dataConnection.checkAll()
141-
];
142-
143-
Promise.all(tasks)
144-
.then(results => {
145-
state.connections.data = results[0];
146-
147-
if (state.connections.data) log.info('DataConnection', 'Connected');
148-
})
149-
.catch(error => {
150-
log.error(error);
151-
log.error('initializeConnections', `Initialization failed: Database OK = ${state.connections.data}`);
152-
})
153-
.finally(() => {
154-
state.ready = Object.values(state.connections).every(x => x);
155-
if (!state.ready) shutdown();
156-
});
157-
158-
} catch (error) {
159-
log.error('initializeConnections', 'Connection initialization failure', error.message);
160-
if (!state.ready) shutdown();
161-
}
162-
163-
// Start asynchronous connection probe
164-
connectionProbe();
199+
const tasks = [
200+
dataConnection.checkAll()
201+
];
202+
203+
Promise.all(tasks)
204+
.then(results => {
205+
state.connections.data = results[0];
206+
207+
if (state.connections.data) log.info('DataConnection', 'Reachable');
208+
})
209+
.catch(error => {
210+
log.error('initializeConnections', `Initialization failed: Database OK = ${state.connections.data}`);
211+
log.error('initializeConnections', 'Connection initialization failure', error.message);
212+
if (!state.ready) {
213+
process.exitCode = 1;
214+
shutdown();
215+
}
216+
})
217+
.finally(() => {
218+
state.ready = Object.values(state.connections).every(x => x);
219+
if (state.ready) {
220+
log.info('Service ready to accept traffic');
221+
// Start periodic 10 second connection probe check
222+
probeId = setInterval(checkConnections, 10000);
223+
}
224+
});
165225
}
166226

167227
/**
168-
* @function connectionProbe
169-
* Periodically checks the status of the connections at a specific interval
170-
* This will force the application to exit a connection fails
171-
* @param {integer} [interval=10000] Number of milliseconds to wait before
228+
* @function checkConnections
229+
* Checks Database connectivity
230+
* This will force the application to exit if a connection fails
172231
*/
173-
function connectionProbe(interval = 10000) {
174-
const checkConnections = () => {
175-
if (!state.shutdown) {
176-
const tasks = [
177-
dataConnection.checkConnection()
178-
];
232+
function checkConnections() {
233+
const wasReady = state.ready;
234+
if (!state.shutdown) {
235+
const tasks = [
236+
dataConnection.checkConnection()
237+
];
179238

239+
Promise.all(tasks).then(results => {
240+
state.connections.data = results[0];
241+
state.ready = Object.values(state.connections).every(x => x);
242+
if (!wasReady && state.ready) log.info('Service ready to accept traffic');
180243
log.verbose(JSON.stringify(state));
181-
Promise.all(tasks).then(results => {
182-
state.connections.data = results[0];
183-
state.ready = Object.values(state.connections).every(x => x);
184-
if (!state.ready) shutdown();
185-
});
186-
}
187-
};
188-
189-
setInterval(checkConnections, interval);
244+
if (!state.ready) {
245+
process.exitCode = 1;
246+
shutdown();
247+
}
248+
});
249+
}
190250
}
191251

192252
module.exports = app;

app/config/custom-environment-variables.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"realm": "SERVER_KC_REALM",
4949
"serverUrl": "SERVER_KC_SERVERURL"
5050
},
51+
"logFile": "SERVER_LOGFILE",
5152
"logLevel": "SERVER_LOGLEVEL",
5253
"morganFormat": "SERVER_MORGANFORMAT",
5354
"port": "SERVER_PORT"

app/config/database.js

Lines changed: 0 additions & 29 deletions
This file was deleted.

0 commit comments

Comments
 (0)