Signal child process(es) when pdm receives a signal
Is your feature request related to a problem? Please describe.
When using pdm run, the target process is executed as a child of pdm, and is subsequently passed stdin/stdout. The child process then becomes interactible by the end user, including via signals.
In a Docker container, pdm becomes pid 1, not the Python script. Docker's behavior on executing docker stop is to send a SIGTERM to only pid 1, which pid 1 is responsible for propogating to its children if applicable.
Because pdm does not currently propogate signals to child processes, it is not possible to use Docker's normal signaling behavior to safely shutdown a Python script executed by pdm. Instead, the request to stop will stall until Docker SIGKILLs pid 1, after 10 seconds of waiting.
Describe the solution you'd like
pdm should propogate signals to the child process to be handled.
Other Notes
I have prepared a small test environment locking pdm==2.1.1. With a docker daemon running, do the following:
make build runmake logs(to demonstrate the script executing)make stop(sending SIGTERM to the container's pid 1)
Note that in 3, the command stalls for 10 seconds; this is Docker's behavior, expecting pid 1 to shut down gracefully in that time or else it will SIGKILL pid 1 and forcibly close the container.
Python's subprocess doesn't offer a good mechanism to forward ALL signals, you can only enumerate the signal list and handle them by subprocess.send_signal. With that said, PDM is by no means a good process for Docker's number 1 process, and you shouldn't rely on that.
Maybe you should kill group instead of just killing the pid=1
I wrote most of the block below before a friend pointed out this part of pdm's documentation. This will let us sidestep the issue and not depend on pdm as pid=1. However, pdm having the capability to signal child processes based on received signals would be beneficial, and utilities such as doppler have this capability. That said, I will leave the feature up to you
I think that behavior of enumerating the signal list and handling each, rather than all, is the standard. I haven't looked to see what pdm currently does with signals, but I can say that it at least appears to ignore SIGTERM; you can preserve this behavior by handling each of the transmissible signals in the same way, passing them to all known child processes, without inspecting further.
Unfortunately, there is no way offered by Docker to signal a container process other than pid=1 when using docker stop or docker kill (which are the same utility with different default signals).
"The main process inside the container" is always pid=1, and is executed based on the CMD and ENTRYPOINT commands. In other words, if we want to execute a python process in Docker with its dependencies covered by pdm (barring the multi-stage build process offered in the docs), we have to do the following:
ENTRYPOINT ["pdm", "run", "python"]
CMD [...] # your python script
Or, more like our actual practice:
Dockerfile:
...
ENTRYPOINT ["pdm", "run", "python"]
docker-compose.yml:
version: "3.8"
services:
worker:
build:
context: "."
dockerfile: "./Dockerfile"
command: ./my_script.py
Theoretically, we could roll our own shutdown process by grabbing the container by ID and executing kill -SIG[NAME] [pid] targeting the process we want to shut down gracefully, but this runs the risk of failing to actually shut down the container since pid=1 would still be alive. It would be better to use Docker's built in system for this purpose.
Now it only handles SIGINT, so would adding SIGTERM and SIGKILL to the list suffice?
Adding SIGTERM would do it; adding SIGKILL would do nothing, as the kernel would step in and kill the process before the process could handle the signal.