docs: add ADR 0009 for static-context cache-first local persistence#75
docs: add ADR 0009 for static-context cache-first local persistence#75jonathannorris wants to merge 28 commits intomainfrom
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces ADR 0009, which proposes that OFREP static-context providers persist their last successful bulk evaluation in local storage by default to enable cache-first initialization. This change aims to eliminate the 'flash-of-defaults' problem and improve offline resilience for web and mobile applications. The review feedback suggests including a version field in the storage schema for better maintainability, recommending the use of platform-specific secure storage to mitigate security risks, and establishing a default TTL for persisted entries to prevent the use of excessively stale data.
There was a problem hiding this comment.
Pull request overview
Adds a new Architecture Decision Record (ADR-0009) describing cache-first startup with local persistence for OFREP static-context providers, to preserve last-known flag evaluations across restarts/offline startup and align provider lifecycle events with OpenFeature expectations.
Changes:
- Introduces ADR 0009 defining persisted cache contents (payload, ETag, cache-key hash, write time).
- Documents cache-hit vs cache-miss initialization flows, including background refresh and provider lifecycle/event mapping.
- Adds implementation notes and open questions (multi-context caching, TTL).
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| 1. Should providers support caching evaluations for multiple targeting keys (like LaunchDarkly's `maxCachedContexts`), or only retain the most recent? Multi-context caching enables instant user switching on shared devices but increases storage usage. | ||
| 2. Should the storage key include a namespace to prevent collisions when multiple OFREP providers share the same local storage origin? | ||
| - **Answer:** Yes. Providers should support an optional `cacheKeyPrefix` configuration option. When provided, the cache key becomes `hash(cacheKeyPrefix + targetingKey)` instead of `hash(targetingKey)`. The prefix value is left to the application author (e.g., the OFREP base URL, a project or auth token, or any other distinguishing string). The default (no prefix) keeps the single-provider case simple. See the `cacheKeyPrefix` section in the Decision above. |
There was a problem hiding this comment.
There was a problem hiding this comment.
hash('foo' + 'bar') == hash('fo' + 'obar')This might lead to collisions, so introducing an additional character in between may help avoid them.
Co-authored-by: Jonathan Norris <jonathannorris@users.noreply.github.com> Signed-off-by: Norris <jonathan.norris@dynatrace.com> Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Co-authored-by: Jonathan Norris <jonathannorris@users.noreply.github.com> Signed-off-by: Norris <jonathan.norris@dynatrace.com> Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Clarify ADR 0009 with provider behavior, persistence examples, and implementation guidance for local cached bulk evaluations. Signed-off-by: Norris <jonathan.norris@dynatrace.com> Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Clarify initialization flow, explain the persisted timestamp, and define temporary server failures as eligible for persisted fallback. Signed-off-by: Norris <jonathan.norris@dynatrace.com> Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Specify the cacheKeyHash formula and restore explicit open questions for reviewer feedback. Signed-off-by: Norris <jonathan.norris@dynatrace.com> Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Document an explicit provider option for turning off persisted local storage. Signed-off-by: Norris <jonathan.norris@dynatrace.com> Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Drop authToken from cache key derivation and replace sha256 with generic hash(), per reviewer feedback. Signed-off-by: Norris <jonathan.norris@dynatrace.com> Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
…tion Specify CACHED as the evaluation reason when serving from persisted storage. Remove fallback scope open question since the decision section already addresses it. Signed-off-by: Norris <jonathan.norris@dynatrace.com> Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Use must not for auth/config error fallback to prevent masking real problems. Signed-off-by: Norris <jonathan.norris@dynatrace.com> Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Local storage availability is a platform constraint, not a consequence of the proposal. Signed-off-by: Norris <jonathan.norris@dynatrace.com> Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Specify that flag values are stored in plaintext and accessible to same-origin code or compromised devices. Signed-off-by: Norris <jonathan.norris@dynatrace.com> Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
The specific storage key and record model are implementation details. Signed-off-by: Norris <jonathan.norris@dynatrace.com> Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Remove redundant implementation notes that overlap with the decision section. Simplify mermaid diagram initialize call to use context. Signed-off-by: Norris <jonathan.norris@dynatrace.com> Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Replace fallback-on-failure with cache-first initialization pattern aligned with vendor SDKs (LaunchDarkly, Statsig, DevCycle, Eppo). Provider loads from persisted cache immediately on startup, refreshes from network in background, and emits PROVIDER_CONFIGURATION_CHANGED when fresh values arrive. Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Fix PROVIDER_FATAL to PROVIDER_ERROR with fatal error code per spec. Add rationale for READY vs STALE on cache-hit startup. Clarify cache key tradeoff (targetingKey vs full context). Note existing provider implementations will need lifecycle refactors. Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
On the cache-hit path, if the background refresh fails with 401/403/400, the provider continues serving cached values for the current session but clears the persisted entry. This ensures the next cold start uses the cache-miss path, making auth errors immediately visible. Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
…rt behavior Providers should cancel in-flight background refresh when onContextChanged() is called. Document that cache-first only applies after the first successful evaluation is persisted. Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
…e URL" This reverts commit b69eafd. Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
… for prefixing the cache key (#74) Signed-off-by: Jason Salaber <jason.salaber@dynatrace.com> Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Don't clear the persisted cache on 401/403/400 errors. The cache TTL is responsible for eventual expiry. This avoids degrading subsequent cold starts to defaults while auth errors are investigated. Aligns with vendor SDK behavior (DevCycle keeps cache through auth errors with a 30-day TTL). Moved TTL from open question to implementation recommendation. Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Move cacheKeyPrefix from open question to decision. Providers support an optional cacheKeyPrefix config option; when set, the cache key becomes hash(cacheKeyPrefix + targetingKey). Standardize terminology across the ADR. Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Add version field to persisted entry example JSON for schema versioning. Rename ADR file from camelCase to kebab-case to match convention. Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
8b7ee7b to
adac228
Compare
| - Populate the in-memory cache from the persisted entry immediately. | ||
| - Return from `initialize()` so the SDK can emit `PROVIDER_READY`. Evaluations served from the persisted entry should use `CACHED` as the evaluation reason. | ||
| - Attempt the `/ofrep/v1/evaluate/flags` request in the background. | ||
| - If the background request succeeds, update the in-memory cache from the response, update the persisted entry, and emit `PROVIDER_CONFIGURATION_CHANGED`. Evaluations should switch to the server-provided reasons. |
There was a problem hiding this comment.
update the persisted entry could fail for various reasons. It would be great to clarify the expected behavior in such cases so all implementations can follow a consistent pattern.
|
|
||
| 1. Should providers support caching evaluations for multiple targeting keys (like LaunchDarkly's `maxCachedContexts`), or only retain the most recent? Multi-context caching enables instant user switching on shared devices but increases storage usage. | ||
| 2. Should the storage key include a namespace to prevent collisions when multiple OFREP providers share the same local storage origin? | ||
| - **Answer:** Yes. Providers should support an optional `cacheKeyPrefix` configuration option. When provided, the cache key becomes `hash(cacheKeyPrefix + targetingKey)` instead of `hash(targetingKey)`. The prefix value is left to the application author (e.g., the OFREP base URL, a project or auth token, or any other distinguishing string). The default (no prefix) keeps the single-provider case simple. See the `cacheKeyPrefix` section in the Decision above. |
There was a problem hiding this comment.
hash('foo' + 'bar') == hash('fo' + 'obar')This might lead to collisions, so introducing an additional character in between may help avoid them.
Summary
PROVIDER_READY,PROVIDER_CONFIGURATION_CHANGED)hash(targetingKey), no auth token dependencyMotivation
Every major vendor SDK (LaunchDarkly, Statsig, DevCycle, Eppo) uses cache-first initialization by default. Current OFREP static-context providers keep their cache in memory only, losing all state on restart. See vendor mobile SDK caching research for a detailed comparison.
Related