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

Clean up threading book-keeping at fork when monkey-patched #611

Merged
merged 1 commit into from Sep 1, 2020

Conversation

tipabu
Copy link
Contributor

@tipabu tipabu commented May 8, 2020

Previously, if we patched threading then forked (or, in some cases, used the subprocess module), Python would log an ignored exception like

Exception ignored in: <function _after_fork at 0x7f16493489d8>
Traceback (most recent call last):
  File "/usr/lib/python3.7/threading.py", line 1335, in _after_fork
    assert len(_active) == 1
AssertionError:

This comes down to threading in Python 3.7+ having an import side-effect of registering an at-fork callback. When we re-import threading to patch it, the old (but still registered) callback still points to the old thread-tracking dict, rather than the new dict that's actually doing the tracking.

Now, register our own at_fork hook that will fix up the dict reference before threading's _at_fork runs and put it back afterwards.

Closes #592

@@ -312,6 +312,26 @@ def monkey_patch(**on):
for attr_name in deleted:
if hasattr(orig_mod, attr_name):
delattr(orig_mod, attr_name)

if name == 'threading' and sys.version_info >= (3, 7):
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Or maybe this should say ... and hasattr(_os, 'register_at_fork'):?

Copy link
Member

Choose a reason for hiding this comment

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

i think yes, they may remove register_at_fork in future version; checking py version in test is the right move though

@codecov-io
Copy link

Codecov Report

Merging #611 into master will not change coverage.
The diff coverage is n/a.

Impacted file tree graph

@@          Coverage Diff          @@
##           master   #611   +/-   ##
=====================================
  Coverage      46%    46%           
=====================================
  Files          81     81           
  Lines        7977   7977           
  Branches     1366   1366           
=====================================
  Hits         3700   3700           
  Misses       4019   4019           
  Partials      258    258           
Flag Coverage Δ
#ipv6 15% <ø> (ø)
#py27epolls 49% <ø> (ø)
#py27poll 49% <ø> (-1%) ⬇️
#py27selects 49% <ø> (ø)
#py34epolls 42% <ø> (ø)
#py34poll 42% <ø> (+<1%) ⬆️
#py34selects 42% <ø> (ø)
#py35epolls 42% <ø> (+<1%) ⬆️
#py35poll 42% <ø> (ø)
#py35selects 42% <ø> (-1%) ⬇️
#py36epolls 42% <ø> (ø)
#py36poll 42% <ø> (+<1%) ⬆️
#py36selects 42% <ø> (-1%) ⬇️
#py37epolls 42% <ø> (+<1%) ⬆️
#py37poll 42% <ø> (+<1%) ⬆️
#py37selects 42% <ø> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 88164c9...3df6c27. Read the comment docs.

Copy link
Contributor

@clayg clayg left a comment

Choose a reason for hiding this comment

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

I can't imagine anything that makes this change any better - amazing work @tipabu

the register_at_fork handling seems very inline with the cpython change that prompted all this and having a clean slate after fork seems like a very reasonable state to struggle towards; so even if it's complicated at least we're all pulling in the same direction to try and reduce complexity.

_global_dict=original('threading').current_thread.__globals__,
_patched=orig_mod
):
_prefork_active = [None]
Copy link
Contributor

Choose a reason for hiding this comment

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

py2/3 is SO annoying, even tho this code is py3 only you can't use nonlocal because it's a SyntaxError on py2 🙄

# but old pythons make it difficult to ensure
if sys.version_info >= (3, 7):
check(1, threading, 'child post-fork patched')
check(1, _threading, 'child post-fork original')
Copy link
Contributor

Choose a reason for hiding this comment

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

so this wasn't expected - and it may be the crux of the new 3.7 feature that's applying some cleaning.

This is closer to what I expected from my experience with python and forking with threads:

diff --git a/tests/patcher_test.py b/tests/patcher_test.py
index 0bebe39..ff8dc24 100644
--- a/tests/patcher_test.py
+++ b/tests/patcher_test.py
@@ -326,6 +326,9 @@ if os.fork() == 0:
     if sys.version_info >= (3, 7):
         check(1, threading, 'child post-fork patched')
         check(1, _threading, 'child post-fork original')
+    else:
+        check(2, threading, 'child post-fork patched')
+        check(3, _threading, 'child post-fork original')
     check(1, eventlet.green.threading, 'child post-fork green')
     exit()
 else:

But I don't have enough experience outside of python to say if that's "reasonable"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

IDK -- seems pretty consistent with how (unpatched) stdlib threading works across a variety of versions:

$ cat test_threads_and_forking.py 
import os
import threading
import time

threads = [threading.Thread(target=time.sleep, args=(1,)) for _ in range(2)]
for t in threads: t.start()
time.sleep(0.1)  # Give py2 threads a chance to actually start
print('pre-fork: {}'.format(len(threading._active)))
label = 'child' if os.fork() == 0 else 'parent'
print('post-fork ({}): {}'.format(label, len(threading._active)))
print('liveness ({}): {}'.format(label, [t.is_alive() for t in threads]))
for t in threads: t.join()
if label == 'child':
	exit()
else:
	os.wait()
$ for p in python2.7 python3.5 python3.6 python3.7 python3.8; do $p --version ; $p test_threads_and_forking.py ; echo ; done
Python 2.7.17
pre-fork: 3
post-fork (parent): 3
liveness (parent): [True, True]
post-fork (child): 1
liveness (child): [False, False]

Python 3.5.9
pre-fork: 3
post-fork (parent): 3
liveness (parent): [True, True]
post-fork (child): 1
liveness (child): [False, False]

Python 3.6.9
pre-fork: 3
post-fork (parent): 3
liveness (parent): [True, True]
post-fork (child): 1
liveness (child): [False, False]

Python 3.7.7
pre-fork: 3
post-fork (parent): 3
liveness (parent): [True, True]
post-fork (child): 1
liveness (child): [False, False]

Python 3.8.3
pre-fork: 3
post-fork (parent): 3
liveness (parent): [True, True]
post-fork (child): 1
liveness (child): [False, False]

_global_dict=original('threading').current_thread.__globals__,
_patched=orig_mod
):
_prefork_active = [None]
Copy link
Contributor

Choose a reason for hiding this comment

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

py2/3 is SO annoying - even tho this code is py3 only you can't use nonlocal because it's a SyntaxError on py2 🙄

Copy link
Member

@4383 4383 left a comment

Choose a reason for hiding this comment

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

LGTM

@nzlosh
Copy link

nzlosh commented Jul 30, 2020

Is there any time frame as to when this PR will be merged. I have high hopes this will resolve a block on supporting Python 3.7+ on a project that depends on eventlet.

@codecov-commenter
Copy link

Codecov Report

Merging #611 into master will decrease coverage by 6%.
The diff coverage is n/a.

Impacted file tree graph

@@           Coverage Diff           @@
##           master   #611     +/-   ##
=======================================
- Coverage      52%    46%     -7%     
=======================================
  Files          87     81      -6     
  Lines        9784   7977   -1807     
  Branches     1719   1366    -353     
=======================================
- Hits         5159   3700   -1459     
+ Misses       4275   4019    -256     
+ Partials      350    258     -92     
Flag Coverage Δ
#ipv6 15% <ø> (-2%) ⬇️
#py27epolls 49% <ø> (ø)
#py27poll 49% <ø> (-1%) ⬇️
#py27selects 49% <ø> (ø)
#py34epolls 42% <ø> (-7%) ⬇️
#py34poll 42% <ø> (-7%) ⬇️
#py34selects 42% <ø> (-8%) ⬇️
#py35epolls 42% <ø> (-8%) ⬇️
#py35poll 42% <ø> (-8%) ⬇️
#py35selects 42% <ø> (-8%) ⬇️
#py36epolls 42% <ø> (ø)
#py36poll 42% <ø> (+<1%) ⬆️
#py36selects 42% <ø> (-1%) ⬇️
#py37epolls 42% <ø> (+<1%) ⬆️
#py37poll 42% <ø> (+<1%) ⬆️
#py37selects 42% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
eventlet/websocket.py
eventlet/wsgi.py
eventlet/greenthread.py
eventlet/__init__.py
eventlet/queue.py

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 88164c9...0d46519. Read the comment docs.

@temoto
Copy link
Member

temoto commented Aug 28, 2020

@tipabu thanks for your work; please say if i'm pushing hospitality with rebasing your branch. Extracted test code into isolated/ file. Otherwise LGTM and ready to merge if you are fine with these changes.

@tipabu
Copy link
Contributor Author

tipabu commented Aug 29, 2020

By all means, push away! I'm always a fan of whatever gets fixes mergeable faster 👍

The extraction makes sense; was just looking at the precedent around it. I may have set those sleeps too short to be robust in CI, but other than that, :shipit:

Previously, if we patched threading then forked (or, in some cases, used
the subprocess module), Python would log an ignored exception like

   Exception ignored in: <function _after_fork at 0x7f16493489d8>
   Traceback (most recent call last):
     File "/usr/lib/python3.7/threading.py", line 1335, in _after_fork
       assert len(_active) == 1
   AssertionError:

This comes down to threading in Python 3.7+ having an import side-effect
of registering an at-fork callback. When we re-import threading to patch
it, the old (but still registered) callback still points to the old
thread-tracking dict, rather than the new dict that's actually doing the
tracking.

Now, register our own at_fork hook that will fix up the dict reference
before threading's _at_fork runs and put it back afterwards.

Closes eventlet#592
@codecov-commenter
Copy link

codecov-commenter commented Aug 29, 2020

Codecov Report

Merging #611 into master will decrease coverage by 0%.
The diff coverage is 0%.

Impacted file tree graph

@@          Coverage Diff           @@
##           master    #611   +/-   ##
======================================
- Coverage      44%     44%   -1%     
======================================
  Files          87      87           
  Lines       11813   11831   +18     
  Branches     1773    1774    +1     
======================================
  Hits         5269    5269           
- Misses       6150    6168   +18     
  Partials      394     394           
Flag Coverage Δ
#ipv6 16% <0%> (-1%) ⬇️
#py27epolls ?
#py27poll ?
#py27selects 55% <0%> (-1%) ⬇️
#py35epolls 49% <0%> (-1%) ⬇️
#py35poll 49% <0%> (-1%) ⬇️
#py35selects 49% <0%> (-1%) ⬇️
#py36epolls 49% <0%> (-1%) ⬇️
#py36poll 49% <0%> (-1%) ⬇️
#py36selects 49% <0%> (-1%) ⬇️
#py37epolls ?
#py37poll 49% <0%> (-1%) ⬇️
#py37selects 49% <0%> (-1%) ⬇️
#py38epolls 41% <0%> (-1%) ⬇️
#py38poll 41% <0%> (-1%) ⬇️
#py38selects ?

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
eventlet/patcher.py 32% <0%> (-2%) ⬇️
eventlet/green/ssl.py 48% <0%> (-1%) ⬇️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 44bfc1e...115103d. Read the comment docs.

@temoto temoto merged commit 115103d into eventlet:master Sep 1, 2020
@temoto
Copy link
Member

temoto commented Sep 1, 2020

v0.27.0 on PyPI

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.

Monkey patching interferes with threading in Python 3.7
7 participants