Skip to content

Add lifecycle and concurrency tests for OptimizelyFeatureProvider#158

Merged
EtaCassiopeia merged 2 commits into
mainfrom
feat/optimizely-lifecycle-concurrency
May 13, 2026
Merged

Add lifecycle and concurrency tests for OptimizelyFeatureProvider#158
EtaCassiopeia merged 2 commits into
mainfrom
feat/optimizely-lifecycle-concurrency

Conversation

@EtaCassiopeia
Copy link
Copy Markdown
Owner

Hardens `OptimizelyFeatureProvider` against state-machine and concurrency edge cases, and ships a production fix the tests surfaced. WireMock-backed — runs on every PR, no Docker required.

Re-targets the same work that was previously merged to the `feat/optimizely-it` integration branch (#157) at `main` so the production fix and the on-every-PR tests land on the default branch independently of the docker-compose IT suite.

Production fix in `OptimizelyFeatureProvider.decide()`

The lifecycle spec exposed a real bug: calling `optimizely.isValid()` after `shutdown()` re-enters the polling HTTP client (closed by then), and Apache HttpClient throws an `IOException` that escapes `decide()` and surfaces to callers as a raw exception instead of a clean `PROVIDER_NOT_READY` error.

Fix: `decide()` now checks the provider's own `stateRef` first (no SDK call needed for NOT_READY / ERROR states) and wraps the defensive `isValid` probe in `Try` so any SDK-internal throw degrades to `PROVIDER_NOT_READY`:

```scala
if (stateRef.get() != ProviderState.READY) Left("PROVIDER_NOT_READY")
else if (transformed.userId.isEmpty) Left("TARGETING_KEY_MISSING")
else if (!Try(optimizely.isValid).getOrElse(false)) Left("PROVIDER_NOT_READY")
else ...
```

Check order also changed: provider state takes precedence over targeting-key validation, since a non-ready provider makes the caller's context irrelevant. Existing tests still pass — the targeting spec's provider is READY, so the new state check falls through to the targeting-key check unchanged.

`OptimizelyProviderLifecycleSpec` (8 tests)

  • `initialize` is idempotent — second call is a no-op; state stays READY; subsequent evaluation still returns the rolled-out value.
  • `shutdown` is idempotent — second call is a no-op; state remains NOT_READY.
  • `shutdown` before `initialize` — no exception; state stays NOT_READY.
  • Evaluation before `initialize` surfaces `PROVIDER_NOT_READY`.
  • Evaluation after `shutdown` surfaces `PROVIDER_NOT_READY` (the bug fixed above).
  • State transitions on the happy path: NOT_READY → READY → NOT_READY.
  • Failed init (404 datafile) transitions NOT_READY → ERROR; subsequent evaluation surfaces `PROVIDER_NOT_READY`.
  • Concurrent `initialize` from 16 threads — exactly one progresses the init, all return cleanly, state ends READY.

`OptimizelyProviderConcurrencySpec` (4 tests)

  • 1000 concurrent boolean evaluations after init — all return the same value, no exceptions.
  • 500 concurrent mixed-type evaluations (boolean / string / int / double / object) — interleave safely, no exceptions.
  • Evaluations racing `initialize` — each is either ready-with-the-rolled-out-value or `PROVIDER_NOT_READY`; nothing throws.
  • Evaluations racing `shutdown` — clean errors only; nothing throws; final state is NOT_READY.

Test fixture

`optimizely/src/test/resources/test-datafile-with-flag.json` — minimal Optimizely v4 datafile with one boolean flag rolled out 100%, used by both new specs (the existing `test-datafile-v1.json` is empty and can't exercise actual evaluation paths).

Verification

  • `sbt test` (root): 40 optimizely tests pass (12 new + 28 existing), plus 370 core + 30 OFREP tests unchanged.
  • New suite runtime: ~35 s for both specs combined (~5 s lifecycle, ~30 s concurrency — the racing-initialize test deliberately delays the WireMock response).

@EtaCassiopeia EtaCassiopeia merged commit 9cb9ab3 into main May 13, 2026
8 checks passed
@EtaCassiopeia EtaCassiopeia deleted the feat/optimizely-lifecycle-concurrency branch May 13, 2026 20:01
@EtaCassiopeia EtaCassiopeia mentioned this pull request May 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant