datadog-async-handler 0.1.3__tar.gz → 0.1.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.1.4] - 2026-09-02
9
+
10
+ ### Fixed
11
+ - Fork safety: a process that called `fork()` while the worker thread was inside a TLS
12
+ send handed the child OpenSSL state mid-update, and the child deadlocked on its own
13
+ first TLS handshake (observed with Celery prefork workers). `os.register_at_fork`
14
+ hooks now hold the send lock across `fork()` and give the child a fresh queue, lock,
15
+ and worker thread. The lock covers only the network attempt, not retry back-off.
16
+ - The worker thread is named `datadog-http-handler`.
17
+
8
18
  ## [0.1.2] - 2025-08-25
9
19
 
10
20
  ### Added
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: datadog-async-handler
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: High-performance async HTTP logging handler for Datadog with batching and retry logic
5
5
  Project-URL: Homepage, https://github.com/enlyft/datadog-async-handler
6
6
  Project-URL: Documentation, https://enlyft.github.io/datadog-async-handler
@@ -53,7 +53,7 @@ Description-Content-Type: text/markdown
53
53
 
54
54
  # Datadog Async Handler
55
55
 
56
- [![PyPI version](https://badge.fury.io/py/datadog-async-handler.svg)](https://badge.fury.io/py/datadog-async-handler)
56
+ [![PyPI version](https://img.shields.io/pypi/v/datadog-async-handler)](https://pypi.org/project/datadog-async-handler/)
57
57
  [![Python versions](https://img.shields.io/pypi/pyversions/datadog-async-handler.svg)](https://pypi.org/project/datadog-async-handler/)
58
58
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
59
59
  [![Code style: ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
@@ -1,6 +1,6 @@
1
1
  # Datadog Async Handler
2
2
 
3
- [![PyPI version](https://badge.fury.io/py/datadog-async-handler.svg)](https://badge.fury.io/py/datadog-async-handler)
3
+ [![PyPI version](https://img.shields.io/pypi/v/datadog-async-handler)](https://pypi.org/project/datadog-async-handler/)
4
4
  [![Python versions](https://img.shields.io/pypi/pyversions/datadog-async-handler.svg)](https://pypi.org/project/datadog-async-handler/)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
6
  [![Code style: ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
@@ -9,6 +9,7 @@ import logging
9
9
  import os
10
10
  import threading
11
11
  import time
12
+ import weakref
12
13
  from queue import Empty, Queue
13
14
  from typing import Optional
14
15
 
@@ -18,6 +19,17 @@ from datadog_api_client.v2.model.http_log import HTTPLog
18
19
  from datadog_api_client.v2.model.http_log_item import HTTPLogItem
19
20
 
20
21
 
22
+ def _at_fork(ref: "weakref.ReferenceType[DatadogHTTPHandler]", method: str) -> None:
23
+ """Dispatch an ``os.register_at_fork`` callback to a handler that is still alive.
24
+
25
+ ``os.register_at_fork`` keeps its callables for the life of the process, so the
26
+ hooks hold only a weak reference and become no-ops once the handler is collected.
27
+ """
28
+ handler = ref()
29
+ if handler is not None:
30
+ getattr(handler, method)()
31
+
32
+
21
33
  class DatadogHTTPHandler(logging.Handler):
22
34
  """
23
35
  High-performance logging handler that sends logs to Datadog via HTTP API.
@@ -26,6 +38,12 @@ class DatadogHTTPHandler(logging.Handler):
26
38
  the main application thread. It includes retry logic with exponential backoff,
27
39
  comprehensive error handling, and support for all Datadog sites.
28
40
 
41
+ The handler is fork-safe. Its worker thread sends over TLS, and a child forked
42
+ while that handshake is in flight inherits OpenSSL state mid-update and deadlocks
43
+ on its own first TLS handshake. ``os.register_at_fork`` hooks hold a send lock
44
+ across ``fork()`` so no child is forked mid-send, and give the child a fresh
45
+ queue, lock, and worker thread (threads do not survive ``fork()``).
46
+
29
47
  Args:
30
48
  api_key: Datadog API key (or set DD_API_KEY env var)
31
49
  site: Datadog site (default: datadoghq.com)
@@ -60,6 +78,9 @@ class DatadogHTTPHandler(logging.Handler):
60
78
  >>> logger.addHandler(handler)
61
79
  """
62
80
 
81
+ # Bounded so a worker wedged inside C can never freeze the parent's fork() forever.
82
+ _FORK_WAIT_SECONDS = 15.0
83
+
63
84
  def __init__(
64
85
  self,
65
86
  api_key: Optional[str] = None,
@@ -103,7 +124,16 @@ class DatadogHTTPHandler(logging.Handler):
103
124
  self._log_queue: Queue[HTTPLogItem] = Queue()
104
125
  self._stop_event = threading.Event()
105
126
  self._worker_thread: Optional[threading.Thread] = None
127
+ self._send_lock = threading.Lock()
128
+ self._fork_lock_held = False
106
129
  self._start_worker()
130
+ if hasattr(os, "register_at_fork"): # POSIX only
131
+ ref = weakref.ref(self)
132
+ os.register_at_fork(
133
+ before=lambda: _at_fork(ref, "_before_fork"),
134
+ after_in_parent=lambda: _at_fork(ref, "_after_fork_in_parent"),
135
+ after_in_child=lambda: _at_fork(ref, "_after_fork_in_child"),
136
+ )
107
137
 
108
138
  def _setup_api_client(self) -> None:
109
139
  """Setup the Datadog API client."""
@@ -117,7 +147,9 @@ class DatadogHTTPHandler(logging.Handler):
117
147
  def _start_worker(self) -> None:
118
148
  """Start the background worker thread."""
119
149
  if self._worker_thread is None or not self._worker_thread.is_alive():
120
- self._worker_thread = threading.Thread(target=self._worker, daemon=True)
150
+ self._worker_thread = threading.Thread(
151
+ target=self._worker, daemon=True, name="datadog-http-handler"
152
+ )
121
153
  self._worker_thread.start()
122
154
 
123
155
  def _worker(self) -> None:
@@ -161,7 +193,8 @@ class DatadogHTTPHandler(logging.Handler):
161
193
  for attempt in range(self.max_retries + 1):
162
194
  try:
163
195
  http_log = HTTPLog(batch)
164
- self.logs_api.submit_log(body=http_log)
196
+ with self._send_lock: # fork() waits for this, not for back-off
197
+ self.logs_api.submit_log(body=http_log)
165
198
  return # Success
166
199
 
167
200
  except Exception as e:
@@ -174,6 +207,33 @@ class DatadogHTTPHandler(logging.Handler):
174
207
  # Wait before retry (exponential backoff)
175
208
  time.sleep(2**attempt)
176
209
 
210
+ def _before_fork(self) -> None:
211
+ """Hold the send lock across ``fork()`` so no child is forked mid-send."""
212
+ self._fork_lock_held = self._send_lock.acquire(timeout=self._FORK_WAIT_SECONDS)
213
+ if not self._fork_lock_held:
214
+ self._handle_error(
215
+ "forking while a log send is wedged; the child's TLS may deadlock"
216
+ )
217
+
218
+ def _after_fork_in_parent(self) -> None:
219
+ """Release the lock taken by ``_before_fork``."""
220
+ if self._fork_lock_held:
221
+ self._fork_lock_held = False
222
+ self._send_lock.release()
223
+
224
+ def _after_fork_in_child(self) -> None:
225
+ """Give the child a fresh lock, queue, and worker thread.
226
+
227
+ The parent's worker thread does not exist in the child, and the inherited
228
+ lock may still be held by it.
229
+ """
230
+ self._send_lock = threading.Lock()
231
+ self._fork_lock_held = False
232
+ self._log_queue = Queue()
233
+ self._stop_event = threading.Event()
234
+ self._worker_thread = None
235
+ self._start_worker()
236
+
177
237
  def _handle_error(self, message: str) -> None:
178
238
  """Handle errors that occur during log submission."""
179
239
  # Log to stderr to avoid infinite recursion