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

Fix #1978: Support Python 3.7's new pre- and post-fork handlers #1995

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

nickwilliams-eventbrite

This change addresses #1978 by ensuring that, in Python 3.7 and newer versions, the following always happens:

  • PyOS_BeforeFork() is called before the parent process calls fork()
  • PyOS_AfterFork_Parent() is called in the master process after the parent process calls fork()
  • PyOS_AfterFork_Child() is called in all worker processes after the parent process calls fork()

This new behavior ignores the value of the py-call-osafterfork setting (only in Python 3.7 and newer), because not calling the before- and after-fork hooks is invalid behavior for CPython extensions in Python 3.7 and newer.

I tested this by compiling, installing, and using in our new Python 3.7 systems:

mkdir /usr/lib/uwsgi
mkdir /usr/lib/uwsgi/plugins
cd /usr/local/src
git clone https://github.com/nickwilliams-eventbrite/uwsgi.git
cd uwsgi
git checkout fix-py37-errors
python uwsgiconfig.py --build core
python uwsgiconfig.py --plugin plugins/rsyslog core rsyslog
python uwsgiconfig.py --plugin plugins/python core python3
UWSGI_PLUGINS_BUILDER_CFLAGS= python uwsgiconfig.py --extra-plugin https://github.com/Datadog/uwsgi-dogstatsd dogstatsd
cp -v ./uwsgi /usr/bin/uwsgi-core
cp -v ./dogstatsd_plugin.so ./python3_plugin.so ./rsyslog_plugin.so /usr/lib/uwsgi/plugins/

This also adds a Python 3.7 build to the Travis build (I won't know if that works until posting this PR).

@nickwilliams-eventbrite
Copy link
Author

[bump] Could I get some 👀 on this? Thanks!

Copy link
Collaborator

@xrmx xrmx left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes looks fine to me but there's a bit that puzzles me. We are adding a new callback for the plugins to set when we have a uwsgi hook just one line above below which would be very cool to reuse if possible.

@nickwilliams-eventbrite
Copy link
Author

The changes looks fine to me but there's a bit that puzzles me. We are adding a new callback for the plugins to set when we have a uwsgi hook just one line above which would be very cool to reuse if possible.

I'm not sure I understand what bit puzzles you. Could you elaborate more? It sounds like you're saying that it would be very cool to re-use the new master_start callback I'm adding. Anyone is free to re-use this new callback.

@xrmx
Copy link
Collaborator

xrmx commented May 15, 2019

What I mean is that it would be cool to reuse, if possible, the hook at master.c:643 https://github.com/unbit/uwsgi/pull/1995/files#diff-d752b2bf332e435717f74037eb981f1fL642 instead of adding the master_start callback in the uwsgi_plugin struct.

@nickwilliams-eventbrite
Copy link
Author

Oh, so you're talking about this line, which is already there?

uwsgi_hooks_run(uwsgi.hook_master_start, "master-start", 1);

I thought about that at first, but I couldn't figure out how to detect hook_master_start in a plugin. Could you explain a bit more about how I could attach to hook_master_start within a plugin? AFAICT, the pattern of lopping over the list of plugins and calling a hook on each plugin is the pattern used everywhere else for all plugin hooks.

@nickwilliams-eventbrite
Copy link
Author

@xrmx: Per my most recent comment about two weeks ago, could you advise on how I can change this to work as you suggest? I can't figure out how to make what you suggest work.

@xrmx
Copy link
Collaborator

xrmx commented May 30, 2019

@nickwilliams-eventbrite AFAIK there's no interface for internal callers to use the hook system. Maybe @unbit can chime in to sort out if it's a good idea or not?


#ifdef REQUIRES_PyOS_BeforeAndAfterFork_ParentAndChild
if (getpid() == masterpid) {
uwsgi_log("*** WARNING: post_fork called for masterpid (pid: %d)! ***\n", uwsgi.mypid);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

without master this will print the warning, we should probably check uwsgi.master_process

spawned uWSGI worker 1 (and the only) (pid: 16127, cores: 1)
*** WARNING: post_fork called for masterpid (pid: 16127)! ***

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So do you think this is correct?

    if (getpid() == masterpid) {
        if (uwsgi.master_process) {
            uwsgi_log("*** WARNING: post_fork called for masterpid (pid: %d)! ***\n", uwsgi.mypid);
        }
    } else {
        // debug log if enabled
        PyOS_AfterFork_Child();
    }

Or should I do this (doesn't seem right to me, but just checking)?

    if (getpid() == masterpid && uwsgi.master_process) {
        uwsgi_log("*** WARNING: post_fork called for masterpid (pid: %d)! ***\n", uwsgi.mypid);
    } else {
        // debug log if enabled
        PyOS_AfterFork_Child();
    }

@@ -194,7 +195,7 @@ struct uwsgi_option uwsgi_python_options[] = {
{"py-sharedarea", required_argument, 0, "create a sharedarea from a python bytearray object of the specified size", uwsgi_opt_add_string_list, &up.sharedarea, 0},
#endif

{"py-call-osafterfork", no_argument, 0, "enable child processes running cpython to trap OS signals", uwsgi_opt_true, &up.call_osafterfork, 0},
{"py-call-osafterfork", no_argument, 0, "enable child processes running cpython to trap OS signals (ignored in Python 3.7+, because PyOS_BeforeFork and PyOS_AfterFork_* are always called)", uwsgi_opt_true, &up.call_osafterfork, 0},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd add just the (ignored in Python 3.7) bits. Eventually we can add a more detailed explanation if it has been called with --py-call-osafterfork.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I can remove the extra verbiage.

@edufelipe
Copy link
Contributor

@nickwilliams-eventbrite

Hi there! Thanks for this code, it really helped our issue. I have a few quick questions if you don't mind my asking:

  1. Are you guys using this patch in production?
  2. Do you guys run the master branch on production, instead of the latest tag from the uwsgi-2.0 branch?
  3. Could I backport it to 2.0.18 and open a pull request against the uwsgi-2.0 branch? I'll mark you as the author and just be the committer.

Thanks again for all this work :D

@awelzel
Copy link
Contributor

awelzel commented Sep 23, 2019

Hey @nickwilliams-eventbrite ,

thanks for taking a stab at that issue. I've looked through your changes and attempted to run them with the test app from #1978 - some thoughts as follows.

  1. The exception shown in RuntimeError at startup in Python 3.7.1+ when using --py-call-osafterfork option and logging module #1978 does happen (again) when a worker is respawned after hitting max-requests. E.g starting with the following and sending enough requests should reproduce:
./uwsgi --master --workers 2 --max-requests 5 --min-worker-lifetime 1  --http-socket :9090  --plugin ./python37_plugin.so --wsgi-file ./tests/testosafterfork.py
...
...The work of process 27456 is done (max requests reached (5 >= 5)). Seeya!
worker 1 killed successfully (pid: 27456)
Respawned uWSGI worker 1 (new pid: 27474)
Exception ignored in: <function _releaseLock at 0x7fa63364af28>
Traceback (most recent call last):
  File "/usr/lib/python3.7/logging/__init__.py", line 226, in _releaseLock
    _lock.release()
RuntimeError: cannot release un-acquired lock
Ignoring exception from logging atfork Exception ignored in: <function _after_at_fork_weak_calls at 0x7fa63364c268>                                                                     
Traceback (most recent call last):
  File "/usr/lib/python3.7/logging/__init__.py", line 269, in _after_at_fork_weak_calls
    _at_fork_weak_calls('release')
  File "/usr/lib/python3.7/logging/__init__.py", line 261, in _at_fork_weak_calls
    method_name, "method:", err, file=sys.stderr)
  File "/usr/lib/python3.7/logging/__init__.py", line 1066, in __repr__
    name += ' '
TypeError: unsupported operand type(s) for +=: 'int' and 'str'
  1. Using master_fixup() for the PyOS_BeforeFork() does seem a bit off. master_fixup(0) is called before spawning the initial set of workers (with or without an actual master), but only once. And then master_fixup(1) is called in every worker after spawning it, too. I'm somewhat thinking that the master part in the name doesn't actually describe that it'll be executed only in the master, but means something else. IMO calling the hooks should actually be within uwsgi_respawn_worker() where the actual fork is happening.

  2. IIUC, the idea is to call PyOS_BeforeFork() and the PyOS_AfterFork_Parent() for every fork() call. I don't think that this is given with your changes, but the flow of execution is a bit hard to follow.

Almost certainly 3) and 2) somehow lead to 1).

Hmm, hmm. I stared at it for quite a while and dabbled around a bit - see branch below. The only real solution I came up with involved adding two plugin hooks pre_fork() and post_fork_parent(). It does survive the simple and respawned test-case, though and maybe it's possible to fold master_fixup() in one...

https://github.com/awelzel/uwsgi/tree/before-after-fork

Thoughts? @xrmx - happy to open that as a separate request if that could be useful?

edufelipe added a commit to onsigntv/uwsgi that referenced this pull request Nov 5, 2019
This commit works around a requirement introduced in Python 3.7 that all
calls to fork() be preceeded by calls to PyOS_BeforeFork() in the parent
and PyOS_AfterFork_Parent() and PyOS_AfterFork_Child() in the respective
processes.

Also, Python 3.7 always initialize threads, but if the option is not set
on uWSGI the thread-local data is not set and it causes random crashes,
therefore it is now a fatal error if that option is not set.

A plethora of new hooks were added because Python specifies that after
fork the first calls must be to PyOS_AfterFork_* and unfortunately uWSGI
was doing a bunch of stuff before calling the existing post_fork() hook.

We decided to modify the function uwsgi_fork() itself because it would
make it easier to handle all use-cases: mules, spoolers, re-forking of
workers, lazy apps, etc.

Reference:

https://docs.python.org/3.7/c-api/sys.html#c.PyOS_BeforeFork
https://docs.python.org/3.7/c-api/init.html#c.PyEval_InitThreads
unbit#1995
unbit#1978
https://github.com/awelzel/uwsgi/tree/before-after-fork
markbreedlove pushed a commit to mitodl/odl-video-service that referenced this pull request Apr 8, 2020
Remove the py-call-osafterfork setting from uwsgi.ini. This is about to
be deprecated, and is buggy with Python 3.7.

References:

- mitodl/micromasters#4569
- unbit/uwsgi#1978
- unbit/uwsgi#1995
markbreedlove added a commit to mitodl/odl-video-service that referenced this pull request Apr 16, 2020
Remove the py-call-osafterfork setting from uwsgi.ini. This is about to
be deprecated, and is buggy with Python 3.7.

References:

- mitodl/micromasters#4569
- unbit/uwsgi#1978
- unbit/uwsgi#1995
edufelipe added a commit to onsigntv/uwsgi that referenced this pull request Oct 15, 2021
This commit works around a requirement introduced in Python 3.7 that all
calls to fork() be preceeded by calls to PyOS_BeforeFork() in the parent
and PyOS_AfterFork_Parent() and PyOS_AfterFork_Child() in the respective
processes.

Also, Python 3.7 always initialize threads, but if the option is not set
on uWSGI the thread-local data is not set and it causes random crashes,
therefore it is now a fatal error if that option is not set.

A plethora of new hooks were added because Python specifies that after
fork the first calls must be to PyOS_AfterFork_* and unfortunately uWSGI
was doing a bunch of stuff before calling the existing post_fork() hook.

We decided to modify the function uwsgi_fork() itself because it would
make it easier to handle all use-cases: mules, spoolers, re-forking of
workers, lazy apps, etc.

Reference:

https://docs.python.org/3.7/c-api/sys.html#c.PyOS_BeforeFork
https://docs.python.org/3.7/c-api/init.html#c.PyEval_InitThreads
unbit#1995
unbit#1978
https://github.com/awelzel/uwsgi/tree/before-after-fork
@zaheerabbas-prodigal
Copy link

Is there any update on this PR getting merged and released?

Has anyone found a solution that fixes the Runtime Exceptions, would appreciate any help here. Thanks

@edufelipe
Copy link
Contributor

@zaheerabbas-prodigal They already did something similar in d80f9d9

My company does not use this fix because we found that it is not fully working for mules that fork, but we haven't got the time to port this to Python 3.11 so we are still running this branch under Python 3.8.

@zaheerabbas-prodigal
Copy link

@edufelipe - thanks for a quick response. Do you use this branch nickwilliams-eventbrite:fix-py37-errors or the PR branch #2388. We used uWSGI version 2.0.21, the PR #2388 was merged in to the versions we have. But running with flag py-call-uwsgi-fork-hooks raises the weird Runtime lock exception.

@edufelipe
Copy link
Contributor

@zaheerabbas-prodigal we actually use our own branch based on uWSGI 2.0.20: https://github.com/onsigntv/uwsgi/tree/lts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants