11const compression = require ( 'compression' ) ;
22const config = require ( 'config' ) ;
33const express = require ( 'express' ) ;
4+ const fs = require ( 'fs' ) ;
45const log = require ( 'npmlog' ) ;
56const morgan = require ( 'morgan' ) ;
67const path = require ( 'path' ) ;
78const Problem = require ( 'api-problem' ) ;
89const querystring = require ( 'querystring' ) ;
10+ const Writable = require ( 'stream' ) . Writable ;
911
1012const keycloak = require ( './src/components/keycloak' ) ;
1113const 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
2428const app = express ( ) ;
2529app . use ( compression ( ) ) ;
@@ -30,20 +34,52 @@ app.use(express.urlencoded({ extended: true }));
3034log . level = config . get ( 'server.logLevel' ) ;
3135log . 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
3455if ( 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
4570app . 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
4884apiRouter . 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
80116app . 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 */
119169function 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 */
136197function 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
192252module . exports = app ;
0 commit comments