brettlangdon: ```
RuntimeError: cannot join thread before it is started
Error in atexit._run_exitfuncs:
Traceback (most recent call last):
  File "/root/project/ddtrace/_worker.py", line 47, in _atexit
    self.join(self.exit_timeout)
  File "/root/project/ddtrace/_worker.py", line 63, in join
    return self._thread.join(timeout)
  File "/root/.pyenv/versions/3.6.8/lib/python3.6/threading.py", line 1051, in join
    raise RuntimeError("cannot join thread before it is started")
```
brettlangdon: @jd I opened https://github.com/DataDog/dd-trace-py/pull/1195 since I had a few changes I wanted to make, didn't want to commit over your branch here.
diff --git a/ddtrace/_worker.py b/ddtrace/_worker.py
index ed25db418..8b2b6db7a 100644
--- a/ddtrace/_worker.py
+++ b/ddtrace/_worker.py
@@ -32,13 +32,14 @@ class PeriodicWorkerThread(object):
         self._thread = threading.Thread(target=self._target, name=name)
         self._thread.daemon = daemon
         self._stop = threading.Event()
+        self.started = False
         self.interval = interval
         self.exit_timeout = exit_timeout
         atexit.register(self._atexit)
 
     def _atexit(self):
         self.stop()
-        if self.exit_timeout is not None:
+        if self.exit_timeout is not None and self.started:
             key = 'ctrl-break' if os.name == 'nt' else 'ctrl-c'
             _LOG.debug(
                 'Waiting %d seconds for %s to finish. Hit %s to quit.',
@@ -50,6 +51,7 @@ class PeriodicWorkerThread(object):
         """Start the periodic worker."""
         _LOG.debug('Starting %s thread', self._thread.name)
         self._thread.start()
+        self.started = True
 
     def stop(self):
         """Stop the worker."""
diff --git a/ddtrace/internal/writer.py b/ddtrace/internal/writer.py
index 13250b837..d39c3baa8 100644
--- a/ddtrace/internal/writer.py
+++ b/ddtrace/internal/writer.py
@@ -50,7 +50,7 @@ class AgentWriter(_worker.PeriodicWorkerThread):
         )
         if hasattr(time, "thread_time"):
             self._last_thread_time = time.thread_time()
-        self.start()
+        self._started = False
 
     def recreate(self):
         """ Create a new instance of :class:`AgentWriter` using the same settings from this instance
@@ -76,6 +76,12 @@ class AgentWriter(_worker.PeriodicWorkerThread):
         return bool(config.health_metrics_enabled and self.dogstatsd)
 
     def write(self, spans=None, services=None):
+        # Start the AgentWriter on first write.
+        # Starting it earlier might be an issue with gevent, see:
+        # https://github.com/DataDog/dd-trace-py/issues/1192
+        if self._started is False:
+            self.start()
+            self._started = True
         if spans:
             self._trace_queue.put(spans)
 

`ddtrace>=0.30.0` starts a thread at import, leading to issues with gevent.monkey.patch_all
# Package versions

```
% python --version
Python 3.7.6
```

```
% pip freeze
ddtrace==0.33.0
gevent==1.4.0
greenlet==0.4.15
psutil==5.6.7
```

This occurs with `python>=3.6`, `gevent>=1.3` (older versions are not compatible with `py3.6` anyway) and `ddtrace>=0.30.0,<=0.33.0` (current latest as of this bug filing).

# Steps to reproduce

With `test_gevent.py` as follows:

```
import ddtrace

import gevent.monkey

gevent.monkey.patch_all()
```

run

```
% python -c "import test_gevent"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "<frozen importlib._bootstrap>", line 983, in _find_and_load
  File "<frozen importlib._bootstrap>", line 152, in __exit__
  File "<frozen importlib._bootstrap>", line 107, in release
RuntimeError: cannot release un-acquired lock
```

The `gevent` patching fails because a thread has already been started, but `ddtrace` patching has not been explicitly invoked.

# Possible cause

The `Writer` and `Worker` classes were merged in https://github.com/DataDog/dd-trace-py/pull/988 . As part of that change, the worker thread is now started when `AgentWriter.__init__` is called, whereas (if I read correctly) previously the worker thread was only started upon the first call to `AgentWriter.write`:

Change: https://github.com/DataDog/dd-trace-py/commit/217a8c62461ec742667306d4974fca1cb459db68#diff-88c4dee1a45666e54351f6960196a037R36 (more obvious in split view, but previously `_reset_worker` started the worker thread in the `write` function)

We will work around this by only importing `ddtrace` if we are certain we will then call `ddtrace.patch`, but ideally simply importing `ddtrace` would be a safe operation.
diff --git a/tests/test_tracer.py b/tests/test_tracer.py
index 2ce840a21..2da8aa2b0 100644
--- a/tests/test_tracer.py
+++ b/tests/test_tracer.py
@@ -629,11 +629,6 @@ def test_tracer_fork():
                 assert t.writer != original_writer
                 assert t.writer._trace_queue != original_writer._trace_queue
 
-            # Stop the background worker so we don't accidetnally flush the
-            # queue before we can assert on it
-            t.writer.stop()
-            t.writer.join()
-
         # Assert the trace got written into the correct queue
         assert original_writer._trace_queue.qsize() == 0
         assert t.writer._trace_queue.qsize() == 1
@@ -656,11 +651,6 @@ def test_tracer_fork():
         assert t.writer == original_writer
         assert t.writer._trace_queue == original_writer._trace_queue
 
-        # Stop the background worker so we don't accidentally flush the
-        # queue before we can assert on it
-        t.writer.stop()
-        t.writer.join()
-
     # Assert the trace got written into the correct queue
     assert original_writer._trace_queue.qsize() == 1
     assert t.writer._trace_queue.qsize() == 1
diff --git a/tests/test_worker.py b/tests/test_worker.py
index a08d2a2af..73f682c7f 100644
--- a/tests/test_worker.py
+++ b/tests/test_worker.py
@@ -56,3 +56,16 @@ def test_restart():
 
     with pytest.raises(RuntimeError):
         w.start()
+
+
+def test_atexit():
+    w = _worker.PeriodicWorkerThread(exit_timeout=1)
+    w.start()
+    assert w.started
+    w._atexit()
+
+
+def test_atexit_not_started():
+    w = _worker.PeriodicWorkerThread(exit_timeout=1)
+    assert not w.started
+    w._atexit()

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest pytest-benchmark pytest-cov pytest-django pytest-mock mock
apt-get update apt-get install -y gcc
3.9
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
coverage==7.8.0
-e git+https://github.com/DataDog/dd-trace-py.git@1e87c9bdf7769032982349c4ccc0e1c2e6866a16#egg=ddtrace
exceptiongroup==1.2.2
iniconfig==2.1.0
mock==5.2.0
packaging==24.2
pluggy==1.5.0
psutil==7.0.0
py-cpuinfo==9.0.0
pytest==8.3.5
pytest-benchmark==5.1.0
pytest-cov==6.0.0
pytest-django==4.10.0
pytest-mock==3.14.0
tomli==2.2.1

name: dd-trace-py
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - coverage==7.8.0
      - ddtrace==0.33.1.dev6+g1e87c9bdf
      - exceptiongroup==1.2.2
      - iniconfig==2.1.0
      - mock==5.2.0
      - packaging==24.2
      - pluggy==1.5.0
      - psutil==7.0.0
      - py-cpuinfo==9.0.0
      - pytest==8.3.5
      - pytest-benchmark==5.1.0
      - pytest-cov==6.0.0
      - pytest-django==4.10.0
      - pytest-mock==3.14.0
      - tomli==2.2.1
prefix: /opt/conda/envs/dd-trace-py

tests/test_worker.py::test_atexit tests/test_worker.py::test_atexit_not_started
tests/test_tracer.py::TracerTestCase::test_adding_services tests/test_tracer.py::TracerTestCase::test_configure_dogstatsd_host tests/test_tracer.py::TracerTestCase::test_configure_dogstatsd_host_port tests/test_tracer.py::TracerTestCase::test_configure_dogstatsd_url_host_port tests/test_tracer.py::TracerTestCase::test_configure_dogstatsd_url_socket tests/test_tracer.py::TracerTestCase::test_configure_runtime_worker tests/test_tracer.py::TracerTestCase::test_default_provider_get tests/test_tracer.py::TracerTestCase::test_default_provider_set tests/test_tracer.py::TracerTestCase::test_default_provider_trace tests/test_tracer.py::TracerTestCase::test_global_context tests/test_tracer.py::TracerTestCase::test_only_root_span_runtime_external_span_types tests/test_tracer.py::TracerTestCase::test_only_root_span_runtime_internal_span_types tests/test_tracer.py::TracerTestCase::test_span_no_runtime_tags tests/test_tracer.py::TracerTestCase::test_start_child_from_context tests/test_tracer.py::TracerTestCase::test_start_child_span tests/test_tracer.py::TracerTestCase::test_start_child_span_attributes tests/test_tracer.py::TracerTestCase::test_start_span tests/test_tracer.py::TracerTestCase::test_start_span_optional tests/test_tracer.py::TracerTestCase::test_tracer tests/test_tracer.py::TracerTestCase::test_tracer_current_root_span_missing_context tests/test_tracer.py::TracerTestCase::test_tracer_current_span tests/test_tracer.py::TracerTestCase::test_tracer_current_span_missing_context tests/test_tracer.py::TracerTestCase::test_tracer_disabled tests/test_tracer.py::TracerTestCase::test_tracer_disabled_mem_leak tests/test_tracer.py::TracerTestCase::test_tracer_global_tags tests/test_tracer.py::TracerTestCase::test_tracer_pid tests/test_tracer.py::TracerTestCase::test_tracer_vars tests/test_tracer.py::TracerTestCase::test_tracer_wrap tests/test_tracer.py::TracerTestCase::test_tracer_wrap_class tests/test_tracer.py::TracerTestCase::test_tracer_wrap_default_name tests/test_tracer.py::TracerTestCase::test_tracer_wrap_exception tests/test_tracer.py::TracerTestCase::test_tracer_wrap_factory tests/test_tracer.py::TracerTestCase::test_tracer_wrap_factory_nested tests/test_tracer.py::TracerTestCase::test_tracer_wrap_multiple_calls tests/test_tracer.py::TracerTestCase::test_tracer_wrap_span_nesting tests/test_tracer.py::TracerTestCase::test_tracer_wrap_span_nesting_current_root_span tests/test_tracer.py::test_installed_excepthook tests/test_tracer.py::test_excepthook tests/test_tracer.py::test_tracer_url tests/test_tracer.py::test_tracer_dogstatsd_url tests/test_tracer.py::test_tracer_fork tests/test_worker.py::test_start tests/test_worker.py::test_periodic tests/test_worker.py::test_on_shutdown tests/test_worker.py::test_restart
ddtrace/_worker.py ddtrace/internal/writer.py
ddtrace/_worker.py ddtrace/internal/writer.py