From 7d5b8ee77d443c88547759ab007cbe047d103ade Mon Sep 17 00:00:00 2001 From: Maxim Lapan Date: Mon, 28 Sep 2026 08:37:18 +0200 Subject: [PATCH 01/15] Switch to daemon thread --- exasol/telemetry/client/setup.py | 10 ++++++++++ exasol/telemetry/client/worker.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/exasol/telemetry/client/setup.py b/exasol/telemetry/client/setup.py index e5ee75d..344db09 100644 --- a/exasol/telemetry/client/setup.py +++ b/exasol/telemetry/client/setup.py @@ -1,3 +1,4 @@ +import atexit import os import typing as tt from urllib.parse import urlparse @@ -52,6 +53,14 @@ def setup_verbose_if_needed(): verbose.setup_logging() +def setup_exit_handlers(): + """ + Trigger shutdown on exit of application. + Function have to be called only once - during the setup. + """ + atexit.register(shutdown) + + def setup(endpoint: tt.Optional[str] = None, disable: tt.Optional[bool] = None) -> bool: """ Telemetry client setup function. @@ -99,6 +108,7 @@ def setup(endpoint: tt.Optional[str] = None, disable: tt.Optional[bool] = None) config.store(conf) if enabled: setup_verbose_if_needed() + setup_exit_handlers() worker.start_worker() verbose.log("Setup is done, enabled=%s", conf.enabled) return conf.enabled diff --git a/exasol/telemetry/client/worker.py b/exasol/telemetry/client/worker.py index 5277689..41de97e 100644 --- a/exasol/telemetry/client/worker.py +++ b/exasol/telemetry/client/worker.py @@ -257,7 +257,7 @@ def start_worker() -> bool: if not config.was_enabled(): return False _queue = queue.Queue(maxsize=MAX_QUEUE_CAPACITY) - _worker = threading.Thread(target=worker_proc, args=(_queue,)) + _worker = threading.Thread(target=worker_proc, args=(_queue,), daemon=True) _worker.start() return True From 7da8e9582c3f026cbbe086ee1dfd827e291e1ec9 Mon Sep 17 00:00:00 2001 From: Maxim Lapan Date: Mon, 28 Sep 2026 08:46:37 +0200 Subject: [PATCH 02/15] Fix setup logic --- exasol/telemetry/client/setup.py | 4 +++- test/unit/client/test_setup.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/exasol/telemetry/client/setup.py b/exasol/telemetry/client/setup.py index 344db09..31d8704 100644 --- a/exasol/telemetry/client/setup.py +++ b/exasol/telemetry/client/setup.py @@ -80,7 +80,9 @@ def setup(endpoint: tt.Optional[str] = None, disable: tt.Optional[bool] = None) False if it was disabled """ if config.was_setup(): - return config.was_enabled() + if disable is None or disable != config.was_enabled(): + return config.was_enabled() + shutdown() val_endpoint = get_value(endpoint, config.ENV_ENDPOINT, config.DEFAULT_ENDPOINT) # Checking the presence of CI=true env variable diff --git a/test/unit/client/test_setup.py b/test/unit/client/test_setup.py index 3085c4c..8cd7385 100644 --- a/test/unit/client/test_setup.py +++ b/test/unit/client/test_setup.py @@ -80,8 +80,8 @@ def test_setup_env_disabled( assert not setup("http://endpoint") assert config.was_setup() assert not config.was_enabled() - # double-call to setup skips reconfiguration and returns the enable status - assert not setup(disable=False) + # double-call to setup skips reconfiguration if disable is the same + assert not setup(disable=True) def test_setup_env_enabled( From 7144bb7f7ea4aa692a134e927d3f78d60754e152 Mon Sep 17 00:00:00 2001 From: Maxim Lapan Date: Mon, 28 Sep 2026 08:50:27 +0200 Subject: [PATCH 03/15] Prepare release --- doc/changes/changelog.md | 2 ++ doc/changes/changes_0.2.1.md | 9 +++++++++ pyproject.toml | 2 +- 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 doc/changes/changes_0.2.1.md diff --git a/doc/changes/changelog.md b/doc/changes/changelog.md index e1874a0..1b26859 100644 --- a/doc/changes/changelog.md +++ b/doc/changes/changelog.md @@ -1,6 +1,7 @@ # Changes * [unreleased](unreleased.md) +* [0.2.1](changes_0.2.1.md) * [0.2.0](changes_0.2.0.md) * [0.1.6](changes_0.1.6.md) @@ -9,6 +10,7 @@ hidden: --- unreleased +changes_0.2.1 changes_0.2.0 changes_0.1.6 ``` diff --git a/doc/changes/changes_0.2.1.md b/doc/changes/changes_0.2.1.md new file mode 100644 index 0000000..6b3819c --- /dev/null +++ b/doc/changes/changes_0.2.1.md @@ -0,0 +1,9 @@ +# 0.2.1 - 2026-09-28 + +## Summary + +Fix of threading block at the process exit. + +## Bugs + +- #10: Threading blocks at python exit diff --git a/pyproject.toml b/pyproject.toml index 621daf2..21fef52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "exasol-telemetry-client" -version = "0.2.0" +version = "0.2.1" description = "" authors = [{ name = "Exasol AG", email = "opensource@exasol.com" }] requires-python = ">=3.10,<4.0" From 11871c8081a643bbc8482f0c502b0ae91a41e8c5 Mon Sep 17 00:00:00 2001 From: Maxim Lapan Date: Tue, 29 Sep 2026 08:46:07 +0200 Subject: [PATCH 04/15] Timeout on join --- doc/changes/changes_0.2.1.md | 3 ++- exasol/telemetry/client/worker.py | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/doc/changes/changes_0.2.1.md b/doc/changes/changes_0.2.1.md index 6b3819c..1a8ede0 100644 --- a/doc/changes/changes_0.2.1.md +++ b/doc/changes/changes_0.2.1.md @@ -2,8 +2,9 @@ ## Summary -Fix of threading block at the process exit. +Fix of threading block at the process exit. Fixes `setup()` logic of reconfiguration (semi-private API). ## Bugs - #10: Threading blocks at python exit +- Setup logic was wrong in one of tests. diff --git a/exasol/telemetry/client/worker.py b/exasol/telemetry/client/worker.py index 41de97e..ea0336d 100644 --- a/exasol/telemetry/client/worker.py +++ b/exasol/telemetry/client/worker.py @@ -19,6 +19,9 @@ # requests' timeout value SEND_TIMEOUT_SECONDS = 30 +# how long to wait before thread exits +THREAD_EXIT_TIMEOUT_SECONDS = 1 + # how long in seconds to wait before the first batch send DATA_SEND_FIRST_INTERVAL_SECONDS = 0.5 @@ -275,7 +278,7 @@ def stop_worker(flush_buffers: bool): if flush_buffers: _queue.put(WorkerMessage.make_send_buffers()) _queue.put(WorkerMessage.make_terminate()) - _worker.join() + _worker.join(timeout=THREAD_EXIT_TIMEOUT_SECONDS) _worker = None _queue = None From 3a6447d0492d944921d62878c5e6c84d0176697e Mon Sep 17 00:00:00 2001 From: Maxim Lapan Date: Tue, 29 Sep 2026 08:55:46 +0200 Subject: [PATCH 05/15] Removal of exit handlers --- exasol/telemetry/client/setup.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/exasol/telemetry/client/setup.py b/exasol/telemetry/client/setup.py index 31d8704..1274e93 100644 --- a/exasol/telemetry/client/setup.py +++ b/exasol/telemetry/client/setup.py @@ -61,6 +61,13 @@ def setup_exit_handlers(): atexit.register(shutdown) +def drop_exit_handlers(): + """ + Removes previously configured exit handlers + """ + atexit.unregister(shutdown) + + def setup(endpoint: tt.Optional[str] = None, disable: tt.Optional[bool] = None) -> bool: """ Telemetry client setup function. @@ -127,6 +134,7 @@ def shutdown(flush_buffers: bool = True): return verbose.log("Shutdown") worker.stop_worker(flush_buffers) + drop_exit_handlers() def disable(): From 22f85ff38786e7134b5857cb83c745943d8e8ee1 Mon Sep 17 00:00:00 2001 From: Maxim Lapan Date: Tue, 29 Sep 2026 08:57:16 +0200 Subject: [PATCH 06/15] Extra test case --- test/unit/client/test_setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/unit/client/test_setup.py b/test/unit/client/test_setup.py index 8cd7385..9b3e4fe 100644 --- a/test/unit/client/test_setup.py +++ b/test/unit/client/test_setup.py @@ -82,6 +82,7 @@ def test_setup_env_disabled( assert not config.was_enabled() # double-call to setup skips reconfiguration if disable is the same assert not setup(disable=True) + assert setup(disable=False) def test_setup_env_enabled( From 52c70d0942badedcde21b6c286f0eb1fabb32f5e Mon Sep 17 00:00:00 2001 From: Maxim Lapan Date: Tue, 29 Sep 2026 08:57:50 +0200 Subject: [PATCH 07/15] Update release date --- doc/changes/changes_0.2.1.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changes/changes_0.2.1.md b/doc/changes/changes_0.2.1.md index 1a8ede0..8b94bbc 100644 --- a/doc/changes/changes_0.2.1.md +++ b/doc/changes/changes_0.2.1.md @@ -1,4 +1,4 @@ -# 0.2.1 - 2026-09-28 +# 0.2.1 - 2026-09-29 ## Summary From dd68238baf877c6377e31d60d3adf0537acaec99 Mon Sep 17 00:00:00 2001 From: Maxim Lapan Date: Tue, 29 Sep 2026 13:25:31 +0200 Subject: [PATCH 08/15] Docstrings update, fix of queue.put potential block --- exasol/telemetry/client/setup.py | 6 ++++-- exasol/telemetry/client/worker.py | 7 +++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/exasol/telemetry/client/setup.py b/exasol/telemetry/client/setup.py index 1274e93..0f757e8 100644 --- a/exasol/telemetry/client/setup.py +++ b/exasol/telemetry/client/setup.py @@ -70,13 +70,15 @@ def drop_exit_handlers(): def setup(endpoint: tt.Optional[str] = None, disable: tt.Optional[bool] = None) -> bool: """ - Telemetry client setup function. + Telemetry client setup function (should not be called from a client code). Explicitly given arguments have the highest priority. If they are not given, we check the environment variables (EXASOL_TELEMETRY_XXX), if no environment value, we use defaults (DEFAULT_XXX). - If setup() was called before, we return the enabled status and do not reconfigure. + If setup() was called before, we reconfigure only when previous disable + option was different than a new one. On reconfiguration, we flush the buffers (if any). + In all other cases we return the enabled status without reconfiguration. :param endpoint: Telemetry endpoint to send data. If not given, default endpoint is used. diff --git a/exasol/telemetry/client/worker.py b/exasol/telemetry/client/worker.py index ea0336d..a00644a 100644 --- a/exasol/telemetry/client/worker.py +++ b/exasol/telemetry/client/worker.py @@ -310,5 +310,8 @@ def track( global _queue if _queue is not None: - if _queue.not_full: - _queue.put(WorkerMessage.make_track(product_name, product_version, feature)) + try: + msg = WorkerMessage.make_track(product_name, product_version, feature) + _queue.put_nowait(msg) + except queue.Full: + pass From d4b3e9330f37e82795898e1f67f81335d89509fa Mon Sep 17 00:00:00 2001 From: Maxim Lapan Date: Tue, 29 Sep 2026 15:44:24 +0200 Subject: [PATCH 09/15] Extra test --- test/unit/client/test_worker.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/unit/client/test_worker.py b/test/unit/client/test_worker.py index 3ad5d77..8cca630 100644 --- a/test/unit/client/test_worker.py +++ b/test/unit/client/test_worker.py @@ -97,6 +97,24 @@ def test_worker_proc_sent_quick( mock_post.assert_called_once() +# Make sure that buffers are sent after reconfiguration +@mock.patch("requests.post", return_value=mock.MagicMock(status_code=200)) +def test_worker_proc_sent_after_disable( + mock_post: mock.MagicMock, + telemetry_reset, + telemetry_unset_ci, + telemetry_unset_disable, +): + # enable and send track feature + assert setup(disable=False) + track("product", "ver", "test") + # after reconfiguration we should be disabled and flushed the buffers + assert not setup(disable=True) + mock_post.assert_called_once() + assert config.was_setup() + assert not config.was_enabled() + + # Make sure that features are not sent if not enabled @mock.patch("requests.post") def test_worker_proc_not_sent_when_disabled( From d617d627a8c777164b8188641c6f3a6ce29e3df1 Mon Sep 17 00:00:00 2001 From: Maxim Lapan Date: Wed, 30 Sep 2026 05:54:10 +0200 Subject: [PATCH 10/15] Test on blocked worker --- test/unit/client/test_worker.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/unit/client/test_worker.py b/test/unit/client/test_worker.py index 8cca630..607914f 100644 --- a/test/unit/client/test_worker.py +++ b/test/unit/client/test_worker.py @@ -2,6 +2,8 @@ import time from unittest import mock +from more_itertools.more import side_effect + from exasol.telemetry.client import * from exasol.telemetry.client import ( config, @@ -132,6 +134,19 @@ def test_worker_proc_not_sent_when_disabled( mock_post.assert_not_called() +@mock.patch("requests.post", side_effect=lambda *w, **kw: time.sleep(1000)) +def test_shutdown_exits_on_blocked_worker( + mock_post: mock.MagicMock, + telemetry_reset, + telemetry_unset_ci, + telemetry_unset_disable, +): + assert setup(disable=False) + track("test", "0.1", "test-feature") + shutdown(flush_buffers=True) + mock_post.assert_called_once() + + @mock.patch("exasol.telemetry.client.worker.send_features") def test_worker_proc_no_send(mock_send_features: mock.MagicMock): msg_queue = queue.Queue() From 62cbb668f8843eeb001dfbda078bc5f400a31343 Mon Sep 17 00:00:00 2001 From: Maxim Lapan Date: Wed, 30 Sep 2026 05:55:14 +0200 Subject: [PATCH 11/15] Format --- test/unit/client/test_worker.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/test/unit/client/test_worker.py b/test/unit/client/test_worker.py index 607914f..abd34b0 100644 --- a/test/unit/client/test_worker.py +++ b/test/unit/client/test_worker.py @@ -2,8 +2,6 @@ import time from unittest import mock -from more_itertools.more import side_effect - from exasol.telemetry.client import * from exasol.telemetry.client import ( config, @@ -136,10 +134,10 @@ def test_worker_proc_not_sent_when_disabled( @mock.patch("requests.post", side_effect=lambda *w, **kw: time.sleep(1000)) def test_shutdown_exits_on_blocked_worker( - mock_post: mock.MagicMock, - telemetry_reset, - telemetry_unset_ci, - telemetry_unset_disable, + mock_post: mock.MagicMock, + telemetry_reset, + telemetry_unset_ci, + telemetry_unset_disable, ): assert setup(disable=False) track("test", "0.1", "test-feature") From 2c1cc77ada6418f955eeb83b98f8028f97b95d97 Mon Sep 17 00:00:00 2001 From: Maxim Lapan Date: Wed, 30 Sep 2026 14:19:12 +0200 Subject: [PATCH 12/15] Handle full queue on shutdown --- exasol/telemetry/client/worker.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/exasol/telemetry/client/worker.py b/exasol/telemetry/client/worker.py index a00644a..28b7e94 100644 --- a/exasol/telemetry/client/worker.py +++ b/exasol/telemetry/client/worker.py @@ -268,6 +268,8 @@ def start_worker() -> bool: def stop_worker(flush_buffers: bool): """ Gracefully stops the worker process. + + In case of connectivity issues, flush of buffers might not happen. :param flush_buffers: if True, we'll try to send the buffers (if any), otherwise, we'll just shut down the worker process. """ @@ -277,8 +279,15 @@ def stop_worker(flush_buffers: bool): return if flush_buffers: _queue.put(WorkerMessage.make_send_buffers()) - _queue.put(WorkerMessage.make_terminate()) - _worker.join(timeout=THREAD_EXIT_TIMEOUT_SECONDS) + try: + _queue.put_nowait(WorkerMessage.make_terminate()) + _worker.join(timeout=THREAD_EXIT_TIMEOUT_SECONDS) + except queue.Full: + # rare situation - if the thread is blocked on send and + # we have lots of messages in the queue, we can have no capacity + # in the queue. In such cases, we just don't stop the thread, + # which is fine as thread is daemon. + pass _worker = None _queue = None From d505950236fcd843e3ebb772902fe1021abf49e7 Mon Sep 17 00:00:00 2001 From: Maxim Lapan Date: Wed, 30 Sep 2026 14:21:21 +0200 Subject: [PATCH 13/15] Update docstring --- exasol/telemetry/client/config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/exasol/telemetry/client/config.py b/exasol/telemetry/client/config.py index dc18595..0f3598f 100644 --- a/exasol/telemetry/client/config.py +++ b/exasol/telemetry/client/config.py @@ -56,6 +56,7 @@ def was_enabled() -> bool: def disable_config(): """ Call disables telemetry entirely for all subsequent calls. + Telemetry still might be re-enabled by call to setup() with the opposite disable flag. """ conf = Config(enabled=False, endpoint=DEFAULT_ENDPOINT) store(conf) From c83aa27cc45596d39114fed0914cbddc0d9ab124 Mon Sep 17 00:00:00 2001 From: Maxim Lapan Date: Wed, 30 Sep 2026 14:21:57 +0200 Subject: [PATCH 14/15] Update release date --- doc/changes/changes_0.2.1.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changes/changes_0.2.1.md b/doc/changes/changes_0.2.1.md index 8b94bbc..c25c25f 100644 --- a/doc/changes/changes_0.2.1.md +++ b/doc/changes/changes_0.2.1.md @@ -1,4 +1,4 @@ -# 0.2.1 - 2026-09-29 +# 0.2.1 - 2026-09-30 ## Summary From e387f8a28355cb3cd53068ff5269a057558ec805 Mon Sep 17 00:00:00 2001 From: Maxim Lapan Date: Thu, 1 Oct 2026 07:53:12 +0200 Subject: [PATCH 15/15] Make flush of buffers non-blocking --- exasol/telemetry/client/worker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/exasol/telemetry/client/worker.py b/exasol/telemetry/client/worker.py index 28b7e94..e39f67e 100644 --- a/exasol/telemetry/client/worker.py +++ b/exasol/telemetry/client/worker.py @@ -277,9 +277,9 @@ def stop_worker(flush_buffers: bool): if _worker is None or _queue is None: return - if flush_buffers: - _queue.put(WorkerMessage.make_send_buffers()) try: + if flush_buffers: + _queue.put_nowait(WorkerMessage.make_send_buffers()) _queue.put_nowait(WorkerMessage.make_terminate()) _worker.join(timeout=THREAD_EXIT_TIMEOUT_SECONDS) except queue.Full: