Repository: eventlet/eventlet (Python)

Issue:
Greening existing locks doesn't always work right
Following https://github.com/eventlet/eventlet/commit/abbe7a5d6569b4518fbc68309175d93e9cf9081f (though I feel bad -- the problem seems most likely to be in code I wrote in an earlier version of https://github.com/eventlet/eventlet/pull/754), monkey-patching sometimes leaves lock references as empty dicts (??)

Given a simple repro script like
``` python
import unittest.mock
import eventlet
import sys

print(f'{sys.version.split()[0]=}, {eventlet.__version__=}')
print(f'{unittest.mock.NonCallableMock._lock=}')
eventlet.monkey_patch(thread=True)
print(f'{unittest.mock.NonCallableMock._lock=}')
```
(Note that `unittest.mock.NonCallableMock._lock` only exists for python>=3.10.9,!=3.11.0; see https://github.com/python/cpython/issues/98624 for some background.)

I get output like
```
sys.version.split()[0]='3.10.12', eventlet.__version__='0.34.2'
unittest.mock.NonCallableMock._lock=<unlocked _thread.RLock object owner=0 count=0 at 0x7fac882f5b00>
unittest.mock.NonCallableMock._lock={}
Segmentation fault (core dumped)
```
The segfault seems like a give-away that something is amiss; trying again with `-X dev` and `gdb` attached, it either continues to segfault during garbage collection:
```
sys.version.split()[0]='3.10.12', eventlet.__version__='0.34.2'
unittest.mock.NonCallableMock._lock=<unlocked _thread.RLock object owner=0 count=0 at 0x7ffff6c416d0>
unittest.mock.NonCallableMock._lock={}

Program received signal SIGSEGV, Segmentation fault.
0x000055555567d7eb in gc_list_remove (node=0x7ffff6c416c0) at ../Modules/gcmodule.c:273
273     ../Modules/gcmodule.c: No such file or directory.
(gdb) #0  0x000055555567d7eb in gc_list_remove (node=0x7ffff6c416c0) at ../Modules/gcmodule.c:273
#1  PyObject_GC_Del (op=0x7ffff6c416d0) at ../Modules/gcmodule.c:2369
#2  0x0000555555736bb8 in _PyDict_ClearFreeList (interp=0x555555b3e260) at ../Objects/dictobject.c:269
#3  0x000055555567e328 in clear_freelists (interp=0x555555b3e260) at ../Modules/gcmodule.c:1045
#4  gc_collect_main (tstate=0x555555b5a130, generation=2, n_collected=0x7fffffffe148, n_uncollectable=0x7fffffffe140, nofail=0) at ../Modules/gcmodule.c:1326
#5  0x0000555555785470 in gc_collect_with_callback (tstate=0x555555b5a130, generation=2) at ../Modules/gcmodule.c:1413
#6  0x00005555557b6aae in PyGC_Collect () at ../Modules/gcmodule.c:2099
#7  0x00005555557b44c0 in Py_FinalizeEx () at ../Python/pylifecycle.c:1781
#8  0x00005555557a5913 in Py_RunMain () at ../Modules/main.c:668
#9  0x000055555577c02d in Py_BytesMain (argc=<optimized out>, argv=<optimized out>) at ../Modules/main.c:720
(More stack frames follow...)
```
Or bombs our trying to get the repr for the last print:
```
sys.version.split()[0]='3.10.12', eventlet.__version__='0.34.2'
unittest.mock.NonCallableMock._lock=<unlocked _thread.RLock object owner=0 count=0 at 0x7ffff6c416d0>

Program received signal SIGSEGV, Segmentation fault.
type_getattro (type=<optimized out>, name=<optimized out>) at ../Objects/typeobject.c:3936
3936    ../Objects/typeobject.c: No such file or directory.
(gdb) #0  type_getattro (type=<optimized out>, name=<optimized out>) at ../Objects/typeobject.c:3936
#1  0x00005555556aae5a in PyObject_GetAttr (v=<type at remote 0x555555d8b950>, name='_lock') at ../Objects/object.c:932
#2  0x000055555569c971 in _PyEval_EvalFrameDefault (tstate=<optimized out>, f=<optimized out>, throwflag=<optimized out>) at ../Python/ceval.c:3592
#3  0x00005555556939c6 in _PyEval_EvalFrame (throwflag=0, f=Frame 0x555555bbe060, for file /home/tburke/repro.py, line 8, in <module> (), tstate=0x555555b5a130)
    at ../Include/internal/pycore_ceval.h:46
#4  _PyEval_Vector (tstate=0x555555b5a130, con=<optimized out>, locals=<optimized out>, args=<optimized out>, argcount=<optimized out>, kwnames=<optimized out>) at ../Python/ceval.c:5067
#5  0x0000555555789256 in PyEval_EvalCode (co=<code at remote 0x7ffff7798ee0>, 
    globals={'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <SourceFileLoader(name='__main__', path='/home/tburke/repro.py') at remote 0x7ffff777e710>, '__spec__': None, '__annotations__': {}, '__builtins__': <module at remote 0x7ffff7914290>, '__file__': '/home/tburke/repro.py', '__cached__': None, 'unittest': <module at remote 0x7ffff765f710>, 'eventlet': <module at remote 0x7ffff765f3b0>, 'sys': <module at remote 0x7ffff78f35f0>}, locals=<optimized out>) at ../Python/ceval.c:1134
#6  0x00005555557b4108 in run_eval_code_obj (tstate=0x555555b5a130, co=0x7ffff7798ee0, 
    globals={'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <SourceFileLoader(name='__main__', path='/home/tburke/repro.py') at remote 0x7ffff777e710>, '__spec__': None, '__annotations__': {}, '__builtins__': <module at remote 0x7ffff7914290>, '__file__': '/home/tburke/repro.py', '__cached__': None, 'unittest': <module at remote 0x7ffff765f710>, 'eventlet': <module at remote 0x7ffff765f3b0>, 'sys': <module at remote 0x7ffff78f35f0>}, 
    locals={'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <SourceFileLoader(name='__main__', path='/home/tburke/repro.py') at remote 0x7ffff777e710>, '__spec__': None, '__annotations__': {}, '__builtins__': <module at remote 0x7ffff7914290>, '__file__': '/home/tburke/repro.py', '__cached__': None, 'unittest': <module at remote 0x7ffff765f710>, 'eventlet': <module at remote 0x7ffff765f3b0>, 'sys': <module at remote 0x7ffff78f35f0>}) at ../Python/pythonrun.c:1291
#7  0x00005555557ad9cb in run_mod (mod=<optimized out>, filename=<optimized out>, 
    globals={'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <SourceFileLoader(name='__main__', path='/home/tburke/repro.py') at remote 0x7ffff777e710>, '__spec__': None, '__annotations__': {}, '__builtins__': <module at remote 0x7ffff7914290>, '__file__': '/home/tburke/repro.py', '__cached__': None, 'unittest': <module at remote 0x7ffff765f710>, 'eventlet': <module at remote 0x7ffff765f3b0>, 'sys': <module at remote 0x7ffff78f35f0>}, 
    locals={'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <SourceFileLoader(name='__main__', path='/home/tburke/repro.py') at remote 0x7ffff777e710>, '__spec__': None, '__annotations__': {}, '__builtins__': <module at remote 0x7ffff7914290>, '__file__': '/home/tburke/repro.py', '__cached__': None, 'unittest': <module at remote 0x7ffff765f710>, 'eventlet': <module at remote 0x7ffff765f3b0>, 'sys': <module at remote 0x7ffff78f35f0>}, flags=<optimized out>, arena=<optimized out>) at ../Python/pythonrun.c:1312
#8  0x00005555557b3e55 in pyrun_file (fp=fp@entry=0x555555b69280, filename=filename@entry='/home/tburke/repro.py', start=start@entry=257, 
    globals=globals@entry={'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <SourceFileLoader(name='__main__', path='/home/tburke/repro.py') at remote 0x7ffff777e710>, '__spec__': None, '__annotations__': {}, '__builtins__': <module at remote 0x7ffff7914290>, '__file__': '/home/tburke/repro.py', '__cached__': None, 'unittest': <module at remote 0x7ffff765f710>, 'eventlet': <module at remote 0x7ffff765f3b0>, 'sys': <module at remote 0x7ffff78f35f0>}, 
    locals=locals@entry={'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <SourceFileLoader(name='__main__', path='/home/tburke/repro.py') at remote 0x7ffff777e710>, '__spec__': None, '__annotations__': {}, '__builtins__': <module at remote 0x7ffff7914290>, '__file__': '/home/tburke/repro.py', '__cached__': None, 'unittest': <module at remote 0x7ffff765f710>, 'eventlet': <module at remote 0x7ffff765f3b0>, 'sys': <module at remote 0x7ffff78f35f0>}, closeit=closeit@entry=1, flags=0x7fffffffe218)
    at ../Python/pythonrun.c:1208
#9  0x00005555557b3338 in _PyRun_SimpleFileObject (fp=0x555555b69280, filename='/home/tburke/repro.py', closeit=1, flags=0x7fffffffe218) at ../Python/pythonrun.c:456
(More stack frames follow...)
```
FWIW, I see similar trouble with python 3.12.1.


Discussion:
If you got Python to segfault it's worth filing a bug against CPython cause that isn't supposed to happen.
Random brain dump, just so I don't have it stuck in my head: the current code may have additional issues, it might transform the reference lock! Which means later references might not get transformed. If so, solution is to cache `id()` and then compare to that. Will investigate both issues when I'm back to working later in the week.
Hi, @tipabu 
I'm not sure if this is an appropriate place to post this, but there is no "Github Issues" page for the other project. I am trying to work on the OpenStack Swift project you maintain, which has a dependency on Eventlet. After setting up SAIO (Swift All In One) on Ubuntu 22.04 with Python 3.10.12, I've observed that the Swift unit test results are unstable due to the same issue. Occasionally, it passes 100% (1 out of 20 runs), but it usually fails.

Segmentation fault when run `$HOME/swift/.unittests`, test stopped at 
File "/home/parallels/swift/test/unit/common/test_wsgi.py", line 1101 in test_run_server_strategy_plumbing,
the error message points to "/usr/local/lib/python3.10/dist-packages/mock/mock.py", line 699 in __getattr__, which is ` with NonCallableMock._lock:`

```
Current thread 0x0000ffff85261420 (most recent call first):
  File "/usr/local/lib/python3.10/dist-packages/mock/mock.py", line 699 in __getattr__
  File "/home/parallels/swift/swift/common/wsgi.py", line 888 in run_wsgi
  File "/home/parallels/swift/test/unit/common/test_wsgi.py", line 1101 in test_run_server_strategy_plumbing
  File "/usr/local/lib/python3.10/dist-packages/mock/mock.py", line 1452 in patched
  File "/usr/lib/python3.10/unittest/case.py", line 549 in _callTestMethod
  File "/usr/lib/python3.10/unittest/case.py", line 591 in run
  File "/usr/lib/python3.10/unittest/case.py", line 650 in __call__
  File "/usr/local/lib/python3.10/dist-packages/_pytest/unittest.py", line 333 in runtest
  File "/usr/local/lib/python3.10/dist-packages/_pytest/runner.py", line 169 in pytest_runtest_call
  File "/usr/local/lib/python3.10/dist-packages/pluggy/_callers.py", line 77 in _multicall
  File "/usr/local/lib/python3.10/dist-packages/pluggy/_manager.py", line 115 in _hookexec
  File "/usr/local/lib/python3.10/dist-packages/pluggy/_hooks.py", line 493 in __call__
  File "/usr/local/lib/python3.10/dist-packages/_pytest/runner.py", line 262 in <lambda>
  File "/usr/local/lib/python3.10/dist-packages/_pytest/runner.py", line 341 in from_call
  File "/usr/local/lib/python3.10/dist-packages/_pytest/runner.py", line 261 in call_runtest_hook
  File "/usr/local/lib/python3.10/dist-packages/_pytest/runner.py", line 222 in call_and_report
  File "/usr/local/lib/python3.10/dist-packages/_pytest/runner.py", line 133 in runtestprotocol
  File "/usr/local/lib/python3.10/dist-packages/_pytest/runner.py", line 114 in pytest_runtest_protocol
  File "/usr/local/lib/python3.10/dist-packages/pluggy/_callers.py", line 77 in _multicall
  File "/usr/local/lib/python3.10/dist-packages/pluggy/_manager.py", line 115 in _hookexec
  File "/usr/local/lib/python3.10/dist-packages/pluggy/_hooks.py", line 493 in __call__
  File "/usr/local/lib/python3.10/dist-packages/_pytest/main.py", line 350 in pytest_runtestloop
  File "/usr/local/lib/python3.10/dist-packages/pluggy/_callers.py", line 77 in _multicall
  File "/usr/local/lib/python3.10/dist-packages/pluggy/_manager.py", line 115 in _hookexec
  File "/usr/local/lib/python3.10/dist-packages/pluggy/_hooks.py", line 493 in __call__
  File "/usr/local/lib/python3.10/dist-packages/_pytest/main.py", line 325 in _main
  File "/usr/local/lib/python3.10/dist-packages/_pytest/main.py", line 271 in wrap_session
  File "/usr/local/lib/python3.10/dist-packages/_pytest/main.py", line 318 in pytest_cmdline_main
  File "/usr/local/lib/python3.10/dist-packages/pluggy/_callers.py", line 77 in _multicall
  File "/usr/local/lib/python3.10/dist-packages/pluggy/_manager.py", line 115 in _hookexec
  File "/usr/local/lib/python3.10/dist-packages/pluggy/_hooks.py", line 493 in __call__
  File "/usr/local/lib/python3.10/dist-packages/_pytest/config/__init__.py", line 169 in main
  File "/usr/local/lib/python3.10/dist-packages/_pytest/config/__init__.py", line 192 in console_main
  File "/usr/local/bin/pytest", line 8 in <module>

Extension modules: greenlet._greenlet, __original_module__thread, __original_module_select, __original_module_time, _cffi_backend, pyeclib_c, lxml._elementpath, lxml.etree, systemd._journal, systemd._reader, systemd.id128 (total: 11)
/home/parallels/swift/.unittests: line 6: 87000 Segmentation fault      (core dumped) pytest --cov-report=html:"$TOP_DIR"/cover $@
/tmp
```

When run test_wsgi seperately, no longer receive segmentfault but get three new test failures in addition, for example:
```
test/unit/common/test_wsgi.py:994 (TestWSGI.test_run_server_success)
self = <test.unit.common.test_wsgi.TestWSGI testMethod=test_run_server_success>

    def test_run_server_success(self):
        calls = defaultdict(int)
    
        def _initrp(conf_file, app_section, *args, **kwargs):
            calls['_initrp'] += 1
            return (
                {'__file__': 'test', 'workers': 0, 'bind_port': 12345},
                'logger',
                'log_name')
    
        def _loadapp(uri, name=None, **kwargs):
            calls['_loadapp'] += 1
    
        logging.logThreads = 1  # reset to default
        with mock.patch.object(wsgi, '_initrp', _initrp), \
                mock.patch.object(wsgi, 'get_socket'), \
                mock.patch.object(wsgi, 'drop_privileges') as _d_privs, \
                mock.patch.object(wsgi, 'clean_up_daemon_hygiene') as _c_hyg, \
                mock.patch.object(wsgi, 'loadapp', _loadapp), \
                mock.patch.object(wsgi, 'capture_stdio'), \
                mock.patch.object(wsgi, 'run_server'), \
                mock.patch(
                    'swift.common.wsgi.systemd_notify') as mock_notify, \
                mock.patch('swift.common.utils.eventlet') as _utils_evt:
>           rc = wsgi.run_wsgi('conf_file', 'app_section')

/home/parallels/swift/test/unit/common/test_wsgi.py:1019: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
/home/parallels/swift/swift/common/wsgi.py:874: in run_wsgi
    conf, logger, global_conf, strategy = check_config(
/home/parallels/swift/swift/common/wsgi.py:842: in check_config
    utils.monkey_patch()
/home/parallels/swift/swift/common/utils/__init__.py:491: in monkey_patch
    eventlet_monkey_patch()
/home/parallels/swift/swift/common/utils/__init__.py:483: in eventlet_monkey_patch
    eventlet.patcher.monkey_patch(all=False, socket=True, select=True,
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <MagicMock name='eventlet' id='281473493835136'>, name = 'patcher'

    def __getattr__(self, name):
        if name in {'_mock_methods', '_mock_unsafe'}:
            raise AttributeError(name)
        elif self._mock_methods is not None:
            if name not in self._mock_methods or name in _all_magics:
                raise AttributeError("Mock object has no attribute %r" % name)
        elif _is_magic(name):
            raise AttributeError(name)
        if not self._mock_unsafe and (not self._mock_methods or name not in self._mock_methods):
            if name.startswith(('assert', 'assret', 'asert', 'aseert', 'assrt')) or name in _ATTRIB_DENY_LIST:
                raise AttributeError(
                    f"{name!r} is not a valid assertion. Use a spec "
                    f"for the mock if {name!r} is meant to be an attribute.")
    
>       with NonCallableMock._lock:
E       AttributeError: __enter__

/usr/local/lib/python3.10/dist-packages/mock/mock.py:699: AttributeError
```
The failed test are test_run_server_strategy_plumbing, test_run_server_success, test_run_server_test_config, test_run_server_with_latest_eventlet, all of them called the monkey_patch method during test procedure. The error messages also points to "/usr/local/lib/python3.10/dist-packages/mock/mock.py", line 699

It's worth mentioning that if I run these tests one by one, no error will occur. I do believe that this issue is caused by the monkey_patch() method. Once test_run_server_strategy_plumbing has been excecuted, unittest.mock.NonCallableMock._lock becomes empty, which leads to the following three errors.

Now I am doubting if it is the mutex introduced in [CPython PR](https://github.com/python/cpython/pull/98798) this PR which caused this issue as all error message points to this code snippet in mock.py file:
```
        with NonCallableMock._lock:
            result = self._mock_children.get(name)
            if result is _deleted:
                raise AttributeError(name)
            elif result is None:
                wraps = None
                if self._mock_wraps is not None:
                    # XXXX should we get the attribute without triggering code
                    # execution?
                    wraps = getattr(self._mock_wraps, name)

                result = self._get_child_mock(
                    parent=self, name=name, wraps=wraps, _new_name=name,
                    _new_parent=self
                )
                self._mock_children[name]  = result

            elif isinstance(result, _SpecState):
                try:
                    result = create_autospec(
                        result.spec, result.spec_set, result.instance,
                        result.parent, result.name
                    )
                except InvalidSpecError:
                    target_name = self.__dict__['_mock_name'] or self
                    raise InvalidSpecError(
                        f'Cannot autospec attr {name!r} from target '
                        f'{target_name!r} as it has already been mocked out. '
                        f'[target={self!r}, attr={result.spec!r}]')
                self._mock_children[name]  = result
```

I will also report this issue to the lauchpad for Swift. Hope it could be mitigated soon.
https://github.com/python/cpython/issues/113631 is the underlying problem I believe.
> If you got Python to segfault it's worth filing a bug against CPython cause that isn't supposed to happen.

Yes -- that was my plan when I got a little more time after the holidays, but then @jstasiak beat me to it! Thanks! That seems likely to be the underlying cause.

> the current code may have additional issues, it might transform the reference lock!

Ugh -- yeah, that makes sense. Maybe we've been lucky with the iteration order of `gc.get_referrers()`, but I don't feel good about continuing to rely on it.

> I am trying to work on the OpenStack Swift project you maintain

Yeah, that's where I first came upon the issue :grin: @YIHONG-JIN thanks for writing up [LP #2047768](https://bugs.launchpad.net/swift/+bug/2047768)! You might try https://review.opendev.org/c/openstack/swift/+/904459 as a test fix; I still need to do more functional tests with latest eventlet. The rest of the Swift-specific discussion should probably move to that bug or patch, or to #openstack-swift on OFTC.
So I got an email talking about `gc.freeze()`, but I don't see it in the issue history; was it deleted? Even if that attempt at a fix failed, would be nice to have that fact in the issue history rather than just deleting it.
I tried `gc.freeze()` and it fixes the segfault. Also, the bug I was worried about appears to be real... Running the example above with `gc.freeze()` added before the monkey patching: 

```
$ python3.11 poc.py 
sys.version.split()[0]='3.11.7', eventlet.__version__='0.34.2.dev1+ge1325777'
unittest.mock.NonCallableMock._lock=<unlocked _thread.RLock object owner=0 count=0 at 0x7f98ee4732c0>
unittest.mock.NonCallableMock._lock=<unlocked _thread.RLock object owner=0 count=0 at 0x7f98ee4732c0>`
```

**Update:** Actually that might just be the `gc.freeze()` disables `get_referrers()` discussed below, so not informative.
> So I got an email talking about `gc.freeze()`, but I don't see it in the issue history; was it deleted? Even if that attempt at a fix failed, would be nice to have that fact in the issue history rather than just deleting it.

That was me and it was very late and I thought I was on to something. If I remember correctly the problem with the freeze approach was that `get_referrers` returned nothing.

~~Actually this is the patch I had saved and it does indeed seem to work.~~
```
diff --git a/eventlet/patcher.py b/eventlet/patcher.py
index bdccb69..8829d04 100644
--- a/eventlet/patcher.py
+++ b/eventlet/patcher.py
@@ -460,24 +460,28 @@ def _fix_py3_rlock(old, tid):
         new._owner = tid
     gc.collect()
     for ref in gc.get_referrers(old):
-        if isinstance(ref, dict):
-            for k, v in list(ref.items()):
-                if v is old:
-                    ref[k] = new
-            continue
-        if isinstance(ref, list):
-            for i, v in enumerate(ref):
-                if v is old:
-                    ref[i] = new
-            continue
+        gc.freeze()
         try:
-            ref_vars = vars(ref)
-        except TypeError:
-            pass
-        else:
-            for k, v in ref_vars.items():
-                if v is old:
-                    setattr(ref, k, new)
+            if isinstance(ref, dict):
+                for k, v in list(ref.items()):
+                    if v is old:
+                        ref[k] = new
+                continue
+            if isinstance(ref, list):
+                for i, v in enumerate(ref):
+                    if v is old:
+                        ref[i] = new
+                continue
+            try:
+                ref_vars = vars(ref)
+            except TypeError:
+                pass
+            else:
+                for k, v in ref_vars.items():
+                    if v is old:
+                        setattr(ref, k, new)
+        finally:
+            gc.unfreeze()
 
 
 def _green_os_modules():
```
I tried a variation on `gc.freeze()` above and got the following:

```
$ python poc.py 
sys.version.split()[0]='3.11.7', eventlet.__version__='0.34.2.dev1+ge1325777'
unittest.mock.NonCallableMock._lock=<unlocked _thread.RLock object owner=0 count=0 at 0x7f723996efc0>
unittest.mock.NonCallableMock._lock=<6 object at 0x7f723996efc0>
```

i.e. memory corruption, but no segfault... Good times.
So here's a different approach: forget about auto-upgrading. Instead, support only whatever stdlib modules eventlet might import before monkeypatching (e.g. logging?) and then complain _very loudly_ if monkey patching happens after anything else is imported.
Possibly my variation of `gc.freeze()` was wrong? So will continue exploring that avenue, it might be viable.
btw not sure if it is relevant but I found that setting `threading._CRLock` to `None` early (e.g. in `eventlet/__init__.py`) fixed the crash as well.
Getting the strong impression `gc.freeze()` breaks `get_referrers()` for pre-existing objects even if `gc.unfreeze()` is called later.
A third approach is something like https://github.com/twisted/twisted/blob/88151eb454ebc30d3cd69e30ab6fd762cdbe3cfa/src/twisted/python/reflect.py#L534, implementing a custom `get_referrers()` that relies not on garbage collection but on traversing objects. This won't catch everything, but it'll catch most things if the goal is only to deal with pre-existing imports.

This would also improve the situation on Python earlier than 3.10.
And I guess if you couple that with a warning, "Hey there X more RLocks we failed to upgrade, make sure you monkey patch eventlet as early as possible before any imports", might suffice.
I'm going to try to implement the approach that doesn't rely on `get_referrers()`.
OK, new approach in #866 looks like it works.



Patch:
diff --git a/eventlet/patcher.py b/eventlet/patcher.py
index bdccb6906..366de161d 100644
--- a/eventlet/patcher.py
+++ b/eventlet/patcher.py
@@ -1,3 +1,4 @@
+from __future__ import annotations
 try:
     import _imp as imp
 except ImportError:
@@ -382,61 +383,102 @@ def _green_existing_locks():
 
     This was originally noticed in the stdlib logging module."""
     import gc
+    import os
     import threading
     import eventlet.green.thread
-    lock_type = type(threading.Lock())
     rlock_type = type(threading.RLock())
-    if hasattr(threading, '_PyRLock'):
-        # this happens on CPython3 and PyPy >= 7.0.0: "py3-style" rlocks, they
-        # are implemented natively in C and RPython respectively
-        py3_style = True
-        pyrlock_type = type(threading._PyRLock())
-    else:
-        # this happens on CPython2.7 and PyPy < 7.0.0: "py2-style" rlocks,
-        # they are implemented in pure-python
-        py3_style = False
-        pyrlock_type = None
 
     # We're monkey-patching so there can't be any greenlets yet, ergo our thread
     # ID is the only valid owner possible.
     tid = eventlet.green.thread.get_ident()
-    for obj in gc.get_objects():
-        if isinstance(obj, rlock_type):
-            if not py3_style and isinstance(obj._RLock__block, lock_type):
-                _fix_py2_rlock(obj, tid)
-            elif py3_style and not isinstance(obj, pyrlock_type):
-                _fix_py3_rlock(obj, tid)
-
-    if sys.version_info < (3, 10):
-        # Older py3 won't have RLocks show up in gc.get_objects() -- see
-        # https://github.com/eventlet/eventlet/issues/546 -- so green a handful
-        # that we know are significant
+
+    # Now, upgrade all instances:
+    def upgrade(old_lock):
+        return _convert_py3_rlock(old_lock, tid)
+
+    _upgrade_instances(sys.modules, rlock_type, upgrade)
+
+    # Report if there are RLocks we couldn't upgrade. For cases where we're
+    # using coverage.py in parent process, and more generally for tests in
+    # general, this is difficult to ensure, so just don't complain in that case.
+    if "PYTEST_CURRENT_TEST" in os.environ:
+        return
+    # On older Pythons (< 3.10), gc.get_objects() won't return any RLock
+    # instances, so this warning won't get logged on older Pythons. However,
+    # it's a useful warning, so we try to do it anyway for the benefit of those
+    # users on 3.10 or later.
+    gc.collect()
+    remaining_rlocks = len({o for o in gc.get_objects() if isinstance(o, rlock_type)})
+    if remaining_rlocks:
         import logging
-        if isinstance(logging._lock, rlock_type):
-            _fix_py3_rlock(logging._lock, tid)
-        logging._acquireLock()
-        try:
-            for ref in logging._handlerList:
-                handler = ref()
-                if handler and isinstance(handler.lock, rlock_type):
-                    _fix_py3_rlock(handler.lock, tid)
-                del handler
-        finally:
-            logging._releaseLock()
+        logger = logging.Logger("eventlet")
+        logger.error("{} RLock(s) were not greened,".format(remaining_rlocks) +
+                     " to fix this error make sure you run eventlet.monkey_patch() " +
+                     "before importing any other modules.")
 
 
-def _fix_py2_rlock(rlock, tid):
-    import eventlet.green.threading
-    old = rlock._RLock__block
-    new = eventlet.green.threading.Lock()
-    rlock._RLock__block = new
-    if old.locked():
-        new.acquire()
-        rlock._RLock__owner = tid
+def _upgrade_instances(container, klass, upgrade, visited=None, old_to_new=None):
+    """
+    Starting with a Python object, find all instances of ``klass``, following
+    references in ``dict`` values, ``list`` items, and attributes.
 
+    Once an object is found, replace all instances with
+    ``upgrade(found_object)``, again limited to the criteria above.
 
-def _fix_py3_rlock(old, tid):
-    import gc
+    In practice this is used only for ``threading.RLock``, so we can assume
+    instances are hashable.
+    """
+    if visited is None:
+        visited = {}  # map id(obj) to obj
+    if old_to_new is None:
+        old_to_new = {}  # map old klass instance to upgrade(old)
+
+    # Handle circular references:
+    visited[id(container)] = container
+
+    def upgrade_or_traverse(obj):
+        if id(obj) in visited:
+            return None
+        if isinstance(obj, klass):
+            if obj in old_to_new:
+                return old_to_new[obj]
+            else:
+                new = upgrade(obj)
+                old_to_new[obj] = new
+                return new
+        else:
+            _upgrade_instances(obj, klass, upgrade, visited, old_to_new)
+            return None
+
+    if isinstance(container, dict):
+        for k, v in list(container.items()):
+            new = upgrade_or_traverse(v)
+            if new is not None:
+                container[k] = new
+    if isinstance(container, list):
+        for i, v in enumerate(container):
+            new = upgrade_or_traverse(v)
+            if new is not None:
+                container[i] = new
+    try:
+        container_vars = vars(container)
+    except TypeError:
+        pass
+    else:
+        for k, v in list(container_vars.items()):
+            new = upgrade_or_traverse(v)
+            if new is not None:
+                setattr(container, k, new)
+
+
+def _convert_py3_rlock(old, tid):
+    """
+    Convert a normal RLock to one implemented in Python.
+
+    This is necessary to make RLocks work with eventlet, but also introduces
+    bugs, e.g. https://bugs.python.org/issue13697.  So more of a downgrade,
+    really.
+    """
     import threading
     from eventlet.green.thread import allocate_lock
     new = threading._PyRLock()
@@ -458,26 +500,7 @@ def _fix_py3_rlock(old, tid):
         acquired = True
     if acquired:
         new._owner = tid
-    gc.collect()
-    for ref in gc.get_referrers(old):
-        if isinstance(ref, dict):
-            for k, v in list(ref.items()):
-                if v is old:
-                    ref[k] = new
-            continue
-        if isinstance(ref, list):
-            for i, v in enumerate(ref):
-                if v is old:
-                    ref[i] = new
-            continue
-        try:
-            ref_vars = vars(ref)
-        except TypeError:
-            pass
-        else:
-            for k, v in ref_vars.items():
-                if v is old:
-                    setattr(ref, k, new)
+    return new
 
 
 def _green_os_modules():


Test patch:
diff --git a/tests/isolated/patcher_existing_locks_preexisting.py b/tests/isolated/patcher_existing_locks_preexisting.py
new file mode 100644
index 000000000..e3306ee9f
--- /dev/null
+++ b/tests/isolated/patcher_existing_locks_preexisting.py
@@ -0,0 +1,41 @@
+"""
+Ensure pre-existing RLocks get upgraded in a variety of situations.
+"""
+
+import sys
+import threading
+import unittest.mock
+import eventlet
+
+python_lock = threading._PyRLock
+
+
+class NS:
+    lock = threading.RLock()
+
+    class NS2:
+        lock = threading.RLock()
+
+    dict = {1: 2, 12: threading.RLock()}
+    list = [0, threading.RLock()]
+
+
+def ensure_upgraded(lock):
+    if not isinstance(lock, python_lock):
+        raise RuntimeError(lock)
+
+
+if __name__ == '__main__':
+    # These extra print()s caused either test failures or segfaults until
+    # https://github.com/eventlet/eventlet/issues/864 was fixed.
+    if sys.version_info[:2] > (3, 9):
+        print(unittest.mock.NonCallableMock._lock)
+    print(NS.lock)
+    eventlet.monkey_patch()
+    ensure_upgraded(NS.lock)
+    ensure_upgraded(NS.NS2.lock)
+    ensure_upgraded(NS.dict[12])
+    ensure_upgraded(NS.list[1])
+    if sys.version_info[:2] > (3, 9):
+        ensure_upgraded(unittest.mock.NonCallableMock._lock)
+    print("pass")
diff --git a/tests/patcher_test.py b/tests/patcher_test.py
index cddb581da..ca7bc9996 100644
--- a/tests/patcher_test.py
+++ b/tests/patcher_test.py
@@ -523,3 +523,7 @@ def test_builtin():
 
 def test_open_kwargs():
     tests.run_isolated("patcher_open_kwargs.py")
+
+
+def test_patcher_existing_locks():
+    tests.run_isolated("patcher_existing_locks_preexisting.py")