Skip to content

Commit be5e0f1

Browse files
erikapmadnificent
authored andcommitted
Introduce beforeExit and switch to watchman
beforeExit allows to run a series of commands before exit occurs. The function is async. - setExitHandler is an unpublished escape hatch not intended for public use - exitHandler is not intended for public use but is necessary in start-server.js which has been given its own file nodemon has been removed watchman has its own set of issues. It is not clear when they appear exactly but the issues suggest our current approach may work in our current setting. Because we have not seen any extra errors during testing we'll accept this as being as good as it gets. We switched because nodemon has some open issues and we could not get it to reliably forward signals necessary to clean up state during development.
1 parent 74809c7 commit be5e0f1

File tree

7 files changed

+93
-49
lines changed

7 files changed

+93
-49
lines changed

Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ FROM node:20-bookworm
33
LABEL maintainer="[email protected]"
44

55
RUN apt-get update && apt-get -y upgrade && apt-get -y install git openssh-client rsync jq
6+
RUN cd /tmp/ && wget https://github.com/watchexec/watchexec/releases/download/v2.3.2/watchexec-2.3.2-x86_64-unknown-linux-gnu.deb && dpkg -i watchexec-2.3.2-x86_64-unknown-linux-gnu.deb
67

78
ENV MU_SPARQL_ENDPOINT='http://database:8890/sparql'
89
ENV MU_APPLICATION_GRAPH='http://mu.semte.ch/application'

boot.sh

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@ then
44
# Run live-reload development
55
echo "" > /tmp/service-status.lock
66
echo "" > /tmp/service-restart
7-
exec /usr/src/app/node_modules/.bin/nodemon \
8-
--watch /app \
9-
--watch /config \
10-
--watch /tmp/service-restart \
11-
--ext js,coffee,ts,mjs,cjs,json \
12-
--exec /usr/src/app/run-development.sh
7+
8+
exec watchexec \
9+
--project-origin="/" \
10+
--watch="/app" \
11+
--watch="/config" \
12+
--exts="js,coffee,ts,mjs,cjs,json" \
13+
--stop-timeout="60s" \
14+
--stop-signal="SIGUSR2" \
15+
--shell=none --no-process-group \
16+
--restart \
17+
/usr/src/app/run-development.sh
1318
elif [ "$NODE_ENV" == "production" ]
1419
then
1520
exec /usr/src/app/run-production.sh

helpers/mu/index.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { app, errorHandler, setExitHandler, getExitHandler } from './server.js';
1+
import { app, errorHandler, beforeExit, exitHandler, setExitHandler } from './server.js';
22
import sparql from './sparql.js';
33
import { v1 as uuidV1 } from 'uuid';
44

@@ -22,7 +22,8 @@ const mu = {
2222
sparqlEscapeBool: sparql.sparqlEscapeBool,
2323
uuid,
2424
errorHandler,
25-
getExitHandler,
25+
beforeExit,
26+
exitHandler,
2627
setExitHandler
2728
};
2829

@@ -56,8 +57,9 @@ export {
5657
sparqlEscapeBool,
5758
uuid,
5859
errorHandler,
59-
setExitHandler,
60-
getExitHandler
60+
beforeExit,
61+
exitHandler,
62+
setExitHandler
6163
};
6264

6365
export default mu;

helpers/mu/server.js

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,34 @@ const errorHandler = function(err, req, res, next) {
3434
});
3535
};
3636

37+
/** @type { (() => Promise<void>)[] } */
38+
const beforeExitCallbacks = [];
39+
40+
/**
41+
* Define an async callback function to run on server shutdown
42+
* @param { () => Promise<void> } callback Callback function to execute
43+
*/
44+
function beforeExit(callback) {
45+
beforeExitCallbacks.push(callback);
46+
}
47+
3748
// managing server cleanup
38-
let exitHandler = function(server) {
49+
let exitHandler = async function(server) {
3950
console.debug("Shutting down server");
40-
server.close( () => {
41-
console.debug("Shut down complete");
51+
if (beforeExitCallbacks.length) {
52+
console.debug("Executing before exit callbacks");
53+
for (let callback of beforeExitCallbacks) {
54+
await callback(server);
55+
}
56+
}
57+
await new Promise((acc) => {
58+
server.close( () => {
59+
console.debug("Shut down complete");
60+
acc();
61+
});
4262
});
4363
};
4464

45-
const getExitHandler = function() {
46-
return exitHandler;
47-
}
48-
4965
/**
5066
* Sets a new handler for shutting down the server.
5167
*
@@ -61,6 +77,7 @@ export default app;
6177
export {
6278
app,
6379
errorHandler,
80+
beforeExit,
6481
setExitHandler,
65-
getExitHandler
82+
exitHandler
6683
}

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
"env-var": "^7.0.0",
2424
"express": "^4.17.1",
2525
"express-http-context": "~1.2.4",
26-
"nodemon": "^3.1.10",
2726
"sparql-client-2": "https://github.com/erikap/node-sparql-client.git",
2827
"typescript": "^4.6.2",
2928
"uuid": "^9.0.0"

run-development.sh

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
#!/bin/bash
2-
sleep 0.1
32
SERVICE_STATUS=$(cat /tmp/service-status.lock)
43
if [ "$SERVICE_STATUS" == "exit_after_compile" ]
54
then
6-
echo "More changed files detected during compilation. Continuing compilation and scheduling another compile run."
7-
exit 0
5+
echo "More changed files detected during compilation. Continuing compilation and scheduling another compile run."
6+
exit 0
87
fi
98

10-
trap '{ SERVICE_STATUS=$(cat /tmp/service-status.lock); if [ $SERVICE_STATUS == "compiling" ]; then echo "exit_after_compile" > /tmp/service-status.lock; elif [ $SERVICE_STATUS == "running" ]; then exit 0; fi }' SIGUSR2 > /dev/null 2>&1
9+
# watchexec may send SIGUSR2 during compilation when more files have changed.
10+
# This requires to finish the compilation so no files are lingering about, but
11+
# stop the script before starting node. watchexec will then start the script
12+
# again.
13+
trap '{ SERVICE_STATUS=$(cat /tmp/service-status.lock); if [ $SERVICE_STATUS == "compiling" ]; then echo "exit_after_compile" > /tmp/service-status.lock; fi }' SIGUSR2 > /dev/null 2>&1
1114

1215
echo "compiling" > /tmp/service-status.lock
1316

@@ -25,7 +28,6 @@ source ./helpers.sh
2528
cd /usr/src/app/
2629

2730

28-
2931
######################
3032
# Install dependencies
3133
######################
@@ -43,9 +45,9 @@ fi
4345
cmp -s /app/package.json /tmp/last-build-service-package.json
4446
if [ "$?" == "1" ] && [ $IS_FIRST_RUN == false ]
4547
then
46-
PACKAGE_JSON_CHANGED=true
48+
PACKAGE_JSON_CHANGED=true
4749
else
48-
PACKAGE_JSON_CHANGED=false
50+
PACKAGE_JSON_CHANGED=false
4951
fi
5052

5153
if [ -f /app/package.json ]
@@ -78,23 +80,24 @@ fi
7880
# Determine npm command and install dependencies
7981
if [ -f /app/package.json ]
8082
then
81-
if $IS_FIRST_RUN
82-
then
83-
if $HAS_PACKAGE_LOCK
83+
if $IS_FIRST_RUN
84+
then
85+
if $HAS_PACKAGE_LOCK
86+
then
87+
npm_install_command=ci
88+
else
89+
npm_install_command=install
90+
fi
91+
elif $PACKAGE_JSON_CHANGED
8492
then
85-
npm_install_command=ci
86-
else
87-
npm_install_command=install
93+
npm_install_command=install
8894
fi
89-
elif $PACKAGE_JSON_CHANGED
90-
then
91-
npm_install_command=install
92-
fi
9395
fi
9496
./npm-install-dependencies.sh development $npm_install_command
9597
./validate-package-json.sh
9698
touch /tmp/dependencies-installed-once-for-dev
9799

100+
98101
###############
99102
# Transpilation
100103
###############
@@ -103,19 +106,30 @@ touch /tmp/dependencies-installed-once-for-dev
103106

104107
cp /usr/src/app/helpers/mu/package.json /usr/src/dist/node_modules/mu/
105108

109+
106110
##############
107111
# Start server
108112
##############
109113
SERVICE_STATUS=$(cat /tmp/service-status.lock)
110114
if [ "$SERVICE_STATUS" == "exit_after_compile" ]
111115
then
112-
echo "" > /tmp/service-status.lock
113-
touch /tmp/service-restart
114-
exit 0
116+
echo "" > /tmp/service-status.lock
117+
exit 0
115118
else
116-
echo "running" > /tmp/service-status.lock
117-
cd /usr/src/dist/
118-
node \
119-
--inspect="0.0.0.0:9229" \
120-
./start-server.js
119+
echo "running" > /tmp/service-status.lock
120+
cd /usr/src/dist/
121+
node \
122+
--inspect="0.0.0.0:9229" \
123+
./start-server.js &
124+
NODE_PID=$!
125+
trap 'kill -s SIGUSR2 $NODE_PID' SIGUSR2 # SIGINT and SIGTERM are not necessary here now
126+
127+
# TODO: Is this ps + while verification step is still necessary? The
128+
# process appeared not to fully exit after `wait`.
129+
while ps -p $NODE_PID > /dev/null
130+
do
131+
wait $NODE_PID
132+
done
133+
echo "" > /tmp/service-status.lock
134+
exit 0
121135
fi

start-server.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { app, getExitHandler } from 'mu';
1+
import { app, exitHandler } from 'mu';
22
import './app.js'; // load the user's app
33

44
var port = process.env.PORT || '80';
@@ -10,9 +10,15 @@ const server = app.listen(port, hostname, function () {
1010
});
1111

1212
// faster stopping
13-
process.on('SIGTERM', () => {
14-
getExitHandler()(server)
13+
process.on('SIGTERM', async () => {
14+
await exitHandler(server);
15+
process.exit(0);
1516
});
16-
process.on('SIGINT', () => {
17-
getExitHandler()(server);
17+
process.on('SIGINT', async () => {
18+
await exitHandler(server);
19+
process.exit(0);
20+
});
21+
process.on('SIGUSR2', async () => {
22+
await exitHandler(server);
23+
process.exit(0);
1824
});

0 commit comments

Comments
 (0)