Skip to content

Commit 2bd68d3

Browse files
feat: Handle SIGTERM and shutdown gracefully (serverpod#3256)
1 parent c8742f3 commit 2bd68d3

File tree

3 files changed

+424
-14
lines changed

3 files changed

+424
-14
lines changed

packages/serverpod/lib/src/server/health_check_manager.dart

+25-2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class HealthCheckManager {
2323

2424
bool _running = false;
2525
Timer? _timer;
26+
Completer<void>? _pendingHealthCheck;
2627

2728
/// Creates a new [HealthCheckManager].
2829
HealthCheckManager(this._pod, this.onCompleted);
@@ -41,12 +42,34 @@ class HealthCheckManager {
4142
}
4243

4344
/// Stops the health check manager.
44-
void stop() {
45+
Future<void> stop() async {
4546
_running = false;
4647
_timer?.cancel();
48+
await _pendingHealthCheck?.future;
4749
}
4850

49-
void _performHealthCheck() async {
51+
Future<void> _performHealthCheck() async {
52+
final completer = Completer<void>();
53+
_pendingHealthCheck = completer;
54+
55+
try {
56+
await _innerPerformHealthCheck();
57+
completer.complete();
58+
} catch (e, stackTrace) {
59+
_pod.internalSubmitEvent(
60+
ExceptionEvent(e, stackTrace, message: 'Error in health check'),
61+
space: OriginSpace.framework,
62+
context: DiagnosticEventContext(
63+
serverId: _pod.serverId,
64+
serverRunMode: _pod.commandLineArgs.role.name,
65+
serverName: '',
66+
),
67+
);
68+
completer.completeError(e, stackTrace);
69+
}
70+
}
71+
72+
Future<void> _innerPerformHealthCheck() async {
5073
if (_pod.commandLineArgs.role == ServerpodRole.maintenance) {
5174
stdout.writeln('Performing health checks.');
5275
}

packages/serverpod/lib/src/server/serverpod.dart

+110-12
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,11 @@ class Serverpod {
542542
commandLineArgs.role == ServerpodRole.serverless) {
543543
var serversStarted = true;
544544

545+
ProcessSignal.sigint.watch().listen(_onInterruptSignal);
546+
if (!Platform.isWindows) {
547+
ProcessSignal.sigterm.watch().listen(_onShutdownSignal);
548+
}
549+
545550
// Serverpod Insights.
546551
if (Features.enableInsights) {
547552
if (_isValidSecret(config.serviceSecret)) {
@@ -710,6 +715,28 @@ class Serverpod {
710715
}
711716
}
712717

718+
void _onShutdownSignal(ProcessSignal signal) {
719+
stdout.writeln('${signal.name} (${signal.signalNumber}) received'
720+
', time: ${DateTime.now().toUtc()}');
721+
shutdown(exitProcess: true, signalNumber: signal.signalNumber);
722+
}
723+
724+
bool _interruptSignalSent = false;
725+
726+
void _onInterruptSignal(ProcessSignal signal) {
727+
stdout.writeln('${signal.name} (${signal.signalNumber}) received'
728+
', time: ${DateTime.now().toUtc()}');
729+
730+
if (_interruptSignalSent) {
731+
stdout
732+
.writeln('SERVERPOD immediate exit, time: ${DateTime.now().toUtc()}');
733+
exit(128 + signal.signalNumber);
734+
}
735+
736+
_interruptSignalSent = true;
737+
shutdown(exitProcess: true, signalNumber: signal.signalNumber);
738+
}
739+
713740
Future<bool> _startInsightsServer() async {
714741
var endpoints = internal.Endpoints();
715742

@@ -841,25 +868,68 @@ class Serverpod {
841868

842869
/// Shuts down the Serverpod and all associated servers.
843870
/// If [exitProcess] is set to false, the process will not exit at the end of the shutdown.
844-
Future<void> shutdown({bool exitProcess = true}) async {
845-
try {
846-
await _internalSession.close();
847-
await redisController?.stop();
848-
await server.shutdown();
849-
await _webServer?.stop();
850-
await _serviceServer?.shutdown();
851-
await _futureCallManager?.stop();
852-
_healthCheckManager?.stop();
871+
Future<void> shutdown({
872+
bool exitProcess = true,
873+
int? signalNumber,
874+
}) async {
875+
stdout.writeln(
876+
'SERVERPOD initiating shutdown, time: ${DateTime.now().toUtc()}');
877+
878+
var futures = [
879+
_shutdownTestAuditor(),
880+
_internalSession.close(),
881+
redisController?.stop(),
882+
server.shutdown(),
883+
_webServer?.stop(),
884+
_serviceServer?.shutdown(),
885+
_futureCallManager?.stop(),
886+
_healthCheckManager?.stop(),
887+
].nonNulls;
888+
889+
Object? shutdownError;
890+
await futures.wait.onError((ParallelWaitError e, stackTrace) {
891+
shutdownError = e;
892+
var errors = e.errors;
893+
if (errors is Iterable<AsyncError?>) {
894+
for (var error in errors.nonNulls) {
895+
_reportException(
896+
error.error,
897+
error.stackTrace,
898+
message: 'Error in server shutdown',
899+
);
900+
}
901+
} else {
902+
_reportException(
903+
errors,
904+
stackTrace,
905+
message: 'Error in serverpod shutdown',
906+
);
907+
}
908+
return e.values;
909+
});
853910

911+
try {
854912
// This needs to be closed last as it is used by the other services.
855913
await _databasePoolManager?.stop();
856914
} catch (e, stackTrace) {
857-
_reportException(e, stackTrace, message: 'Error in Serverpod shutdown');
858-
rethrow;
915+
shutdownError = e;
916+
_reportException(
917+
e,
918+
stackTrace,
919+
message: 'Error in database pool manager shutdown',
920+
);
859921
}
860922

923+
stdout.writeln(
924+
'SERVERPOD shutdown completed, time: ${DateTime.now().toUtc()}');
925+
861926
if (exitProcess) {
862-
exit(0);
927+
int conventionalExitCode = signalNumber != null ? 128 + signalNumber : 0;
928+
exit(shutdownError != null ? 1 : conventionalExitCode);
929+
}
930+
931+
if (shutdownError != null) {
932+
throw shutdownError as Object;
863933
}
864934
}
865935

@@ -934,6 +1004,34 @@ class Serverpod {
9341004
}
9351005
}
9361006

1007+
// _shutdownTestAuditor is a stop-gap test approach to verify the robustness
1008+
// of the shutdown process.
1009+
// It is not intended to be used in production and it is not an encouraged pattern.
1010+
// The real solution is to enable dynamic service plugins for Serverpod,
1011+
// with which could plug in custom services for test scenarios without affecting
1012+
// production code like this.
1013+
Future<void>? _shutdownTestAuditor() {
1014+
var testThrowerDelaySeconds = int.tryParse(
1015+
Platform.environment['_SERVERPOD_SHUTDOWN_TEST_AUDITOR'] ?? '',
1016+
);
1017+
if (testThrowerDelaySeconds == null) {
1018+
return null;
1019+
}
1020+
return Future(() {
1021+
stderr.writeln('serverpod shutdown test auditor enabled');
1022+
if (testThrowerDelaySeconds == 0) {
1023+
throw Exception('serverpod shutdown test auditor throwing');
1024+
} else {
1025+
return Future.delayed(
1026+
Duration(seconds: testThrowerDelaySeconds),
1027+
() {
1028+
throw Exception('serverpod shutdown test auditor throwing');
1029+
},
1030+
);
1031+
}
1032+
});
1033+
}
1034+
9371035
/// Experimental API for Serverpod.
9381036
///
9391037
/// Note: These features are experimental and may change or be removed

0 commit comments

Comments
 (0)