rp_python_sdk 1.5.1__tar.gz → 1.5.2__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.
Files changed (37) hide show
  1. {rp_python_sdk-1.5.1 → rp_python_sdk-1.5.2}/PKG-INFO +9 -4
  2. {rp_python_sdk-1.5.1 → rp_python_sdk-1.5.2}/README.md +8 -3
  3. {rp_python_sdk-1.5.1 → rp_python_sdk-1.5.2}/pyproject.toml +1 -1
  4. rp_python_sdk-1.5.2/rp_python_sdk/endpoints/util/version_info.py +2 -0
  5. {rp_python_sdk-1.5.1 → rp_python_sdk-1.5.2}/rp_python_sdk/relying_party_client_sdk.py +17 -6
  6. {rp_python_sdk-1.5.1 → rp_python_sdk-1.5.2}/rp_python_sdk/telemetry/telemetry_config.py +1 -1
  7. {rp_python_sdk-1.5.1 → rp_python_sdk-1.5.2}/rp_python_sdk/telemetry/telemetry_dispatcher.py +34 -0
  8. rp_python_sdk-1.5.2/rp_python_sdk/telemetry/telemetry_session_pool.py +81 -0
  9. {rp_python_sdk-1.5.1 → rp_python_sdk-1.5.2}/rp_python_sdk/telemetry/telemetry_token_provider.py +10 -8
  10. {rp_python_sdk-1.5.1 → rp_python_sdk-1.5.2}/rp_python_sdk/telemetry/telemetry_transport.py +38 -13
  11. rp_python_sdk-1.5.1/rp_python_sdk/endpoints/util/version_info.py +0 -2
  12. {rp_python_sdk-1.5.1 → rp_python_sdk-1.5.2}/rp_python_sdk/__init__.py +0 -0
  13. {rp_python_sdk-1.5.1 → rp_python_sdk-1.5.2}/rp_python_sdk/cache/__init__.py +0 -0
  14. {rp_python_sdk-1.5.1 → rp_python_sdk-1.5.2}/rp_python_sdk/cache/http_cache.py +0 -0
  15. {rp_python_sdk-1.5.1 → rp_python_sdk-1.5.2}/rp_python_sdk/endpoints/get_participants.py +0 -0
  16. {rp_python_sdk-1.5.1 → rp_python_sdk-1.5.2}/rp_python_sdk/endpoints/pushed_authorisation_request.py +0 -0
  17. {rp_python_sdk-1.5.1 → rp_python_sdk-1.5.2}/rp_python_sdk/endpoints/retrieve_tokens.py +0 -0
  18. {rp_python_sdk-1.5.1 → rp_python_sdk-1.5.2}/rp_python_sdk/endpoints/user_info.py +0 -0
  19. {rp_python_sdk-1.5.1 → rp_python_sdk-1.5.2}/rp_python_sdk/endpoints/util/client_assertion.py +0 -0
  20. {rp_python_sdk-1.5.1 → rp_python_sdk-1.5.2}/rp_python_sdk/endpoints/util/fapi.py +0 -0
  21. {rp_python_sdk-1.5.1 → rp_python_sdk-1.5.2}/rp_python_sdk/endpoints/util/mapping.py +0 -0
  22. {rp_python_sdk-1.5.1 → rp_python_sdk-1.5.2}/rp_python_sdk/endpoints/util/system_info.py +0 -0
  23. {rp_python_sdk-1.5.1 → rp_python_sdk-1.5.2}/rp_python_sdk/endpoints/util/user_agent.py +0 -0
  24. {rp_python_sdk-1.5.1 → rp_python_sdk-1.5.2}/rp_python_sdk/filters/participant_filters.py +0 -0
  25. {rp_python_sdk-1.5.1 → rp_python_sdk-1.5.2}/rp_python_sdk/model.py +0 -0
  26. {rp_python_sdk-1.5.1 → rp_python_sdk-1.5.2}/rp_python_sdk/relying_party_client_sdk_exception.py +0 -0
  27. {rp_python_sdk-1.5.1 → rp_python_sdk-1.5.2}/rp_python_sdk/sdk_config.py +0 -0
  28. {rp_python_sdk-1.5.1 → rp_python_sdk-1.5.2}/rp_python_sdk/setup_logger.py +0 -0
  29. {rp_python_sdk-1.5.1 → rp_python_sdk-1.5.2}/rp_python_sdk/telemetry/__init__.py +0 -0
  30. {rp_python_sdk-1.5.1 → rp_python_sdk-1.5.2}/rp_python_sdk/telemetry/async_telemetry_sender.py +0 -0
  31. {rp_python_sdk-1.5.1 → rp_python_sdk-1.5.2}/rp_python_sdk/telemetry/sync_telemetry_sender.py +0 -0
  32. {rp_python_sdk-1.5.1 → rp_python_sdk-1.5.2}/rp_python_sdk/telemetry/telemetry_context.py +0 -0
  33. {rp_python_sdk-1.5.1 → rp_python_sdk-1.5.2}/rp_python_sdk/telemetry/telemetry_event.py +0 -0
  34. {rp_python_sdk-1.5.1 → rp_python_sdk-1.5.2}/rp_python_sdk/telemetry/telemetry_event_factory.py +0 -0
  35. {rp_python_sdk-1.5.1 → rp_python_sdk-1.5.2}/rp_python_sdk/telemetry/telemetry_sender.py +0 -0
  36. {rp_python_sdk-1.5.1 → rp_python_sdk-1.5.2}/rp_python_sdk/telemetry/telemetry_session.py +0 -0
  37. {rp_python_sdk-1.5.1 → rp_python_sdk-1.5.2}/rp_python_sdk/telemetry/telemetry_url_resolver.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rp_python_sdk
3
- Version: 1.5.1
3
+ Version: 1.5.2
4
4
  Summary: Python SDK for Relying Parties to enable simple integration with the Digital Identity ecosystem
5
5
  License: Proprietary
6
6
  Author: Erik Pragt
@@ -421,7 +421,7 @@ with a unique `x-fapi-interaction-id` that was used for this request to the auth
421
421
  The SDK emits non-sensitive telemetry about its PAR, Token and UserInfo endpoint interactions to the
422
422
  centralized ConnectID telemetry service over mTLS. Telemetry is controlled through a `TelemetryConfig` supplied on `CustomConfig`.
423
423
 
424
- > **Note:** Telemetry is temporarily **disabled by default** while a bugfix is in progress. Please do not enable telemetry until the fix is released.
424
+ > **Note:** Telemetry is **enabled by default**. To opt out, set `TelemetryConfig(enabled=False)` on your `CustomConfig`.
425
425
 
426
426
  When telemetry is disabled the SDK constructs no transport, session, dispatcher, token provider or worker
427
427
  threads, so there is no overhead.
@@ -457,7 +457,7 @@ phase is given the full budget afresh.
457
457
 
458
458
  | Field | Default | Description |
459
459
  | --- | --- | --- |
460
- | `enabled` | `False` | Master on/off switch for telemetry. |
460
+ | `enabled` | `True` | Master on/off switch for telemetry. |
461
461
  | `endpoint_url` | `None` | Override for the telemetry ingest URL. When `None`, the URL is derived from the SDK environment. |
462
462
  | `registry_discovery_document_url` | `None` | Override for the Registry OIDC discovery document URL used to obtain a telemetry access token. When `None`, derived from the environment. |
463
463
  | `delivery_mode` | `TelemetryDeliveryMode.ASYNC` | `ASYNC` (background dispatch) or `SYNC` (inline delivery). |
@@ -602,8 +602,13 @@ poetry publish
602
602
 
603
603
  ## Release Notes
604
604
 
605
+ ### 1.5.2 (Sep 30, 2026)
606
+ - **Telemetry re-enabled by default**: Telemetry is now enabled by default again after the concurrency and shutdown bugfixes below. Opt out with `TelemetryConfig(enabled=False)`.
607
+ - **Concurrent telemetry session isolation**: Fixed a race condition where overlapping SDK operations could share or corrupt telemetry session state.
608
+ - **Daemon thread shutdown with atexit handler**: Fixed an issue where daemon worker threads were not cleaned up when `close()` was not called explicitly, by registering an `atexit` handler for graceful shutdown.
609
+
605
610
  ### 1.5.1 (Sep 25, 2026)
606
- - **Telemetry disabled by default**: Telemetry is temporarily disabled while a bugfix is in progress. Please do not enable telemetry until the fix is released.
611
+ - **Telemetry disabled by default**: Telemetry was temporarily disabled while a bugfix was in progress.
607
612
 
608
613
  ### 1.5.0 (Sep 22, 2026)
609
614
  - **SDK Telemetry**: Added opt-out telemetry for PAR, Token, and UserInfo endpoint calls — see the [Telemetry](#telemetry) section for full details
@@ -398,7 +398,7 @@ with a unique `x-fapi-interaction-id` that was used for this request to the auth
398
398
  The SDK emits non-sensitive telemetry about its PAR, Token and UserInfo endpoint interactions to the
399
399
  centralized ConnectID telemetry service over mTLS. Telemetry is controlled through a `TelemetryConfig` supplied on `CustomConfig`.
400
400
 
401
- > **Note:** Telemetry is temporarily **disabled by default** while a bugfix is in progress. Please do not enable telemetry until the fix is released.
401
+ > **Note:** Telemetry is **enabled by default**. To opt out, set `TelemetryConfig(enabled=False)` on your `CustomConfig`.
402
402
 
403
403
  When telemetry is disabled the SDK constructs no transport, session, dispatcher, token provider or worker
404
404
  threads, so there is no overhead.
@@ -434,7 +434,7 @@ phase is given the full budget afresh.
434
434
 
435
435
  | Field | Default | Description |
436
436
  | --- | --- | --- |
437
- | `enabled` | `False` | Master on/off switch for telemetry. |
437
+ | `enabled` | `True` | Master on/off switch for telemetry. |
438
438
  | `endpoint_url` | `None` | Override for the telemetry ingest URL. When `None`, the URL is derived from the SDK environment. |
439
439
  | `registry_discovery_document_url` | `None` | Override for the Registry OIDC discovery document URL used to obtain a telemetry access token. When `None`, derived from the environment. |
440
440
  | `delivery_mode` | `TelemetryDeliveryMode.ASYNC` | `ASYNC` (background dispatch) or `SYNC` (inline delivery). |
@@ -579,8 +579,13 @@ poetry publish
579
579
 
580
580
  ## Release Notes
581
581
 
582
+ ### 1.5.2 (Sep 30, 2026)
583
+ - **Telemetry re-enabled by default**: Telemetry is now enabled by default again after the concurrency and shutdown bugfixes below. Opt out with `TelemetryConfig(enabled=False)`.
584
+ - **Concurrent telemetry session isolation**: Fixed a race condition where overlapping SDK operations could share or corrupt telemetry session state.
585
+ - **Daemon thread shutdown with atexit handler**: Fixed an issue where daemon worker threads were not cleaned up when `close()` was not called explicitly, by registering an `atexit` handler for graceful shutdown.
586
+
582
587
  ### 1.5.1 (Sep 25, 2026)
583
- - **Telemetry disabled by default**: Telemetry is temporarily disabled while a bugfix is in progress. Please do not enable telemetry until the fix is released.
588
+ - **Telemetry disabled by default**: Telemetry was temporarily disabled while a bugfix was in progress.
584
589
 
585
590
  ### 1.5.0 (Sep 22, 2026)
586
591
  - **SDK Telemetry**: Added opt-out telemetry for PAR, Token, and UserInfo endpoint calls — see the [Telemetry](#telemetry) section for full details
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "rp_python_sdk"
3
- version = "1.5.1"
3
+ version = "1.5.2"
4
4
  description = "Python SDK for Relying Parties to enable simple integration with the Digital Identity ecosystem"
5
5
  authors = ["Erik Pragt <erik.pragt@connectid.com.au>"]
6
6
  license = "Proprietary"
@@ -0,0 +1,2 @@
1
+ def get_version_info() -> str:
2
+ return "1.5.2" # Version number
@@ -25,13 +25,24 @@ def _create_telemetry_sender(config: SdkConfig) -> TelemetrySender:
25
25
  logger.info("Telemetry is disabled; no telemetry events will be sent.")
26
26
  return NoopTelemetrySender.instance()
27
27
 
28
- logger.info("Telemetry is enabled; telemetry events will be sent to the telemetry service.")
29
- transport = TelemetryTransport.create(config)
30
- if telemetry.delivery_mode is TelemetryDeliveryMode.SYNC:
31
- return SyncTelemetrySender(transport, telemetry.sync_send_budget_seconds)
28
+ transport: Optional[TelemetryTransport] = None
29
+ try:
30
+ transport = TelemetryTransport.create(config)
31
+ if telemetry.delivery_mode is TelemetryDeliveryMode.SYNC:
32
+ sender: TelemetrySender = SyncTelemetrySender(
33
+ transport, telemetry.sync_send_budget_seconds)
34
+ else:
35
+ dispatcher = TelemetryDispatcher(
36
+ telemetry.queue_capacity, telemetry.max_concurrent_sends)
37
+ sender = AsyncTelemetrySender(transport, dispatcher)
38
+ except Exception as exception:
39
+ if transport is not None:
40
+ transport.close()
41
+ logger.warning(f"Failed to initialise telemetry; telemetry is disabled: {exception}")
42
+ return NoopTelemetrySender.instance()
32
43
 
33
- dispatcher = TelemetryDispatcher(telemetry.queue_capacity, telemetry.max_concurrent_sends)
34
- return AsyncTelemetrySender(transport, dispatcher)
44
+ logger.info("Telemetry is enabled; telemetry events will be sent to the telemetry service.")
45
+ return sender
35
46
 
36
47
 
37
48
  def _close_telemetry_sender(sender: TelemetrySender) -> None:
@@ -10,7 +10,7 @@ class TelemetryDeliveryMode(str, Enum):
10
10
 
11
11
  @dataclass
12
12
  class TelemetryConfig:
13
- enabled: bool = False
13
+ enabled: bool = True
14
14
  endpoint_url: Optional[str] = None
15
15
  registry_discovery_document_url: Optional[str] = None
16
16
  delivery_mode: TelemetryDeliveryMode = TelemetryDeliveryMode.ASYNC
@@ -1,14 +1,41 @@
1
+ import atexit
1
2
  import queue
2
3
  import threading
3
4
  import time
5
+ import weakref
4
6
  from typing import Callable, List
5
7
 
6
8
  from rp_python_sdk.setup_logger import logger
7
9
 
8
10
  DEFAULT_GRACE_PERIOD_SECONDS = 2.0
11
+ DEFAULT_EXIT_GRACE_PERIOD_SECONDS = 12.0
9
12
  DEFAULT_IDLE_TIMEOUT_SECONDS = 30.0
10
13
  POLL_INTERVAL_SECONDS = 0.1
11
14
 
15
+ # Weak references to all live dispatchers. The atexit handler iterates these and
16
+ # calls close() on any that are still alive, ensuring daemon worker threads are
17
+ # joined before the interpreter tears down — preventing the
18
+ # 'Fatal Python error: Aborted' on Python 3.12/Linux when threads are in C-level SSL code.
19
+ _live_dispatchers: weakref.WeakSet["TelemetryDispatcher"] = weakref.WeakSet()
20
+ _atexit_registered = False
21
+ _atexit_lock = threading.Lock()
22
+
23
+
24
+ def _shutdown_all_dispatchers() -> None:
25
+ for dispatcher in list(_live_dispatchers):
26
+ try:
27
+ dispatcher.close(grace_period_seconds=DEFAULT_EXIT_GRACE_PERIOD_SECONDS)
28
+ except Exception as exception:
29
+ logger.debug(f"Ignoring error while closing telemetry dispatcher at exit: {exception}")
30
+
31
+
32
+ def _register_atexit_hook() -> None:
33
+ global _atexit_registered
34
+ with _atexit_lock:
35
+ if not _atexit_registered:
36
+ atexit.register(_shutdown_all_dispatchers)
37
+ _atexit_registered = True
38
+
12
39
 
13
40
  class TelemetryDispatcher:
14
41
 
@@ -32,6 +59,9 @@ class TelemetryDispatcher:
32
59
  # Monotonic so a replacement worker never reuses a retired worker's thread name.
33
60
  self._next_worker_number = 0
34
61
 
62
+ _live_dispatchers.add(self)
63
+ _register_atexit_hook()
64
+
35
65
  def enqueue(self, task: Callable[[], None]) -> bool:
36
66
  """Queues a task for background execution, returning False when it was dropped.
37
67
 
@@ -54,11 +84,15 @@ class TelemetryDispatcher:
54
84
 
55
85
  def close(self, grace_period_seconds: float = DEFAULT_GRACE_PERIOD_SECONDS) -> None:
56
86
  with self._lock:
87
+ if self._shutdown.is_set():
88
+ return
57
89
  self._shutdown.set()
58
90
  self._discard_pending()
59
91
  workers = list(self._workers)
60
92
  self._workers.clear()
61
93
 
94
+ _live_dispatchers.discard(self)
95
+
62
96
  deadline = time.monotonic() + grace_period_seconds
63
97
  for worker in workers:
64
98
  remaining = max(0.0, deadline - time.monotonic())
@@ -0,0 +1,81 @@
1
+ import queue
2
+ import threading
3
+ from typing import Iterable, Optional
4
+
5
+ import requests
6
+
7
+ from rp_python_sdk.setup_logger import logger
8
+
9
+
10
+ class TelemetrySessionPool:
11
+ """Hands each telemetry send exclusive ownership of one HTTP session.
12
+
13
+ ``requests.Session`` contains mutable adapter and connection-pool state. A session therefore
14
+ stays leased to one worker for the complete token-and-event exchange. Shutdown closes idle
15
+ sessions immediately, while a leased session is closed by the worker that returns it. This
16
+ keeps shutdown bounded without closing TLS resources underneath an in-flight request.
17
+ """
18
+
19
+ def __init__(self, sessions: Iterable[requests.Session]) -> None:
20
+ available_sessions = list(sessions)
21
+ if not available_sessions:
22
+ raise ValueError("At least one telemetry session is required")
23
+
24
+ self._available: queue.LifoQueue[requests.Session] = queue.LifoQueue(
25
+ maxsize=len(available_sessions)
26
+ )
27
+ for session in available_sessions:
28
+ self._available.put_nowait(session)
29
+
30
+ self._lock = threading.Lock()
31
+ self._closed = False
32
+
33
+ def acquire(self) -> Optional[requests.Session]:
34
+ """Returns an exclusively leased session, or None without blocking."""
35
+ with self._lock:
36
+ if self._closed:
37
+ return None
38
+ try:
39
+ return self._available.get_nowait()
40
+ except queue.Empty:
41
+ return None
42
+
43
+ def release(self, session: requests.Session) -> None:
44
+ """Returns a lease, closing it instead when pool shutdown has begun."""
45
+ close_session = False
46
+ with self._lock:
47
+ if self._closed:
48
+ close_session = True
49
+ else:
50
+ try:
51
+ self._available.put_nowait(session)
52
+ except queue.Full:
53
+ # A duplicate release is a lifecycle bug, but telemetry cleanup must not
54
+ # surface it to application code. Close the redundant resource instead.
55
+ close_session = True
56
+
57
+ if close_session:
58
+ self._close_session(session)
59
+
60
+ def close(self) -> None:
61
+ """Prevents new leases and closes idle resources without waiting for active sends."""
62
+ idle_sessions: list[requests.Session] = []
63
+ with self._lock:
64
+ if self._closed:
65
+ return
66
+ self._closed = True
67
+ while True:
68
+ try:
69
+ idle_sessions.append(self._available.get_nowait())
70
+ except queue.Empty:
71
+ break
72
+
73
+ for session in idle_sessions:
74
+ self._close_session(session)
75
+
76
+ @staticmethod
77
+ def _close_session(session: requests.Session) -> None:
78
+ try:
79
+ session.close()
80
+ except Exception as exception:
81
+ logger.debug(f"Ignoring error while closing telemetry HTTP session: {exception}")
@@ -27,22 +27,24 @@ class TelemetryTokenProvider:
27
27
  self._expires_at: float = 0.0
28
28
  self._discovery_document: Optional[dict[str, Any]] = None
29
29
 
30
- def get_access_token(self, timeout: RequestsTimeout) -> Optional[str]:
30
+ def get_access_token(self, timeout: RequestsTimeout,
31
+ session: Optional[requests.Session] = None) -> Optional[str]:
31
32
  if self._is_token_valid():
32
33
  return self._access_token
33
34
 
34
35
  with self._lock:
35
36
  if self._is_token_valid():
36
37
  return self._access_token
37
- return self._refresh_token(timeout)
38
+ selected_session = session if session is not None else self._session
39
+ return self._refresh_token(timeout, selected_session)
38
40
 
39
41
  def _is_token_valid(self) -> bool:
40
42
  return (self._access_token is not None
41
43
  and time.time() + self.TOKEN_REFRESH_BUFFER_SECONDS < self._expires_at)
42
44
 
43
- def _refresh_token(self, timeout: RequestsTimeout) -> Optional[str]:
45
+ def _refresh_token(self, timeout: RequestsTimeout, session: requests.Session) -> Optional[str]:
44
46
  try:
45
- discovery_document = self._get_discovery_document(timeout)
47
+ discovery_document = self._get_discovery_document(timeout, session)
46
48
  token_endpoint = self._resolve_token_endpoint(discovery_document)
47
49
  audience = discovery_document.get("issuer") or token_endpoint
48
50
 
@@ -57,7 +59,7 @@ class TelemetryTokenProvider:
57
59
  "client_assertion_type": _CLIENT_ASSERTION_TYPE,
58
60
  }
59
61
 
60
- response = self._session.post(token_endpoint, data=data, timeout=timeout)
62
+ response = session.post(token_endpoint, data=data, timeout=timeout)
61
63
  response.raise_for_status()
62
64
  token_response = response.json()
63
65
 
@@ -73,11 +75,12 @@ class TelemetryTokenProvider:
73
75
  self._expires_at = 0.0
74
76
  return None
75
77
 
76
- def _get_discovery_document(self, timeout: RequestsTimeout) -> dict[str, Any]:
78
+ def _get_discovery_document(self, timeout: RequestsTimeout,
79
+ session: requests.Session) -> dict[str, Any]:
77
80
  if self._discovery_document is not None:
78
81
  return self._discovery_document
79
82
 
80
- response = self._session.get(self._registry_discovery_url, timeout=timeout)
83
+ response = session.get(self._registry_discovery_url, timeout=timeout)
81
84
  response.raise_for_status()
82
85
  discovery_document: dict[str, Any] = response.json()
83
86
 
@@ -89,4 +92,3 @@ class TelemetryTokenProvider:
89
92
  mtls_aliases = discovery_document.get("mtls_endpoint_aliases") or {}
90
93
  token_endpoint = mtls_aliases.get("token_endpoint") or discovery_document["token_endpoint"]
91
94
  return str(token_endpoint)
92
-
@@ -1,5 +1,5 @@
1
1
  import json
2
- from typing import Any, Optional
2
+ from typing import Any, Optional, Sequence
3
3
 
4
4
  import requests
5
5
 
@@ -7,6 +7,7 @@ from rp_python_sdk.sdk_config import SdkConfig
7
7
  from rp_python_sdk.setup_logger import logger
8
8
  from rp_python_sdk.telemetry.telemetry_event import TelemetryEvent
9
9
  from rp_python_sdk.telemetry.telemetry_session import create_telemetry_session
10
+ from rp_python_sdk.telemetry.telemetry_session_pool import TelemetrySessionPool
10
11
  from rp_python_sdk.telemetry.telemetry_token_provider import (
11
12
  TelemetryTokenProvider,
12
13
  RequestsTimeout,
@@ -19,19 +20,34 @@ _JSON_MEDIA_TYPE = "application/json"
19
20
 
20
21
  class TelemetryTransport:
21
22
  def __init__(self, session: requests.Session, token_provider: TelemetryTokenProvider,
22
- telemetry_endpoint_url: str) -> None:
23
- self._session = session
23
+ telemetry_endpoint_url: str,
24
+ additional_sessions: Sequence[requests.Session] = ()) -> None:
25
+ self._sessions = TelemetrySessionPool([session, *additional_sessions])
24
26
  self._token_provider = token_provider
25
27
  self._telemetry_endpoint_url = telemetry_endpoint_url
26
28
 
27
29
  @classmethod
28
30
  def create(cls, config: SdkConfig) -> "TelemetryTransport":
29
31
  telemetry_urls = resolve_telemetry_urls(config)
30
- session = create_telemetry_session(config, config.custom_config.telemetry_config.max_concurrent_sends)
31
- token_provider = TelemetryTokenProvider(session, config,
32
- telemetry_urls.registry_discovery_document_url)
33
-
34
- return cls(session, token_provider, telemetry_urls.telemetry_endpoint_url)
32
+ session_count = config.custom_config.telemetry_config.max_concurrent_sends
33
+ sessions: list[requests.Session] = []
34
+ try:
35
+ # A session is never shared between workers. Each session needs only one connection
36
+ # slot because the dispatcher already bounds the number of concurrent sends.
37
+ for _ in range(session_count):
38
+ sessions.append(create_telemetry_session(config, pool_size=1))
39
+ token_provider = TelemetryTokenProvider(
40
+ sessions[0], config, telemetry_urls.registry_discovery_document_url)
41
+ return cls(sessions[0], token_provider, telemetry_urls.telemetry_endpoint_url,
42
+ additional_sessions=sessions[1:])
43
+ except Exception:
44
+ for session in sessions:
45
+ try:
46
+ session.close()
47
+ except Exception as close_error:
48
+ logger.debug(f"Ignoring error while cleaning up telemetry HTTP session: "
49
+ f"{close_error}")
50
+ raise
35
51
 
36
52
  @property
37
53
  def token_provider(self) -> TelemetryTokenProvider:
@@ -39,13 +55,20 @@ class TelemetryTransport:
39
55
 
40
56
  def send(self, event: TelemetryEvent, timeout: RequestsTimeout,
41
57
  token: Optional[str] = None) -> None:
58
+ session = self._sessions.acquire()
59
+ if session is None:
60
+ logger.debug(f"Skipping telemetry event type {event.type} "
61
+ f"(x-fapi-interaction-id: {event.x_fapi_interaction_id}): "
62
+ f"no exclusive HTTP session is available.")
63
+ return
64
+
42
65
  try:
43
66
  logger.info(f"Dispatching telemetry event type {event.type} "
44
67
  f"(x-fapi-interaction-id: {event.x_fapi_interaction_id}) "
45
68
  f"to {self._telemetry_endpoint_url}")
46
69
 
47
70
  if token is None:
48
- token = self._token_provider.get_access_token(timeout)
71
+ token = self._token_provider.get_access_token(timeout, session)
49
72
  if token is None:
50
73
  logger.warning(f"Skipping telemetry event type {event.type} "
51
74
  f"(x-fapi-interaction-id: {event.x_fapi_interaction_id}): "
@@ -59,9 +82,9 @@ class TelemetryTransport:
59
82
  _FAPI_INTERACTION_ID_HEADER: event.x_fapi_interaction_id,
60
83
  }
61
84
 
62
- response = self._session.post(self._telemetry_endpoint_url, data=body,
63
- headers=headers,
64
- timeout=timeout)
85
+ response = session.post(self._telemetry_endpoint_url, data=body,
86
+ headers=headers,
87
+ timeout=timeout)
65
88
 
66
89
  if not response.ok:
67
90
  logger.warning(f"Telemetry send for event type {event.type} "
@@ -74,6 +97,8 @@ class TelemetryTransport:
74
97
  except Exception as exception:
75
98
  logger.warning(f"Failed to send telemetry event type {event.type} "
76
99
  f"(x-fapi-interaction-id: {event.x_fapi_interaction_id}): {exception}")
100
+ finally:
101
+ self._sessions.release(session)
77
102
 
78
103
  def close(self) -> None:
79
- self._session.close()
104
+ self._sessions.close()
@@ -1,2 +0,0 @@
1
- def get_version_info() -> str:
2
- return "1.5.1" # Version number