Skip to content

Commit 5c91462

Browse files
authored
Better handle initialization if cache not present and Unleash server not accessible (#30)
* Add logging documentation. * Better handles cases where cache is not present and Unleash server is not accessible during startup. * Update changelog. * Fix flake8 issues. * Make integration scrip generic again. * Make integration scrip generic again.
1 parent 5fec9d3 commit 5c91462

File tree

6 files changed

+149
-40
lines changed

6 files changed

+149
-40
lines changed

UnleashClient/loader.py

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -45,32 +45,36 @@ def load_features(cache: FileCache,
4545
:return:
4646
"""
4747
# Pull raw provisioning from cache.
48-
feature_provisioning = cache[FEATURES_URL]
49-
50-
# Parse provisioning
51-
parsed_features = {}
52-
feature_names = [d["name"] for d in feature_provisioning["features"]]
53-
54-
for provisioning in feature_provisioning["features"]:
55-
parsed_features[provisioning["name"]] = provisioning
56-
57-
# Delete old features/cache
58-
for feature in list(feature_toggles.keys()):
59-
if feature not in feature_names:
60-
del feature_toggles[feature]
61-
62-
# Update existing objects
63-
for feature in feature_toggles.keys():
64-
feature_for_update = feature_toggles[feature]
65-
strategies = parsed_features[feature]["strategies"]
66-
67-
feature_for_update.enabled = parsed_features[feature]["enabled"]
68-
if strategies:
69-
parsed_strategies = _create_strategies(parsed_features[feature], strategy_mapping)
70-
feature_for_update.strategies = parsed_strategies
71-
72-
# Handle creation or deletions
73-
new_features = list(set(feature_names) - set(feature_toggles.keys()))
74-
75-
for feature in new_features:
76-
feature_toggles[feature] = _create_feature(parsed_features[feature], strategy_mapping)
48+
try:
49+
feature_provisioning = cache[FEATURES_URL]
50+
51+
# Parse provisioning
52+
parsed_features = {}
53+
feature_names = [d["name"] for d in feature_provisioning["features"]]
54+
55+
for provisioning in feature_provisioning["features"]:
56+
parsed_features[provisioning["name"]] = provisioning
57+
58+
# Delete old features/cache
59+
for feature in list(feature_toggles.keys()):
60+
if feature not in feature_names:
61+
del feature_toggles[feature]
62+
63+
# Update existing objects
64+
for feature in feature_toggles.keys():
65+
feature_for_update = feature_toggles[feature]
66+
strategies = parsed_features[feature]["strategies"]
67+
68+
feature_for_update.enabled = parsed_features[feature]["enabled"]
69+
if strategies:
70+
parsed_strategies = _create_strategies(parsed_features[feature], strategy_mapping)
71+
feature_for_update.strategies = parsed_strategies
72+
73+
# Handle creation or deletions
74+
new_features = list(set(feature_names) - set(feature_toggles.keys()))
75+
76+
for feature in new_features:
77+
feature_toggles[feature] = _create_feature(parsed_features[feature], strategy_mapping)
78+
except KeyError as cache_exception:
79+
LOGGER.warning("Cache Exception: %s", cache_exception)
80+
LOGGER.warning("Unleash client does not have cached features. Please make sure client can communicate with Unleash server!")

UnleashClient/periodic_tasks/fetch_and_load.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@ def fetch_and_load_features(url: str,
1818
cache[FEATURES_URL] = feature_provisioning
1919
cache.sync()
2020
else:
21-
LOGGER.info("Unable to get feature flag toggles, using cached values.")
21+
LOGGER.warning("Unable to get feature flag toggles, using cached provisioning.")
2222

2323
load_features(cache, features, strategy_mapping)

docs/changelog.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## Next
2+
3+
**General**
4+
* (Minor) Unleash client will not error if cache is not present and Unleash server not accessible during initialization.
5+
16
## v2.4.0
27

38
**General**

docs/index.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,25 @@ Supplying application context:
4040
app_context = {"userId": "test@email.com"}
4141
client.is_enabled("User ID Toggle", app_context)
4242
```
43+
44+
## Logging
45+
46+
Unleash Client uses the built-in logging facility to show information about errors, background jobs (feature-flag updates and metrics), et cetera.
47+
48+
It's highly recommended that users implement
49+
50+
To see what's going on when PoCing code, you can use the following:
51+
```python
52+
import logging
53+
import sys
54+
55+
root = logging.getLogger()
56+
root.setLevel(logging.INFO)
57+
58+
handler = logging.StreamHandler(sys.stdout)
59+
handler.setLevel(logging.DEBUG)
60+
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
61+
handler.setFormatter(formatter)
62+
root.addHandler(handler)
63+
64+
```

tests/integration_tests/integration.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
root.addHandler(handler)
1616
# ---
1717

18-
1918
my_client = UnleashClient(
2019
url="http://localhost:4242/api",
2120
app_name="pyIvan"

tests/unit_tests/test_client.py

Lines changed: 88 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,20 +30,41 @@ def __call__(self, context: dict = None) -> bool:
3030

3131

3232
@pytest.fixture()
33-
def unleash_client():
34-
unleash_client = UnleashClient(URL, APP_NAME, refresh_interval=REFRESH_INTERVAL, metrics_interval=METRICS_INTERVAL)
33+
def unleash_client(tmpdir):
34+
unleash_client = UnleashClient(
35+
URL,
36+
APP_NAME,
37+
refresh_interval=REFRESH_INTERVAL,
38+
metrics_interval=METRICS_INTERVAL,
39+
cache_directory=tmpdir.dirname
40+
)
3541
yield unleash_client
3642
unleash_client.destroy()
3743

3844

3945
@pytest.fixture()
40-
def unleash_client_toggle_only():
41-
unleash_client = UnleashClient(URL,
42-
APP_NAME,
43-
refresh_interval=REFRESH_INTERVAL,
44-
metrics_interval=METRICS_INTERVAL,
45-
disable_registration=True,
46-
disable_metrics=True)
46+
def unleash_client_nodestroy(tmpdir):
47+
unleash_client = UnleashClient(
48+
URL,
49+
APP_NAME,
50+
refresh_interval=REFRESH_INTERVAL,
51+
metrics_interval=METRICS_INTERVAL,
52+
cache_directory=tmpdir.dirname
53+
)
54+
yield unleash_client
55+
56+
57+
@pytest.fixture()
58+
def unleash_client_toggle_only(tmpdir):
59+
unleash_client = UnleashClient(
60+
URL,
61+
APP_NAME,
62+
refresh_interval=REFRESH_INTERVAL,
63+
metrics_interval=METRICS_INTERVAL,
64+
disable_registration=True,
65+
disable_metrics=True,
66+
cache_directory=str(tmpdir)
67+
)
4768
yield unleash_client
4869
unleash_client.destroy()
4970

@@ -112,6 +133,26 @@ def test_uc_is_enabled(unleash_client):
112133
assert unleash_client.is_enabled("testFlag")
113134

114135

136+
@responses.activate
137+
def test_uc_dirty_cache(unleash_client_nodestroy):
138+
unleash_client = unleash_client_nodestroy
139+
# Set up API
140+
responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202)
141+
responses.add(responses.GET, URL + FEATURES_URL, json=MOCK_FEATURE_RESPONSE, status=200)
142+
responses.add(responses.POST, URL + METRICS_URL, json={}, status=202)
143+
144+
# Create Unleash client and check initial load
145+
unleash_client.initialize_client()
146+
time.sleep(5)
147+
assert unleash_client.is_enabled("testFlag")
148+
unleash_client.scheduler.shutdown()
149+
150+
# Check that everything works if previous cache exists.
151+
unleash_client.initialize_client()
152+
time.sleep(5)
153+
assert unleash_client.is_enabled("testFlag")
154+
155+
115156
@responses.activate
116157
def test_uc_is_enabled_with_context():
117158
# Set up API
@@ -185,3 +226,41 @@ def test_uc_disabled_registration(unleash_client_toggle_only):
185226

186227
for api_call in responses.calls:
187228
assert '/api/client/features' in api_call.request.url
229+
230+
231+
@responses.activate
232+
def test_uc_server_error(unleash_client):
233+
# Verify that Unleash Client will still fall back gracefully if SERVER ANGRY RAWR, and then recover gracefully.
234+
235+
unleash_client = unleash_client
236+
# Set up APIs
237+
responses.add(responses.POST, URL + REGISTER_URL, json={}, status=401)
238+
responses.add(responses.GET, URL + FEATURES_URL, status=500)
239+
responses.add(responses.POST, URL + METRICS_URL, json={}, status=401)
240+
241+
unleash_client.initialize_client()
242+
assert not unleash_client.is_enabled("testFlag")
243+
244+
responses.remove(responses.GET, URL + FEATURES_URL)
245+
responses.add(responses.GET, URL + FEATURES_URL, json=MOCK_FEATURE_RESPONSE, status=200)
246+
time.sleep(20)
247+
assert unleash_client.is_enabled("testFlag")
248+
249+
250+
@responses.activate
251+
def test_uc_server_error_recovery(unleash_client):
252+
# Verify that Unleash Client will still fall back gracefully if SERVER ANGRY RAWR, and then recover gracefully.
253+
254+
unleash_client = unleash_client
255+
# Set up APIs
256+
responses.add(responses.POST, URL + REGISTER_URL, json={}, status=401)
257+
responses.add(responses.GET, URL + FEATURES_URL, status=500)
258+
responses.add(responses.POST, URL + METRICS_URL, json={}, status=401)
259+
260+
unleash_client.initialize_client()
261+
assert not unleash_client.is_enabled("testFlag")
262+
263+
responses.remove(responses.GET, URL + FEATURES_URL)
264+
responses.add(responses.GET, URL + FEATURES_URL, json=MOCK_FEATURE_RESPONSE, status=200)
265+
time.sleep(20)
266+
assert unleash_client.is_enabled("testFlag")

0 commit comments

Comments
 (0)