Skip to content

Commit 3700d1d

Browse files
authored
feat: adding file data source as an intializer (#381)
**Requirements** - [x] I have added test coverage for new or changed functionality - [x] I have followed the repository's [pull request submission guidelines](../blob/main/CONTRIBUTING.md#submitting-pull-requests) - [x] I have validated my changes against all supported platform versions This PR will: 1. add the file data source to the custom fdv2 datasystem configuration 2. modify the initialization logic so it aligns with spec. Namely, we will continue through the initializer list until we either run out of intializers (in which case the synchronizers will try to do a valid initialization) OR if an intializer fetched a proper basis with a selector. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Introduces a file-based initializer and updates FDv2 to mark ready only when an initializer provides a selector, with tests validating initializer ordering and early completion. > > - **Core**: > - Add `file_ds_builder(paths)` returning `_FileDataSourceV2` initializer in `ldclient/datasystem.py`; import `_FileDataSourceV2`. > - **FDv2 Initialization Logic** (`ldclient/impl/datasystem/fdv2.py`): > - After applying an initializer basis, set ready only if `basis.change_set.selector` is defined; then return to proceed to synchronizers. > - **Tests** (`ldclient/testing/impl/datasystem/test_fdv2_datasystem.py`): > - Add tests ensuring initializers run until first success and stop on first initializer that provides a selector; verify transition to synchronizers. > - Include file-based data source usage via `file_ds_builder` with temp JSON files. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 04a2c53. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
2 parents 7b1a1c3 + 04a2c53 commit 3700d1d

File tree

3 files changed

+126
-2
lines changed

3 files changed

+126
-2
lines changed

ldclient/datasystem.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
StreamingDataSource,
1717
StreamingDataSourceBuilder
1818
)
19+
from ldclient.impl.integrations.files.file_data_sourcev2 import (
20+
_FileDataSourceV2
21+
)
1922
from ldclient.interfaces import (
2023
DataStoreMode,
2124
FeatureStore,
@@ -125,6 +128,13 @@ def builder(config: LDConfig) -> StreamingDataSource:
125128
return builder
126129

127130

131+
def file_ds_builder(paths: List[str]) -> Builder[Initializer]:
132+
def builder(_: LDConfig) -> Initializer:
133+
return _FileDataSourceV2(paths)
134+
135+
return builder
136+
137+
128138
def default() -> ConfigBuilder:
129139
"""
130140
Default is LaunchDarkly's recommended flag data acquisition strategy.

ldclient/impl/datasystem/fdv2.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -409,9 +409,10 @@ def _run_initializers(self, set_on_ready: Event):
409409
# Apply the basis to the store
410410
self._store.apply(basis.change_set, basis.persist)
411411

412-
# Set ready event
413-
if not set_on_ready.is_set():
412+
# Set ready event if an only if a selector is defined for the changeset
413+
if basis.change_set.selector is not None and basis.change_set.selector.is_defined():
414414
set_on_ready.set()
415+
return
415416
except Exception as e:
416417
log.error("Initializer failed with exception: %s", e)
417418

ldclient/testing/impl/datasystem/test_fdv2_datasystem.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
# pylint: disable=missing-docstring
22

3+
import os
4+
import tempfile
35
from threading import Event
46
from typing import List
57

68
from mock import Mock
79

810
from ldclient.config import Config, DataSystemConfig
11+
from ldclient.datasystem import file_ds_builder
912
from ldclient.impl.datasystem import DataAvailability
1013
from ldclient.impl.datasystem.fdv2 import FDv2
1114
from ldclient.integrations.test_datav2 import TestDataV2
@@ -432,3 +435,113 @@ def test_fdv2_stays_on_fdv1_after_fallback():
432435
store = fdv2.store
433436
flag = store.get(FEATURES, "fdv1-flag", lambda x: x)
434437
assert flag is not None
438+
439+
440+
def test_fdv2_initializer_should_run_until_success():
441+
"""
442+
Test that FDv2 initializers will run in order until a successful run. Then
443+
the datasystem is expected to transition to run synchronizers.
444+
"""
445+
initial_flag_data = '''
446+
{
447+
"flags": {
448+
"feature-flag": {
449+
"key": "feature-flag",
450+
"version": 0,
451+
"on": false,
452+
"fallthrough": {
453+
"variation": 0
454+
},
455+
"variations": ["off", "on"]
456+
}
457+
}
458+
}
459+
'''
460+
f, path = tempfile.mkstemp(suffix='.json')
461+
try:
462+
os.write(f, initial_flag_data.encode("utf-8"))
463+
os.close(f)
464+
465+
td_initializer = TestDataV2.data_source()
466+
td_initializer.update(td_initializer.flag("feature-flag").on(True))
467+
468+
# We actually do not care what this synchronizer does.
469+
td_synchronizer = TestDataV2.data_source()
470+
471+
data_system_config = DataSystemConfig(
472+
initializers=[file_ds_builder([path]), td_initializer.build_initializer],
473+
primary_synchronizer=td_synchronizer.build_synchronizer,
474+
)
475+
476+
set_on_ready = Event()
477+
synchronizer_ran = Event()
478+
fdv2 = FDv2(Config(sdk_key="dummy"), data_system_config)
479+
count = 0
480+
481+
def listener(_: FlagChange):
482+
nonlocal count
483+
count += 1
484+
if count == 3:
485+
synchronizer_ran.set()
486+
487+
fdv2.flag_tracker.add_listener(listener)
488+
489+
fdv2.start(set_on_ready)
490+
assert set_on_ready.wait(1), "Data system did not become ready in time"
491+
assert synchronizer_ran.wait(1), "Data system did not transition to synchronizer"
492+
finally:
493+
os.remove(path)
494+
495+
496+
def test_fdv2_should_finish_initialization_on_first_successful_initializer():
497+
"""
498+
Test that when a FDv2 initializer returns a basis and selector that the rest
499+
of the intializers will be skipped and the client starts synchronizing phase.
500+
"""
501+
initial_flag_data = '''
502+
{
503+
"flags": {
504+
"feature-flag": {
505+
"key": "feature-flag",
506+
"version": 0,
507+
"on": false,
508+
"fallthrough": {
509+
"variation": 0
510+
},
511+
"variations": ["off", "on"]
512+
}
513+
}
514+
}
515+
'''
516+
f, path = tempfile.mkstemp(suffix='.json')
517+
try:
518+
os.write(f, initial_flag_data.encode("utf-8"))
519+
os.close(f)
520+
521+
td_initializer = TestDataV2.data_source()
522+
td_initializer.update(td_initializer.flag("feature-flag").on(True))
523+
524+
# We actually do not care what this synchronizer does.
525+
td_synchronizer = TestDataV2.data_source()
526+
527+
data_system_config = DataSystemConfig(
528+
initializers=[td_initializer.build_initializer, file_ds_builder([path])],
529+
primary_synchronizer=None,
530+
)
531+
532+
set_on_ready = Event()
533+
fdv2 = FDv2(Config(sdk_key="dummy"), data_system_config)
534+
count = 0
535+
536+
def listener(_: FlagChange):
537+
nonlocal count
538+
count += 1
539+
540+
fdv2.flag_tracker.add_listener(listener)
541+
542+
fdv2.start(set_on_ready)
543+
assert set_on_ready.wait(1), "Data system did not become ready in time"
544+
assert count == 1, "Invalid initializer process"
545+
fdv2.stop()
546+
finally:
547+
os.remove(path)

0 commit comments

Comments
 (0)