@@ -542,6 +542,11 @@ class Serverpod {
542
542
commandLineArgs.role == ServerpodRole .serverless) {
543
543
var serversStarted = true ;
544
544
545
+ ProcessSignal .sigint.watch ().listen (_onInterruptSignal);
546
+ if (! Platform .isWindows) {
547
+ ProcessSignal .sigterm.watch ().listen (_onShutdownSignal);
548
+ }
549
+
545
550
// Serverpod Insights.
546
551
if (Features .enableInsights) {
547
552
if (_isValidSecret (config.serviceSecret)) {
@@ -710,6 +715,28 @@ class Serverpod {
710
715
}
711
716
}
712
717
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
+
713
740
Future <bool > _startInsightsServer () async {
714
741
var endpoints = internal.Endpoints ();
715
742
@@ -841,25 +868,68 @@ class Serverpod {
841
868
842
869
/// Shuts down the Serverpod and all associated servers.
843
870
/// 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
+ });
853
910
911
+ try {
854
912
// This needs to be closed last as it is used by the other services.
855
913
await _databasePoolManager? .stop ();
856
914
} 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
+ );
859
921
}
860
922
923
+ stdout.writeln (
924
+ 'SERVERPOD shutdown completed, time: ${DateTime .now ().toUtc ()}' );
925
+
861
926
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 ;
863
933
}
864
934
}
865
935
@@ -934,6 +1004,34 @@ class Serverpod {
934
1004
}
935
1005
}
936
1006
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
+
937
1035
/// Experimental API for Serverpod.
938
1036
///
939
1037
/// Note: These features are experimental and may change or be removed
0 commit comments