Skip to content

Commit c3059bd

Browse files
authored
fix: prevent aiohttp from becoming mandatory to use the sync SDK (#441)
to do so, I've moved the async metrics reporter to its own file.
1 parent e17e615 commit c3059bd

7 files changed

Lines changed: 555 additions & 515 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
> went on. It needs to be consolidated into what will eventually become the
55
> final changelog + documentation changes (if any).
66
7+
* (Bugfix): `UnleashClient` can be imported again without the optional `aiohttp` dependency. Importing the package used to fail with an `ImportError` asking for `UnleashClient[async]`, even for code that never touches the asynchronous client. Only the asynchronous client requires `aiohttp` now, and importing it without `aiohttp` still raises that `ImportError`.
78
* (Minor): The in-progress asynchronous client now exposes `feature_definitions()`. It returns the same dict as `UnleashClient`, keyed by feature name with each toggle's `type` and `project`. It is a plain method, not a coroutine, and it does not wait for the server: before the first fetch it answers from the cached state, and without any state it returns an empty dict. Every method on the client is now implemented. The client is still unexported. Nothing changes for code using `UnleashClient`.
89
* (Minor): The in-progress asynchronous client can now resolve variants. `get_variant()` returns the same variant dict as `UnleashClient` and emits the same impression events. It is a plain method, not a coroutine, and it does not wait for the server: before the first fetch it answers from the cached state, and a toggle the client does not know resolves to the disabled variant. When an initialized client is asked for a toggle it does not know, it logs at `verbose_log_level` that the client does not know the toggle. `feature_definitions()` still raises `NotImplementedError`. The client is still unexported. Nothing changes for code using `UnleashClient`.
910
* (Minor): The in-progress asynchronous client can now evaluate feature toggles. `is_enabled()` resolves a toggle against the feature state the client holds, with the same results, `fallback_function` handling and impression events as `UnleashClient`. It is a plain method, not a coroutine, and it does not wait for the server: before the first fetch it answers from the cached state, and a toggle the client does not know resolves to the fallback's answer, or to false without one. `get_variant()` and `feature_definitions()` still raise `NotImplementedError`. The client is still unexported. Nothing changes for code using `UnleashClient`.

‎UnleashClient/_async_metrics.py‎

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
"""Metrics reporting, for the async Unleash client."""
2+
3+
from typing import Optional
4+
5+
from yggdrasil_engine.engine import UnleashEngine
6+
7+
from UnleashClient._async_scheduler import _AsyncJob, _AsyncScheduler
8+
from UnleashClient._async_transport import _AsyncTransport
9+
from UnleashClient._payloads import _build_metrics_payload
10+
from UnleashClient.config import UnleashConfig
11+
from UnleashClient.impact_metrics import ImpactMetrics
12+
from UnleashClient.utils import LOGGER
13+
14+
15+
class _AsyncMetricsReporter:
16+
"""
17+
Sends feature and impact metrics to Unleash on a recurring interval.
18+
19+
:meth:`start` must be called, and :meth:`stop` awaited, from the event loop the client
20+
runs on, and the loop must stay open for as long as metrics are being reported.
21+
22+
Example::
23+
24+
reporter = _AsyncMetricsReporter(
25+
config=config,
26+
transport=transport,
27+
scheduler=scheduler,
28+
engine=engine,
29+
impact_metrics=impact_metrics,
30+
)
31+
reporter.start()
32+
33+
await reporter.flush()
34+
35+
await reporter.stop()
36+
"""
37+
38+
def __init__(
39+
self,
40+
config: UnleashConfig,
41+
transport: _AsyncTransport,
42+
scheduler: _AsyncScheduler,
43+
engine: UnleashEngine,
44+
impact_metrics: ImpactMetrics,
45+
) -> None:
46+
self._config: UnleashConfig = config
47+
self._transport: _AsyncTransport = transport
48+
self._scheduler: _AsyncScheduler = scheduler
49+
self._engine: UnleashEngine = engine
50+
self._impact_metrics: ImpactMetrics = impact_metrics
51+
self._job: Optional[_AsyncJob] = None
52+
53+
def start(self) -> None:
54+
"""Schedules a send every ``metrics_interval`` seconds, with ``metrics_jitter`` of jitter."""
55+
self._job = self._scheduler.every(
56+
interval_seconds=int(self._config.metrics_interval),
57+
jitter_seconds=self._config.metrics_jitter,
58+
fn=self.flush,
59+
)
60+
61+
async def flush(self) -> None:
62+
"""
63+
Sends one bucket of feature and impact metrics.
64+
65+
Sends nothing when neither has anything to report. When a send fails or is
66+
cancelled, its impact metrics are restored so the next send carries them.
67+
"""
68+
bucket = self._engine.get_metrics()
69+
impact_metrics = self._impact_metrics.collect()
70+
71+
if not (bucket or impact_metrics):
72+
LOGGER.debug("No feature flags with metrics, skipping metrics submission.")
73+
return
74+
75+
payload = _build_metrics_payload(self._config, bucket, impact_metrics)
76+
sent = False
77+
try:
78+
sent = await self._transport.send_metrics(payload)
79+
finally:
80+
if not sent and impact_metrics:
81+
self._impact_metrics.restore(impact_metrics)
82+
83+
async def stop(self) -> None:
84+
"""
85+
Stops the recurring send and flushes whatever is left.
86+
87+
Does nothing when :meth:`start` was never called. A send still in flight is
88+
cancelled: its impact metrics go out with the final flush, and its feature
89+
metrics are lost.
90+
"""
91+
if self._job is None:
92+
return
93+
94+
job, self._job = self._job, None
95+
await self._scheduler.cancel_and_wait(job)
96+
await self.flush()

‎UnleashClient/_metrics.py‎

Lines changed: 1 addition & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
1-
"""Metrics reporting, for the sync and async Unleash clients."""
2-
3-
from typing import Optional
1+
"""Metrics reporting, for the sync Unleash client."""
42

53
from yggdrasil_engine.engine import UnleashEngine
64

7-
from UnleashClient._async_scheduler import _AsyncJob, _AsyncScheduler
8-
from UnleashClient._async_transport import _AsyncTransport
95
from UnleashClient._payloads import _build_metrics_payload
106
from UnleashClient._scheduler import _ScheduledJob, _Scheduler
117
from UnleashClient._transport import _Transport
@@ -98,87 +94,3 @@ def stop(self) -> None:
9894
self.flush()
9995
self._scheduler.cancel(self._job)
10096
self._job = None
101-
102-
103-
class _AsyncMetricsReporter:
104-
"""
105-
Sends feature and impact metrics to Unleash on a recurring interval.
106-
107-
:meth:`start` must be called, and :meth:`stop` awaited, from the event loop the client
108-
runs on, and the loop must stay open for as long as metrics are being reported.
109-
110-
Example::
111-
112-
reporter = _AsyncMetricsReporter(
113-
config=config,
114-
transport=transport,
115-
scheduler=scheduler,
116-
engine=engine,
117-
impact_metrics=impact_metrics,
118-
)
119-
reporter.start()
120-
121-
await reporter.flush()
122-
123-
await reporter.stop()
124-
"""
125-
126-
def __init__(
127-
self,
128-
config: UnleashConfig,
129-
transport: _AsyncTransport,
130-
scheduler: _AsyncScheduler,
131-
engine: UnleashEngine,
132-
impact_metrics: ImpactMetrics,
133-
) -> None:
134-
self._config: UnleashConfig = config
135-
self._transport: _AsyncTransport = transport
136-
self._scheduler: _AsyncScheduler = scheduler
137-
self._engine: UnleashEngine = engine
138-
self._impact_metrics: ImpactMetrics = impact_metrics
139-
self._job: Optional[_AsyncJob] = None
140-
141-
def start(self) -> None:
142-
"""Schedules a send every ``metrics_interval`` seconds, with ``metrics_jitter`` of jitter."""
143-
self._job = self._scheduler.every(
144-
interval_seconds=int(self._config.metrics_interval),
145-
jitter_seconds=self._config.metrics_jitter,
146-
fn=self.flush,
147-
)
148-
149-
async def flush(self) -> None:
150-
"""
151-
Sends one bucket of feature and impact metrics.
152-
153-
Sends nothing when neither has anything to report. When a send fails or is
154-
cancelled, its impact metrics are restored so the next send carries them.
155-
"""
156-
bucket = self._engine.get_metrics()
157-
impact_metrics = self._impact_metrics.collect()
158-
159-
if not (bucket or impact_metrics):
160-
LOGGER.debug("No feature flags with metrics, skipping metrics submission.")
161-
return
162-
163-
payload = _build_metrics_payload(self._config, bucket, impact_metrics)
164-
sent = False
165-
try:
166-
sent = await self._transport.send_metrics(payload)
167-
finally:
168-
if not sent and impact_metrics:
169-
self._impact_metrics.restore(impact_metrics)
170-
171-
async def stop(self) -> None:
172-
"""
173-
Stops the recurring send and flushes whatever is left.
174-
175-
Does nothing when :meth:`start` was never called. A send still in flight is
176-
cancelled: its impact metrics go out with the final flush, and its feature
177-
metrics are lost.
178-
"""
179-
if self._job is None:
180-
return
181-
182-
job, self._job = self._job, None
183-
await self._scheduler.cancel_and_wait(job)
184-
await self.flush()

‎UnleashClient/clients/async_unleash_client.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from yggdrasil_engine.engine import UnleashEngine
1212

13+
from UnleashClient._async_metrics import _AsyncMetricsReporter
1314
from UnleashClient._async_scheduler import _AsyncScheduler
1415
from UnleashClient._async_transport import _AsyncTransport
1516
from UnleashClient._context import _ContextEnricher
@@ -18,7 +19,6 @@
1819
from UnleashClient._feature_store import _FeatureStore
1920
from UnleashClient._headers import _HeaderFactory
2021
from UnleashClient._instance_registry import _get_instance_registry
21-
from UnleashClient._metrics import _AsyncMetricsReporter
2222
from UnleashClient._payloads import _build_register_payload
2323
from UnleashClient.cache import BaseCache, FileCache
2424
from UnleashClient.clients.unleash_client import _RunState

‎tests/unit_tests/clients/test_async_unleash_client.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
)
1515
from tests.utilities.testing_constants import APP_NAME, URL
1616
from UnleashClient import INSTANCES, UnleashClient
17-
from UnleashClient._metrics import _AsyncMetricsReporter, _MetricsReporter
17+
from UnleashClient._async_metrics import _AsyncMetricsReporter
18+
from UnleashClient._metrics import _MetricsReporter
1819
from UnleashClient.cache import FileCache
1920
from UnleashClient.clients.async_unleash_client import AsyncUnleashClient
2021
from UnleashClient.constants import (

0 commit comments

Comments
 (0)