Skip to content

Commit d8b1835

Browse files
PetrHeinzclaude
andauthored
T-17985 Handle RuntimeError from Thread.is_alive() on Python 3.14+ (#39)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent dc7c885 commit d8b1835

9 files changed

Lines changed: 72 additions & 31 deletions

File tree

.github/workflows/main.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@ name: tests
33
on:
44
- push
55
- pull_request
6+
- workflow_dispatch
67

78
jobs:
89
build:
9-
runs-on: ubuntu-22.04
10+
runs-on: ubuntu-24.04
1011
strategy:
12+
fail-fast: false
1113
matrix:
12-
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11']
14+
# '3.x' always resolves to the latest stable Python 3 release, so new
15+
# minor versions get exercised as soon as they ship.
16+
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14', '3.x']
1317

1418
steps:
1519
- uses: actions/checkout@v1

example-project/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ In this section, we will take a look at actual logging as shown in the example p
3939

4040
## Setup
4141

42-
First, we need to import the Logtail client library to our code. This can be done using the import keyword. We also need to import the default logging library.
42+
First, we need to import the Better Stack client library to our code. This can be done using the import keyword. We also need to import the default logging library.
4343

4444
```python
45-
# Import Logtail client library and default logging library
45+
# Import Better Stack client library and default logging library
4646
from logtail import LogtailHandler
4747
import logging
4848
```
@@ -81,7 +81,7 @@ Code above will generate only one log because the debug level message has lowere
8181

8282
## Logging example
8383

84-
The `logger` instance we created in the setup section is used to send log messages to Logtail. It provides 6 logging methods for the 6 default log levels. The log levels and their method are:
84+
The `logger` instance we created in the setup section is used to send log messages to Better Stack. It provides 6 logging methods for the 6 default log levels. The log levels and their method are:
8585

8686
- **DEBUG** - Send debug messages using the `debug()` method
8787
- **INFO** - Send informative messages about the application progress using the `info()` method

example-project/example-project.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
# This is an example project of Logtail python integration
2-
# This project showcases how to use Logtail in your python projects
1+
# This is an example project of Better Stack python integration
2+
# This project showcases how to use Better Stack in your python projects
33
# For more information please visit https://github.com/logtail/logtail-python
44

55
# SETUP
66

7-
# Import Logtail client library and default logging library
7+
# Import Better Stack client library and default logging library
88
from logtail import LogtailHandler
99
import logging
1010
import sys
@@ -27,10 +27,10 @@
2727
# Following code showcases logger usage
2828

2929
# Send debug log using the debug() method
30-
logger.debug('I am using Logtail!')
30+
logger.debug('I am using Better Stack!')
3131

3232
# Send info level log about interesting events using the info() method
33-
logger.info('I love Logtail!')
33+
logger.info('I love Better Stack!')
3434

3535
# Send warning level log about worrying events using the warning() method
3636
# You can also add custom structured information to the log by passing it as a second argument

logtail/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@
55
from .helpers import LogtailContext, DEFAULT_CONTEXT
66
from .formatter import LogtailFormatter
77

8-
__version__ = '0.3.4'
8+
__version__ = '0.4.0'
99

1010
context = DEFAULT_CONTEXT

logtail/flusher.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@ def run(self):
2626
while self.should_run:
2727
self.step()
2828

29+
def _is_parent_alive(self):
30+
try:
31+
return self.parent_thread.is_alive()
32+
except RuntimeError:
33+
# Starting with Python 3.14, calling is_alive() on an
34+
# already-terminated thread raises a RuntimeError instead of
35+
# returning False (see the parent thread being torn down during
36+
# interpreter shutdown). Treat that as "no longer alive".
37+
return False
38+
2939
def step(self):
3040
last_flush = time.time()
3141
time_remaining = _initial_time_remaining(self.flush_interval)
@@ -34,7 +44,7 @@ def step(self):
3444

3545
# If the parent thread has exited but there are still outstanding
3646
# events, attempt to send them before exiting.
37-
shutdown = not self.parent_thread.is_alive()
47+
shutdown = not self._is_parent_alive()
3848

3949
# Fill phase: take events out of the queue and group them for sending.
4050
# Takes up to `buffer_capacity` events out of the queue and groups them
@@ -54,11 +64,11 @@ def step(self):
5464
except queue.Empty:
5565
if shutdown or self._flushing:
5666
break
57-
shutdown = not self.parent_thread.is_alive()
67+
shutdown = not self._is_parent_alive()
5868
time_remaining = _calculate_time_remaining(last_flush, self.flush_interval)
5969

6070
# Send phase: takes the outstanding events (up to `buffer_capacity`
61-
# count) and sends them to the Logtail endpoint all at once. If the
71+
# count) and sends them to the Better Stack endpoint all at once. If the
6272
# request fails in a way that can be retried, it is retried with an
6373
# exponential backoff in between attempts.
6474
if frame:

setup.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from setuptools import setup
44

55

6-
VERSION = '0.3.4'
6+
VERSION = '0.4.0'
77
ROOT_DIR = os.path.dirname(__file__)
88

99
REQUIREMENTS = [
@@ -20,25 +20,26 @@
2020
packages=['logtail'],
2121
include_package_data=True,
2222
license='ISC',
23-
description='Logtail.com client library',
23+
description='Better Stack client library',
2424
long_description=long_description,
2525
long_description_content_type='text/markdown',
2626
url='https://github.com/logtail/logtail-python',
2727
download_url='https://github.com/logtail/logtail-python/tarball/%s' % (VERSION),
2828
keywords=['api', 'logtail', 'logging', 'client'],
2929
install_requires=REQUIREMENTS,
30+
python_requires='>=3.10',
3031
author='Logtail',
3132
author_email='hello@logtail.com',
3233
classifiers=[
3334
'Intended Audience :: Developers',
3435
'License :: OSI Approved :: ISC License (ISCL)',
3536
'Operating System :: OS Independent',
3637
'Programming Language :: Python :: 3',
37-
'Programming Language :: Python :: 3.7',
38-
'Programming Language :: Python :: 3.8',
39-
'Programming Language :: Python :: 3.9',
4038
'Programming Language :: Python :: 3.10',
4139
'Programming Language :: Python :: 3.11',
40+
'Programming Language :: Python :: 3.12',
41+
'Programming Language :: Python :: 3.13',
42+
'Programming Language :: Python :: 3.14',
4243
'Programming Language :: Python',
4344
'Topic :: Software Development :: Libraries :: Python Modules',
4445
],

tests/test_flusher.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# coding: utf-8
22
from __future__ import print_function, unicode_literals
33
import mock
4-
import sys
54
import time
65
import threading
76
import unittest
@@ -146,8 +145,39 @@ def uploader(frame):
146145
self.assertEqual(self.upload_calls, 1)
147146
self.assertFalse(fw.should_run)
148147

149-
# test relies on overriding excepthook which is available from 3.8+
150-
@unittest.skipIf(sys.version_info < (3, 8), "Test skipped because overriding excepthook is only available on Python 3.8+")
148+
def test_is_parent_alive_handles_runtime_error(self):
149+
# Starting with Python 3.14, is_alive() raises RuntimeError when
150+
# called on an already-terminated thread instead of returning False.
151+
# The worker must treat that as the parent no longer being alive.
152+
_, _, fw = self._setup_worker()
153+
fw.parent_thread = mock.MagicMock()
154+
fw.parent_thread.is_alive.side_effect = RuntimeError('cannot join current thread')
155+
156+
self.assertFalse(fw._is_parent_alive())
157+
158+
def test_shutdown_condition_when_parent_is_alive_raises(self):
159+
self.buffer_capacity = 10
160+
num_items = 5
161+
first_frame = list(range(self.buffer_capacity))
162+
self.assertLess(num_items, self.buffer_capacity)
163+
164+
self.upload_calls = 0
165+
def uploader(frame):
166+
self.upload_calls += 1
167+
self.assertEqual(frame, first_frame[:num_items])
168+
return mock.MagicMock(status_code=202)
169+
170+
pipe, _, fw = self._setup_worker(uploader)
171+
fw.parent_thread = mock.MagicMock()
172+
fw.parent_thread.is_alive.side_effect = RuntimeError('cannot join current thread')
173+
174+
for i in range(num_items):
175+
pipe.put(first_frame[i], block=False)
176+
177+
fw.step()
178+
self.assertEqual(self.upload_calls, 1)
179+
self.assertFalse(fw.should_run)
180+
151181
def test_shutdown_dont_raise_exception_in_thread(self):
152182
original_excepthook = threading.excepthook
153183
threading.excepthook = mock.Mock()

tests/test_frame.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from logtail.frame import create_frame
44
from logtail.handler import LogtailHandler
55
from logtail.helpers import LogtailContext
6-
from sys import version_info
76
import datetime
87
import unittest
98
import logging
@@ -17,11 +16,8 @@ def test_create_frame_happy_path(self):
1716
# ISO timestamp must end with timezone info
1817
self.assertTrue(frame['dt'].endswith("+00:00"))
1918

20-
# These tests require Python >= 3.7
21-
if version_info.major == 2 or version_info.minor <= 6:
22-
return
2319
# Sent date matches log record date
24-
date_ref = datetime.datetime.utcfromtimestamp(log_record.created).replace(tzinfo=datetime.timezone.utc)
20+
date_ref = datetime.datetime.fromtimestamp(log_record.created, datetime.timezone.utc)
2521
date_sent = datetime.datetime.fromisoformat(frame['dt'])
2622
self.assertEqual(date_ref, date_sent)
2723

tox.ini

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
[tox]
2-
envlist = py37, py38, py39, py310, py311
2+
envlist = py310, py311, py312, py313, py314
33

44
[gh-actions]
55
python =
6-
3.7: py37
7-
3.8: py38
8-
3.9: py39
96
3.10: py310
107
3.11: py311
8+
3.12: py312
9+
3.13: py313
10+
3.14: py314
1111

1212
[testenv]
1313
deps =

0 commit comments

Comments
 (0)