Skip to content

Commit d281bf9

Browse files
committed
get e2e tests running
1 parent fd7ddf5 commit d281bf9

6 files changed

Lines changed: 86 additions & 9 deletions

File tree

poetry.lock

Lines changed: 45 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ python-dateutil = "^2.9.0"
6161
types-python-dateutil = "^2.9.0.20240316"
6262
ruff = "==0.11.5"
6363
aiohttp = {version = ">=3.9,<4", markers = "python_version >= \"3.9\""}
64+
redis = {version = ">=5.0,<6", markers = "python_version >= \"3.10\""}
6465

6566
[tool.pytest.ini_options]
6667
testpaths = [ "tests" ]

src/schematic/datastream/datastream_client.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -331,9 +331,12 @@ async def get_flag(self, flag_key: str) -> Optional[RulesengineFlag]:
331331
if raw is None:
332332
self._logger.debug("Flag cache miss for key: %s", cache_key)
333333
return None
334-
return _validate(RulesengineFlag, raw)
334+
self._logger.debug("Flag cache hit for key: %s, validating...", cache_key)
335+
result = _validate(RulesengineFlag, raw)
336+
self._logger.debug("Flag validated successfully: %s", cache_key)
337+
return result
335338
except Exception as exc:
336-
self._logger.warning("Failed to retrieve flag from cache: %s", exc)
339+
self._logger.warning("Failed to retrieve/validate flag from cache: %s (key=%s)", exc, cache_key)
337340
return None
338341

339342
async def get_all_flags(self) -> None:

src/schematic/datastream/rules_engine.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ def __init__(self, *, wasm_path: Optional[str] = None) -> None:
6262
self._get_result_json_fn: Any = None
6363
self._get_result_json_length_fn: Any = None
6464
self._get_version_key_fn: Any = None
65+
self._version_key: Optional[str] = None
6566

6667
async def initialize(self) -> None:
6768
"""Load and instantiate the WASM module. Safe to call multiple times."""
@@ -110,7 +111,10 @@ async def initialize(self) -> None:
110111
raise RuntimeError("WASM module does not export 'checkFlagCombined'")
111112

112113
self._initialized = True
113-
logger.debug("Rules engine WASM initialized (version: %s)", self.get_version_key())
114+
# Cache the version key immediately — the WASM pointer is only stable
115+
# before any other WASM calls mutate the linear memory.
116+
self._version_key = self._read_version_key_from_wasm()
117+
logger.debug("Rules engine WASM initialized (version: %s)", self._version_key)
114118

115119
def is_initialized(self) -> bool:
116120
return self._initialized
@@ -143,12 +147,15 @@ def get_version_key(self) -> str:
143147
"""Get the version key from the WASM rules engine.
144148
145149
Used for cache key generation to ensure cache invalidation on engine updates.
150+
The value is computed once during initialization and cached.
146151
"""
147152
self._ensure_initialized()
153+
return self._version_key or "1"
148154

155+
def _read_version_key_from_wasm(self) -> str:
156+
"""Read the version key from WASM. Called once during init."""
149157
if self._get_version_key_fn is None:
150158
return "1"
151-
152159
ptr = self._get_version_key_fn(self._store)
153160
return self._read_null_terminated_string(ptr)
154161

@@ -190,4 +197,4 @@ def _read_null_terminated_string(self, ptr: int, max_length: int = 256) -> str:
190197
null_idx = raw.find(0)
191198
if null_idx >= 0:
192199
raw = raw[:null_idx]
193-
return raw.decode("utf-8")
200+
return raw.decode("utf-8").strip()

src/schematic/datastream/websocket_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ async def _connect_and_read(self) -> None:
225225
)
226226
async with websockets.connect(
227227
self._url,
228-
additional_headers=self._headers,
228+
extra_headers=self._headers,
229229
open_timeout=CONNECTION_TIMEOUT,
230230
# Disable the library's built-in keepalive — we manage
231231
# ping/pong ourselves to match the Node SDK behaviour and

testapp/app.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
logger = logging.getLogger("testapp")
3333

3434
CACHE_TTL_MS = 2000 # Short TTL for E2E — tests sleep past this to verify cache expiration
35+
FLAG_CACHE_TTL_MS = 30 * 24 * 60 * 60 * 1000 # 30 days — flags use a long TTL, matching the Go SDK
3536

3637

3738
# ---------------------------------------------------------------------------
@@ -135,13 +136,35 @@ async def handle_configure(request: web.Request) -> web.Response:
135136
ds.company_lookup_cache = RedisCache(redis_client, default_ttl_ms=CACHE_TTL_MS)
136137
ds.user_cache = RedisCache(redis_client, default_ttl_ms=CACHE_TTL_MS)
137138
ds.user_lookup_cache = RedisCache(redis_client, default_ttl_ms=CACHE_TTL_MS)
138-
ds.flag_cache = RedisCache(redis_client, default_ttl_ms=CACHE_TTL_MS)
139+
ds.flag_cache = RedisCache(redis_client, default_ttl_ms=FLAG_CACHE_TTL_MS)
139140

140141
replicator_url = get_config_string("replicatorUrl")
141142
if replicator_url:
142143
ds.replicator_mode = True
143144
ds.replicator_health_url = replicator_url + "/ready"
144145

146+
# Verify the replicator is actually running before proceeding.
147+
# Without this, flag checks silently fall back to the API and
148+
# replicator mode is never truly exercised.
149+
import httpx
150+
151+
try:
152+
async with httpx.AsyncClient(timeout=5.0) as http:
153+
resp = await http.get(ds.replicator_health_url)
154+
resp.raise_for_status()
155+
health = resp.json()
156+
if not health.get("ready", False):
157+
return web.json_response(
158+
{"success": False, "error": f"Replicator is not ready: {health}"},
159+
status=503,
160+
)
161+
logger.info("Replicator is healthy: %s", health)
162+
except Exception as e:
163+
return web.json_response(
164+
{"success": False, "error": f"Replicator health check failed: {e}"},
165+
status=503,
166+
)
167+
145168
cfg.datastream = ds
146169

147170
client = AsyncSchematic(api_key, cfg)

0 commit comments

Comments
 (0)