Skip to content

Commit 9ac8d53

Browse files
jsjeannotteJSJ
and
JSJ
authored
Improve fork() handling (#37)
* For Python3.7+, improve fork() handling by stopping/starting the registry using 'os.register_at_fork()' * Move to bionic for Travis tests Co-authored-by: JSJ <[email protected]>
1 parent 3dc2e44 commit 9ac8d53

File tree

4 files changed

+41
-5
lines changed

4 files changed

+41
-5
lines changed

.travis.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ matrix:
1111
- python: 3.8
1212
env: TOXENV=py38
1313

14-
dist: xenial
14+
dist: bionic
1515

1616
install: pip install tox
1717

README.md

+13-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ See the Spectator [documentation] for an overview of core concepts and details o
99

1010
Supports Python >= 3.5, which is the oldest system Python 3 available on our commonly used OSes.
1111

12+
Note that there is a risk of deadlock if you are running Python 3.6 or lower and using
13+
`os.fork()` or using a library that will fork the process
14+
(see [this section](#concurrent-usage-under-older-python) for workarounds),
15+
so **we recommend using Python >= 3.7**
16+
1217
[Spectator]: https://github.com/Netflix/spectator/
1318
[documentation]: https://netflix.github.io/atlas-docs/spectator/
1419
[usage]: https://netflix.github.io/atlas-docs/spectator/lang/py/usage/
@@ -54,7 +59,9 @@ from spectator import GlobalRegistry
5459

5560
Once the `GlobalRegistry` is imported, it is used to create and manage Meters.
5661

57-
### Concurrent Usage
62+
### Concurrent Usage Under Older Python
63+
64+
> :warning: **Use Python 3.7+ if possible**: But if you can't, here's a workaround to prevent deadlocks
5865
5966
There is a known issue in Python where forking a process after a thread is started can lead to
6067
deadlocks. This is commonly seen when using the `multiprocessing` module with default settings.
@@ -84,6 +91,8 @@ WSGI_GUNICORN_PRELOAD = undef
8491

8592
#### Task Worker Forking
8693

94+
> :warning: **Use Python 3.7+ if possible**: But if you can't, here's a workaround to prevent deadlocks
95+
8796
For other pre-fork worker processing frameworks, such as [huey], you need to be careful about how
8897
and when you start the `GlobalRegistry` to avoid deadlocks in the background publish thread. You
8998
should set the `SPECTATOR_PY_DISABLE_AUTO_START_GLOBAL` environment variable to disable automatic
@@ -120,6 +129,8 @@ workers, to help ensure that it is not started when the module is loaded.
120129

121130
#### Generic Multiprocessing
122131

132+
> :warning: **Use Python 3.7+ if possible**: But if you can't, here's a workaround to prevent deadlocks
133+
123134
In Python 3, you can configure the start method to `spawn` for `multiprocessing`. This will cause
124135
the module to do a `fork()` followed by an `execve()` to start a brand new Python process.
125136

@@ -130,7 +141,7 @@ from multiprocessing import set_start_method
130141
set_start_method("spawn")
131142
```
132143

133-
To configure thid option within a context:
144+
To configure this option within a context:
134145

135146
```python
136147
from multiprocessing import get_context

spectator/__init__.py

+8
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@
1616
if auto_start_global():
1717
GlobalRegistry.start()
1818
atexit.register(GlobalRegistry.stop)
19+
try:
20+
from os import register_at_fork
21+
register_at_fork(before=GlobalRegistry.stop_without_publish,
22+
after_in_parent=GlobalRegistry.start,
23+
after_in_child=GlobalRegistry.clear_meters_and_start)
24+
except ImportError:
25+
pass
26+
1927
else:
2028
logger.debug("module spectatorconfig auto-start is disabled - GlobalRegistry will not start")
2129
except ImportError:

spectator/registry.py

+19-2
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,29 @@ def start(self, config=None):
101101
logger.debug("registry started with config: %s", config)
102102
return RegistryStopper(self)
103103

104-
def stop(self):
104+
def clear_meters_and_start(self):
105+
"""
106+
This is called after a fork in the child process
107+
to clear the cloned `_meters` and prevent duplicates
108+
(the `_meters` are copied with the process
109+
during the forking)
110+
"""
111+
self._meters = {}
112+
self.start()
113+
114+
def stop_without_publish(self):
115+
"""
116+
This is called before a fork to prevent a potential deadlock.
117+
It cancels the background timer thread. After the fork, the timer
118+
thread is restarted in the main and cloned processes.
119+
"""
105120
if self._started:
106-
logger.info("stopping registry")
121+
logger.debug("stopping log registry")
107122
self._timer.cancel()
108123
self._started = False
109124

125+
def stop(self):
126+
self.stop_without_publish()
110127
# Even if not started, attempt to flush data to minimize risk
111128
# of data loss
112129
self._publish()

0 commit comments

Comments
 (0)