Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Signal delivered to a subprocess triggers parent's handler #75670

Closed
Kentzo mannequin opened this issue Sep 16, 2017 · 8 comments
Closed

Signal delivered to a subprocess triggers parent's handler #75670

Kentzo mannequin opened this issue Sep 16, 2017 · 8 comments
Labels
3.7 (EOL) end of life topic-asyncio topic-multiprocessing type-bug An unexpected behavior, bug, or error

Comments

@Kentzo
Copy link
Mannequin

Kentzo mannequin commented Sep 16, 2017

BPO 31489
Nosy @pitrou, @njsmith, @1st1, @Kentzo

Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

Show more details

GitHub fields:

assignee = None
closed_at = None
created_at = <Date 2017-09-16.05:25:54.129>
labels = ['type-bug', '3.7', 'expert-asyncio']
title = "Signal delivered to a subprocess triggers parent's handler"
updated_at = <Date 2017-12-20.00:25:03.190>
user = 'https://github.com/Kentzo'

bugs.python.org fields:

activity = <Date 2017-12-20.00:25:03.190>
actor = 'njs'
assignee = 'none'
closed = False
closed_date = None
closer = None
components = ['asyncio']
creation = <Date 2017-09-16.05:25:54.129>
creator = 'Ilya.Kulakov'
dependencies = []
files = []
hgrepos = []
issue_num = 31489
keywords = []
message_count = 7.0
messages = ['302321', '302323', '308652', '308670', '308677', '308678', '308696']
nosy_count = 4.0
nosy_names = ['pitrou', 'njs', 'yselivanov', 'Ilya.Kulakov']
pr_nums = []
priority = 'normal'
resolution = None
stage = None
status = 'open'
superseder = None
type = 'behavior'
url = 'https://bugs.python.org/issue31489'
versions = ['Python 3.6', 'Python 3.7']

@Kentzo
Copy link
Mannequin Author

Kentzo mannequin commented Sep 16, 2017

It looks like a signal delivered to multiprocessing's process implicitly created by ProcessPoolExecutor triggers signal handler in the parent:

from concurrent.futures import ProcessPoolExecutor
import asyncio
import os
import signal
import sys
import time


def background_task():
    print(f"Running background task {os.getpid()}", file=sys.stderr)
    time.sleep(15)
    print("Exiting background task", file=sys.stderr)


async def task():
    print("Running task")

    executor = ProcessPoolExecutor()
    with executor:
        loop = asyncio.get_event_loop()

        try:
            await loop.run_in_executor(executor, background_task)
        except asyncio.CancelledError:
            print("Cancelling task 1", file=sys.stderr)

            try:
                await asyncio.sleep(15)
            except asyncio.CancelledError:
                print("Cancelling task 2", file=sys.stderr)
                raise

            raise

def main():
    def terminate(coro):
        print(f"Terminating {os.getpid()}")
        coro.cancel()

    loop = asyncio.get_event_loop()
    t = asyncio.ensure_future(task())
    loop.add_signal_handler(signal.SIGTERM, terminate, t)

    try:
        print(f"Running {os.getpid()}", file=sys.stderr)
        loop.run_until_complete(t)
    finally:
        loop.run_until_complete(loop.shutdown_asyncgens())
        loop.close()


if __name__ == '__main__':
    main()
  1. Normal execution:

Running 1000
Running task
Running background task 9999
Exiting background task

  1. Sending SIGTERM to parent once:

Running 1000
Running task
Running background task 9999

< kill -s SIGTERM 9999

Terminating 1000
Cancelling task 1
Exiting background task

  1. Sending SIGTERM to parent twice:

Running 1000
Running task
Running background task 9999

< kill -s SIGTERM 1000

Terminating 1000
Cancelling task 1

< kill -s SIGTERM 1000

Terminating 1000
Cancelling task 2
Exiting background task

  1. Sending SIGTERM to once to parent and once to child:

Running 1000
Running task
Running background task 9999

< kill -s SIGTERM 1000

Terminating 1000
Cancelling task 1

< kill -s SIGTERM 9999

Terminating 1000
Cancelling task 2
Exiting background task

As you can see, sending SIGTERM into a subprocess somehow triggered a signal handler inside a parent. This is unexpected.

@Kentzo Kentzo mannequin added topic-asyncio type-bug An unexpected behavior, bug, or error labels Sep 16, 2017
@Kentzo
Copy link
Mannequin Author

Kentzo mannequin commented Sep 16, 2017

I think either loop's signal handler should not be called from a subprocess or at the very least, os.getpid / os.getpgrp should report correctly.

@pitrou
Copy link
Member

pitrou commented Dec 19, 2017

I get the feeling (without actually investigating) that this is because a fork()-created process inherits all the parent's configuration, including (in this case) signal handlers and whatever file descriptor was configured to receive signal events using signal.set_wakeup_fd(). So the child process, when it receives a signal, also writes on that file descriptor which happens to be the same underlying self-pipe as in the parent.

In Python 3.6 it seems there isn't much you can't do against this (well, nothing obvious, in any case). In Python 3.7, you'll have two fixes available in ProcessPoolExecutor (*):

  • either pass an initializer function that resets signal configuration to a sane default state
  • or pass a "forkserver" multiprocessing context that will avoid inheritance issues in the process pool workers

(*) see docs at https://docs.python.org/3.7/library/concurrent.futures.html#concurrent.futures.ProcessPoolExecutor

I would generally recommend using "forkserver" whenever possible, since it eliminates all those inheritance issues by design.

@pitrou pitrou added the 3.7 (EOL) end of life label Dec 19, 2017
@njsmith
Copy link
Contributor

njsmith commented Dec 19, 2017

Ouch, yes, that's a tricky bug. This is definitely caused by the way that asyncio internally converts signals into messages along a pipe (well, socket, but same thing), and then after a fork-without-exec the child keeps writing into that pipe. It's exacerbated by asyncio's choice to use the self-pipe as its source of truth about which signals have arrived vs just a wake-up pipe (see [1]), but that's not really the main issue; even without this we'd get spurious wakeups and other unpleasantness.

In addition to the workarounds Antoine suggested, it would possibly make sense for forked children to disable any wakeup_fd, perhaps in PyOS_AfterFork or by adding a getpid() check to the C level signal handler. I can't think of any cases where you actually want to processes to share the same wake-up fd. And even if this isn't fixed at that level, it would make sense for asyncio to use the new atfork module to do something similar for asyncio specifically.

Also relevant: python/asyncio#347

[1] dabeaz/curio#118

@Kentzo
Copy link
Mannequin Author

Kentzo mannequin commented Dec 19, 2017

Can you suggest an alternative to ProcessPoolExecutor for 3.6?

@pitrou
Copy link
Member

pitrou commented Dec 19, 2017

You may switch to multiprocessing.Pool (with the "forkserver" method).

Otherwise, you could workaround it by executing a function on all workers that will reset the signal configuration. To maximize the chances that it does get executed on all workers, you could add a sleep() call inside it...

@njsmith
Copy link
Contributor

njsmith commented Dec 20, 2017

It might be possible to create ProcessPoolExecutor and get it to spawn all the workers *before* you start the asyncio loop. It looks like ProcessPoolExecutor delays spawning workers until the first piece of work is submitted, but at that point it spawns all of them immediately, so something like this might work:

executor = ProcessPoolExecutor(...)
executor.submit(lambda: None).wait()
with asyncio.get_event_loop() as loop:
    loop.run_until_complete(...)

@ezio-melotti ezio-melotti transferred this issue from another repository Apr 10, 2022
@kumaraditya303
Copy link
Contributor

Duplicate of #94454

@kumaraditya303 kumaraditya303 marked this as a duplicate of #94454 Oct 6, 2022
@kumaraditya303 kumaraditya303 closed this as not planned Won't fix, can't repro, duplicate, stale Oct 6, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3.7 (EOL) end of life topic-asyncio topic-multiprocessing type-bug An unexpected behavior, bug, or error
Projects
Status: Done
Development

No branches or pull requests

4 participants