Package: graceful-shutdown
Graceful ShutdownThis package provides a context manager which traps signals that would normally cause the program to exit and delay
exiting until (a) a suitable breakpoint is reached, (b) the context manager exits, or (c) the maximum execution time is
reached. In addition, it causes programs run on Windows using python.exe (but not pythonw.exe) to raiseSystemExitexceptions when the window is closed or the system is shutdown.UsageA typical use case is as follows:fromgraceful_shutdownimportShutdownProtectionwithShutdownProtection(4)asprotected_block:# BLOCK 1try:# do some stuff you don't want interruptedexcept(SystemExit,KeyboardInterrupt)asex:# rollback as needed# only called if there is a timeout in the blockprotected_block.allow_break()# BLOCK 2try:# do some other stuffexcept(SystemExit,KeyboardInterrupt)asex:# rollback as neededIn this example, if the Python process is requested to exit viaSIGINT,SIGTERM,SIGQUIT,SIGHUP,SIGBREAK,CTRL_C_EVENT,CTRL_BREAK_EVENT,CTRL_CLOSE_EVENT,CTRL_LOGOFF_EVENT, orCTRL_SHUTDOWN_EVENT, the blocks of
code before and afterallow_break()are guaranteed to have at least 4 seconds of execution time before an exception is
raised. This time is tracked from when the context manager is initialized or from the most recent call toallow_break()orrenew()on the context object.It is recommended to set the execution time to no more than 4.5 seconds as Windows typically allows only 5 seconds for
shutdown routines to execute afterCTRL_CLOSE_EVENTis fired (when the console window is closed). If you know that you
will be in a Unix environment, you can extend this up to your Unix system's time between issuingSIGTERMandSIGKILLwhen a shutdown is issued (typically 90 seconds). If you are using a tool such as NSSM on Windows, the interval betweenCTRL-Cand taskkill being called will vary, so consider your configuration carefully. The default execution time is
4.5 seconds on Windows and 89.5 seconds on other systems which should be suitable for most use cases.If you do not want to useShutdownProtection()but you do wantCTRL_CLOSE_EVENT,CTRL_SHUTDOWN_EVENT, andCTRL_LOGOFF_EVENTto raiseSystemExitinstead of abruptly ending the process, callconfigure_shutdown_manager()before starting your program:fromgraceful_shutdownimportconfigure_shutdown_managerconfigure_shutdown_manager()# do your work hereIn addition, if you want to useShutdownProtection()inside a thread, it is important to configure the shutdown
manager in the main thread so that it can register the signal handlers properly:fromgraceful_shutdownimportconfigure_shutdown_manager,ShutdownProtectionimportthreadingimporttimeclassProtectedThread(threading.Thread):defrun(self):withShutdownProtection()asprotected_block:whileTrue:protected_block.allow_break()# do something protected heretime.sleep(0.1)# Call this first, otherwise signal registration won't happen properlyconfigure_shutdown_manager()# Create, start, and then join our threadt=ProtectedThread()t.daemon=True# Ensures the thread exits when the main thread exitst.start()t.join()# Now if you SIGINT the thread, the main program will wait for the current loop of the thread to finish before exiting.Configurationconfigure_shutdown_manager()takes several arguments that can be used to configure the behaviour. The default value of
None for each will be ignored (the previously set value will be kept).terminate_on_logoff: If set toFalse,CTRL_LOGOFF_EVENTwill be ignored (defaults toTrue)terminate_on_hup: If set toFalse,SIGHUPwill be ignored (defaults toTrue)max_exec_time: Overrides the default value formax_exec_time(4.5 seconds on Windows, 89.5 on Unix)max_attempts: Sets the maximum number of signals or events to handle before interrupting the running process
(defaults to 3). With the default of 3, entering CTRL-C three times will immediately halt the program with a
SystemExit exception; fewer attempts will allow the program to attempt to complete.before_termination: If specified, this function will be called immediately prior to raisingSystemExitorKeyboardInterrupt. This function should be quick to run as any significant delays may lead toSIGKILL. Set this toFalseor another non-truthy value other thanNoneto remove the previously set callback.before_hup: If specified, this function will be called immediately after receiving SIGHUP. It ignores whetherterminate_on_hupis set; if it is True, then the system will proceed to exit after this function is called.Cautionary NoteWhile this package can help you handle shutdown events gracefully, there are two caveats:Your shutdown code must complete within the execution window, otherwise many systems progress toSIGKILL. You can
avoid this by limiting actions to the minimal set of actions necessary for stability.This does not (and can not) protect againstSIGKILL(i.e. kill -9 or taskkill), or abrupt power failure. This is
not a substitute for a UPS. You should design your code and your hardware setup with this in mind, and onlySIGKILLwhen truly necessary and if data loss is not a problem.While I'm actively using this project, there are limited tests for it at the moment. Eventually I will work on the
proper test cases.Events HandledThis package handles the typical POSIX signals for requesting a program end:SIGINT(CTRL-C),SIGTERM,SIGQUIT,
andSIGHUP. Note thatSIGHUPis optional as it is sometimes used to reload configuration instead; you can override
the termination behaviour by settingterminate_on_hup = Falseand an appropriate handler forbefore_hup. If these
signals are sent on Windows by using something like NSSM,SIGINTandSIGTERMwill be handled (the others are not
supported on Windows).On Windows,SIGBREAKis also handled to respond toCTRL-BREAK. In addition, the Windows console events ofCTRL_CLOSE_EVENT,CTRL_SHUTDOWN_EVENT, andCTRL_LOGOFF_EVENTare handled. These only work if you have the pywin32
package installed, and you are running Python usingpython.exe; the console-lesspythonw.execannot receive these
signals. You can override howCTRL_LOGOFF_EVENTis handled by settingterminate_on_logoff = Falseto keep the
process running at logoff (useful for services running in NSSM).