growthbook 2.1.1__tar.gz → 2.1.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.
- {growthbook-2.1.1/growthbook.egg-info → growthbook-2.1.2}/PKG-INFO +1 -1
- {growthbook-2.1.1 → growthbook-2.1.2}/growthbook/__init__.py +1 -1
- {growthbook-2.1.1 → growthbook-2.1.2}/growthbook/growthbook_client.py +2 -2
- {growthbook-2.1.1 → growthbook-2.1.2/growthbook.egg-info}/PKG-INFO +1 -1
- {growthbook-2.1.1 → growthbook-2.1.2}/setup.cfg +1 -1
- {growthbook-2.1.1 → growthbook-2.1.2}/tests/test_growthbook_client.py +79 -5
- {growthbook-2.1.1 → growthbook-2.1.2}/LICENSE +0 -0
- {growthbook-2.1.1 → growthbook-2.1.2}/MANIFEST.in +0 -0
- {growthbook-2.1.1 → growthbook-2.1.2}/README.md +0 -0
- {growthbook-2.1.1 → growthbook-2.1.2}/growthbook/common_types.py +0 -0
- {growthbook-2.1.1 → growthbook-2.1.2}/growthbook/core.py +0 -0
- {growthbook-2.1.1 → growthbook-2.1.2}/growthbook/growthbook.py +0 -0
- {growthbook-2.1.1 → growthbook-2.1.2}/growthbook/plugins/__init__.py +0 -0
- {growthbook-2.1.1 → growthbook-2.1.2}/growthbook/plugins/base.py +0 -0
- {growthbook-2.1.1 → growthbook-2.1.2}/growthbook/plugins/growthbook_tracking.py +0 -0
- {growthbook-2.1.1 → growthbook-2.1.2}/growthbook/plugins/request_context.py +0 -0
- {growthbook-2.1.1 → growthbook-2.1.2}/growthbook/py.typed +0 -0
- {growthbook-2.1.1 → growthbook-2.1.2}/growthbook.egg-info/SOURCES.txt +0 -0
- {growthbook-2.1.1 → growthbook-2.1.2}/growthbook.egg-info/dependency_links.txt +0 -0
- {growthbook-2.1.1 → growthbook-2.1.2}/growthbook.egg-info/requires.txt +0 -0
- {growthbook-2.1.1 → growthbook-2.1.2}/growthbook.egg-info/top_level.txt +0 -0
- {growthbook-2.1.1 → growthbook-2.1.2}/pyproject.toml +0 -0
- {growthbook-2.1.1 → growthbook-2.1.2}/setup.py +0 -0
- {growthbook-2.1.1 → growthbook-2.1.2}/tests/conftest.py +0 -0
- {growthbook-2.1.1 → growthbook-2.1.2}/tests/test_etag.py +0 -0
- {growthbook-2.1.1 → growthbook-2.1.2}/tests/test_growthbook.py +0 -0
- {growthbook-2.1.1 → growthbook-2.1.2}/tests/test_plugins.py +0 -0
|
@@ -90,8 +90,8 @@ class FeatureCache:
|
|
|
90
90
|
def update(self, features: Dict[str, Any], saved_groups: Dict[str, Any]) -> None:
|
|
91
91
|
"""Simple thread-safe update of cache with new API data"""
|
|
92
92
|
with self._lock:
|
|
93
|
-
self._cache['features']
|
|
94
|
-
self._cache['savedGroups']
|
|
93
|
+
self._cache['features'] = dict(features)
|
|
94
|
+
self._cache['savedGroups'] = dict(saved_groups)
|
|
95
95
|
|
|
96
96
|
def get_current_state(self) -> Dict[str, Any]:
|
|
97
97
|
"""Get current cache state"""
|
|
@@ -158,19 +158,93 @@ async def test_concurrent_feature_updates():
|
|
|
158
158
|
client_key="test_key"
|
|
159
159
|
)
|
|
160
160
|
features = {f"feature-{i}": {"defaultValue": i} for i in range(10)}
|
|
161
|
-
|
|
161
|
+
|
|
162
162
|
async def update_features(feature_subset):
|
|
163
163
|
await repo._handle_feature_update({"features": feature_subset, "savedGroups": {}})
|
|
164
|
-
|
|
164
|
+
|
|
165
165
|
await asyncio.gather(*[
|
|
166
|
-
update_features({k: features[k]})
|
|
166
|
+
update_features({k: features[k]})
|
|
167
167
|
for k in features
|
|
168
168
|
])
|
|
169
|
-
|
|
169
|
+
|
|
170
170
|
cache_state = repo._feature_cache.get_current_state()
|
|
171
171
|
# Verify all features were properly stored
|
|
172
|
-
assert cache_state["features"] ==
|
|
172
|
+
assert len(cache_state["features"]) == 1
|
|
173
173
|
assert cache_state["savedGroups"] == {}
|
|
174
|
+
feature_key = list(cache_state["features"].keys())[0]
|
|
175
|
+
assert feature_key in features
|
|
176
|
+
assert cache_state["features"][feature_key] == features[feature_key]
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@pytest.mark.asyncio
|
|
180
|
+
async def test_feature_cache_thread_safety(cache):
|
|
181
|
+
"""Verify FeatureCache is thread-safe during concurrent updates"""
|
|
182
|
+
repo = EnhancedFeatureRepository(
|
|
183
|
+
api_host="https://test.growthbook.io",
|
|
184
|
+
client_key="test_key"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
feature_sets = [
|
|
188
|
+
{f"set-{i}-feature-{j}": {"value": j} for j in range(3)}
|
|
189
|
+
for i in range(5)
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
async def update_full_set(feature_set):
|
|
193
|
+
await repo._handle_feature_update({
|
|
194
|
+
"features": feature_set,
|
|
195
|
+
"savedGroups": {}
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
# Concurrent updates
|
|
199
|
+
await asyncio.gather(*[update_full_set(fs) for fs in feature_sets])
|
|
200
|
+
|
|
201
|
+
cache = repo._feature_cache.get_current_state()
|
|
202
|
+
|
|
203
|
+
# One complete set should be in cache (race condition winner)
|
|
204
|
+
assert len(cache["features"]) == 3
|
|
205
|
+
cache_keys = set(cache["features"].keys())
|
|
206
|
+
assert any(cache_keys == set(fs.keys()) for fs in feature_sets)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@pytest.mark.asyncio
|
|
210
|
+
async def test_disabled_features_removed_from_cache(cache):
|
|
211
|
+
"""
|
|
212
|
+
Regression test: disabled features must be removed from cache.
|
|
213
|
+
|
|
214
|
+
Previously, FeatureCache.update() used dict.update() which only
|
|
215
|
+
adds/modifies entries but never removes them. This caused disabled
|
|
216
|
+
features to persist in the cache indefinitely.
|
|
217
|
+
"""
|
|
218
|
+
repo = EnhancedFeatureRepository(
|
|
219
|
+
api_host="https://test.growthbook.io",
|
|
220
|
+
client_key="test_key"
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Initial state: 2 features enabled
|
|
224
|
+
await repo._handle_feature_update({
|
|
225
|
+
"features": {
|
|
226
|
+
"feature-a": {"defaultValue": True},
|
|
227
|
+
"feature-b": {"defaultValue": False}
|
|
228
|
+
},
|
|
229
|
+
"savedGroups": {}
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
cache = repo._feature_cache.get_current_state()
|
|
233
|
+
assert "feature-a" in cache["features"]
|
|
234
|
+
assert "feature-b" in cache["features"]
|
|
235
|
+
|
|
236
|
+
# User disables feature-b in Growthbook UI
|
|
237
|
+
# API now returns only active features
|
|
238
|
+
await repo._handle_feature_update({
|
|
239
|
+
"features": {
|
|
240
|
+
"feature-a": {"defaultValue": True}
|
|
241
|
+
},
|
|
242
|
+
"savedGroups": {}
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
cache = repo._feature_cache.get_current_state()
|
|
246
|
+
assert "feature-a" in cache["features"]
|
|
247
|
+
assert "feature-b" not in cache["features"] # Must be removed!
|
|
174
248
|
|
|
175
249
|
@pytest.mark.asyncio
|
|
176
250
|
async def test_callback_thread_safety():
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|