Skip to content

fix: don't reload unchanged feature state on every poll - #452

Closed
Elyahou wants to merge 1 commit into
Unleash:mainfrom
Elyahou:fix/skip-reload-unchanged-state
Closed

Elyahou wants to merge 1 commit into
Unleash:mainfrom
Elyahou:fix/skip-reload-unchanged-state

Conversation

@Elyahou

@Elyahou Elyahou commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

#449 (PyO3 engine) | Unleash/yggdrasil-bindings#171 (release the GIL while loading state)

Why the change

Every poll that brings nothing new — a 304, or a failed fetch — reloads the cached state into the engine, so the SDK parses the same payload again every refresh interval, and with the PyO3 engine that parse also blocks every other Python thread in the process while it runs.

Special things to note

  • A 304 alone isn't trusted to mean "nothing changed". When several processes share a cache, another one can write a newer body and ETag, and our next poll gets a 304 for it while our engine still holds the old state. So the store remembers the body the engine last loaded and skips the reload only when the cached body is identical to it.
  • A streamed update changes the engine without going through the cache, so it clears that memory and the next load from the cache always goes through. A body that failed to load isn't remembered either, so it's retried.
  • READY is still emitted on the skipped path, as it was before.

Change outline

apply_fetched(raw_state=None)        # 304 or failed fetch
  load_from_cache
    body = cache.get(FEATURES_URL)
+   if body == the body the engine last loaded
+     emit READY, return             # nothing to parse
    engine.take_state(body)
+   remember body

What a skipped reload saves, per poll, on a real 0.67 MB production payload with 257 toggles (macOS arm64, Python 3.12):

engine parse time longest pause of the main thread while it ran on the poll thread
yggdrasil-engine 1.3.1 16.1 ms 2.1 ms
yggdrasil-engine 3.0.0a1 11.8 ms 14.2 ms

Tests cover the four cases: an unchanged body is not reloaded, a body another process cached is loaded, a body that failed to load is retried, and a streamed update forces the next load. make lint, the unit tests and the client specification tests pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant