Skip to content

Commit d3cb296

Browse files
committed
Address review feedback from cursor
1 parent cc6e742 commit d3cb296

File tree

3 files changed

+357
-6
lines changed

3 files changed

+357
-6
lines changed

ldclient/impl/datasourcev2/streaming.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ def sync(self, ss: SelectorStore) -> Generator[Update, None, None]:
147147
self._running = True
148148
self._connection_attempt_start_time = time()
149149

150+
envid = None
150151
for action in self._sse.all:
151152
if isinstance(action, Fault):
152153
# If the SSE client detects the stream has closed, then it will
@@ -165,7 +166,6 @@ def sync(self, ss: SelectorStore) -> Generator[Update, None, None]:
165166
break
166167
continue
167168

168-
envid = None
169169
if isinstance(action, Start) and action.headers is not None:
170170
fallback = action.headers.get(_LD_FD_FALLBACK_HEADER) == 'true'
171171
envid = action.headers.get(_LD_ENVID_HEADER)

ldclient/testing/impl/datasourcev2/test_polling_synchronizer.py

Lines changed: 167 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
Selector,
2121
ServerIntent
2222
)
23-
from ldclient.impl.util import UnsuccessfulResponseException, _Fail, _Success
23+
from ldclient.impl.util import UnsuccessfulResponseException, _Fail, _Success, _LD_ENVID_HEADER, _LD_FD_FALLBACK_HEADER
2424
from ldclient.interfaces import DataSourceErrorKind, DataSourceState
2525
from ldclient.testing.mock_components import MockSelectorStore
2626

@@ -304,3 +304,169 @@ def test_unrecoverable_error_shuts_down():
304304
assert False, "Expected StopIteration"
305305
except StopIteration:
306306
pass
307+
308+
309+
def test_envid_from_success_headers():
310+
"""Test that environment ID is captured from successful polling response headers"""
311+
change_set = ChangeSetBuilder.no_changes()
312+
headers = {_LD_ENVID_HEADER: 'test-env-polling-123'}
313+
polling_result: PollingResult = _Success(value=(change_set, headers))
314+
315+
synchronizer = PollingDataSource(
316+
poll_interval=0.01, requester=ListBasedRequester(results=iter([polling_result]))
317+
)
318+
319+
valid = next(synchronizer.sync(MockSelectorStore(Selector.no_selector())))
320+
321+
assert valid.state == DataSourceState.VALID
322+
assert valid.error is None
323+
assert valid.revert_to_fdv1 is False
324+
assert valid.environment_id == 'test-env-polling-123'
325+
326+
327+
def test_envid_from_success_with_changeset():
328+
"""Test that environment ID is captured from polling response with actual changes"""
329+
builder = ChangeSetBuilder()
330+
builder.start(intent=IntentCode.TRANSFER_FULL)
331+
builder.add_put(
332+
version=100, kind=ObjectKind.FLAG, key="flag-key", obj={"key": "flag-key"}
333+
)
334+
change_set = builder.finish(selector=Selector(state="p:SOMETHING:300", version=300))
335+
headers = {_LD_ENVID_HEADER: 'test-env-456'}
336+
polling_result: PollingResult = _Success(value=(change_set, headers))
337+
338+
synchronizer = PollingDataSource(
339+
poll_interval=0.01, requester=ListBasedRequester(results=iter([polling_result]))
340+
)
341+
valid = next(synchronizer.sync(MockSelectorStore(Selector.no_selector())))
342+
343+
assert valid.state == DataSourceState.VALID
344+
assert valid.environment_id == 'test-env-456'
345+
assert valid.change_set is not None
346+
assert len(valid.change_set.changes) == 1
347+
348+
349+
def test_envid_from_fallback_headers():
350+
"""Test that environment ID is captured when fallback header is present on success"""
351+
change_set = ChangeSetBuilder.no_changes()
352+
headers = {
353+
_LD_ENVID_HEADER: 'test-env-fallback',
354+
_LD_FD_FALLBACK_HEADER: 'true'
355+
}
356+
polling_result: PollingResult = _Success(value=(change_set, headers))
357+
358+
synchronizer = PollingDataSource(
359+
poll_interval=0.01, requester=ListBasedRequester(results=iter([polling_result]))
360+
)
361+
362+
valid = next(synchronizer.sync(MockSelectorStore(Selector.no_selector())))
363+
364+
assert valid.state == DataSourceState.VALID
365+
assert valid.revert_to_fdv1 is True
366+
assert valid.environment_id == 'test-env-fallback'
367+
368+
369+
def test_envid_from_error_headers_recoverable():
370+
"""Test that environment ID is captured from error response headers for recoverable errors"""
371+
builder = ChangeSetBuilder()
372+
builder.start(intent=IntentCode.TRANSFER_FULL)
373+
builder.add_delete(version=101, kind=ObjectKind.FLAG, key="flag-key")
374+
change_set = builder.finish(selector=Selector(state="p:SOMETHING:300", version=300))
375+
headers_success = {_LD_ENVID_HEADER: 'test-env-success'}
376+
polling_result: PollingResult = _Success(value=(change_set, headers_success))
377+
378+
headers_error = {_LD_ENVID_HEADER: 'test-env-408'}
379+
_failure = _Fail(
380+
error="error for test",
381+
exception=UnsuccessfulResponseException(status=408),
382+
headers=headers_error
383+
)
384+
385+
synchronizer = PollingDataSource(
386+
poll_interval=0.01,
387+
requester=ListBasedRequester(results=iter([_failure, polling_result])),
388+
)
389+
sync = synchronizer.sync(MockSelectorStore(Selector.no_selector()))
390+
interrupted = next(sync)
391+
valid = next(sync)
392+
393+
assert interrupted.state == DataSourceState.INTERRUPTED
394+
assert interrupted.environment_id == 'test-env-408'
395+
assert interrupted.error is not None
396+
assert interrupted.error.status_code == 408
397+
398+
assert valid.state == DataSourceState.VALID
399+
assert valid.environment_id == 'test-env-success'
400+
401+
402+
def test_envid_from_error_headers_unrecoverable():
403+
"""Test that environment ID is captured from error response headers for unrecoverable errors"""
404+
headers_error = {_LD_ENVID_HEADER: 'test-env-401'}
405+
_failure = _Fail(
406+
error="error for test",
407+
exception=UnsuccessfulResponseException(status=401),
408+
headers=headers_error
409+
)
410+
411+
synchronizer = PollingDataSource(
412+
poll_interval=0.01,
413+
requester=ListBasedRequester(results=iter([_failure])),
414+
)
415+
sync = synchronizer.sync(MockSelectorStore(Selector.no_selector()))
416+
off = next(sync)
417+
418+
assert off.state == DataSourceState.OFF
419+
assert off.environment_id == 'test-env-401'
420+
assert off.error is not None
421+
assert off.error.status_code == 401
422+
423+
424+
def test_envid_from_error_with_fallback():
425+
"""Test that environment ID and fallback are captured from error response"""
426+
headers_error = {
427+
_LD_ENVID_HEADER: 'test-env-503',
428+
_LD_FD_FALLBACK_HEADER: 'true'
429+
}
430+
_failure = _Fail(
431+
error="error for test",
432+
exception=UnsuccessfulResponseException(status=503),
433+
headers=headers_error
434+
)
435+
436+
synchronizer = PollingDataSource(
437+
poll_interval=0.01,
438+
requester=ListBasedRequester(results=iter([_failure])),
439+
)
440+
sync = synchronizer.sync(MockSelectorStore(Selector.no_selector()))
441+
off = next(sync)
442+
443+
assert off.state == DataSourceState.OFF
444+
assert off.revert_to_fdv1 is True
445+
assert off.environment_id == 'test-env-503'
446+
447+
448+
def test_envid_from_generic_error_with_headers():
449+
"""Test that environment ID is captured from generic errors with headers"""
450+
builder = ChangeSetBuilder()
451+
builder.start(intent=IntentCode.TRANSFER_FULL)
452+
change_set = builder.finish(selector=Selector(state="p:SOMETHING:300", version=300))
453+
headers_success = {}
454+
polling_result: PollingResult = _Success(value=(change_set, headers_success))
455+
456+
headers_error = {_LD_ENVID_HEADER: 'test-env-generic'}
457+
_failure = _Fail(error="generic error for test", headers=headers_error)
458+
459+
synchronizer = PollingDataSource(
460+
poll_interval=0.01,
461+
requester=ListBasedRequester(results=iter([_failure, polling_result])),
462+
)
463+
sync = synchronizer.sync(MockSelectorStore(Selector.no_selector()))
464+
interrupted = next(sync)
465+
valid = next(sync)
466+
467+
assert interrupted.state == DataSourceState.INTERRUPTED
468+
assert interrupted.environment_id == 'test-env-generic'
469+
assert interrupted.error is not None
470+
assert interrupted.error.kind == DataSourceErrorKind.NETWORK_ERROR
471+
472+
assert valid.state == DataSourceState.VALID

0 commit comments

Comments
 (0)