feat: support isolated API instances#1928
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces support for isolated OpenFeature API instances by adding a createIsolated method to OpenFeatureAPI and a new OpenFeatureAPIFactory in a dedicated package. These changes allow for independent state management (providers, hooks, context) across different instances, satisfying specification requirements. Feedback includes a necessary fix for a compilation error in the tests due to unhandled exceptions, a recommendation to move the shared process-wide lock to an instance level for better performance isolation, and a concern regarding the completeness of global state restoration in the test cleanup logic.
518c67f to
dcf7a52
Compare
Signed-off-by: marcozabel <marco.zabel@dynatrace.com>
dcf7a52 to
e7b91f3
Compare
Signed-off-by: marcozabel <marco.zabel@dynatrace.com>
Signed-off-by: marcozabel <marco.zabel@dynatrace.com>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1928 +/- ##
============================================
+ Coverage 92.25% 93.22% +0.97%
- Complexity 653 662 +9
============================================
Files 59 59
Lines 1589 1610 +21
Branches 179 180 +1
============================================
+ Hits 1466 1501 +35
+ Misses 76 62 -14
Partials 47 47
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
| * @return a new API instance | ||
| * @see OpenFeatureAPI#createIsolated() | ||
| */ | ||
| public static OpenFeatureAPI createAPI() { |
There was a problem hiding this comment.
I don't fully see the point of this method/class. OpenFeatureAPI.createIsolated() has the exact same visibility level and does the same thing. I think this will only lead to confusion
| // relies on a ready event to be emitted | ||
| emitterExecutor.submit(() -> { | ||
| try (var ignored = OpenFeatureAPI.lock.readLockAutoCloseable()) { | ||
| try (var ignored = localLock != null ? localLock.readLockAutoCloseable() : null) { |
There was a problem hiding this comment.
Why is the lock optional? IIRC, we needed that lock to avoid race conditions when emitting events
946eb6d to
c24b936
Compare
Signed-off-by: marcozabel <marco.zabel@dynatrace.com>
Signed-off-by: marcozabel <marco.zabel@dynatrace.com>
…Isolated() Signed-off-by: marcozabel <marco.zabel@dynatrace.com>
Signed-off-by: marcozabel <marco.zabel@dynatrace.com>
c24b936 to
c06e500
Compare
| api1.setProviderAndWait(new NoOpProvider()); | ||
|
|
||
| assertThat(api1HandlerLatch.await(1, TimeUnit.SECONDS)).isTrue(); | ||
| assertThat(api2HandlerCalled.get()).isFalse(); |
There was a problem hiding this comment.
I am not sure this test really tests anything, the lambda for api2 might still get called some time after this tests finished. Unless we can read the queue of the emitterExecutor in the EventProvider, we cannot be sure that the lambda isn't scheduled for execution.
We could wait before this line for a few hundred ms. While this would slow down the test suite and still would not be a real proof, it should catch most errors
896fe71 to
013eb7d
Compare
…vider binding Signed-off-by: marcozabel <marco.zabel@dynatrace.com>
013eb7d to
0e53150
Compare
| * @see <a href="https://openfeature.dev/specification/sections/flag-evaluation#18-isolated-api-instances"> | ||
| * Spec §1.8 — Isolated API Instances</a> | ||
| */ | ||
| public final class OpenFeatureAPIFactory { |
There was a problem hiding this comment.
Section 1.8 is marked experimental in the spec. Might be worth adding an @apiNote to flag that, e.g.:
* @apiNote This API is experimental and subject to change.Could go on the class-level Javadoc or on createAPI() itself (or both).
| */ | ||
| @Test | ||
| @DisplayName("isolated instance does not interfere with singleton") | ||
| void isolatedInstanceDoesNotInterfereWithSingleton() { |
There was a problem hiding this comment.
This tests that mutating an isolated instance doesn't affect the singleton, which is great. Worth adding the reverse direction too; e.g. set a provider/hook/context on the singleton and assert a previously created isolated instance is unaffected.
Signed-off-by: marcozabel <marco.zabel@dynatrace.com>
Signed-off-by: marcozabel <marco.zabel@dynatrace.com>
Signed-off-by: marcozabel <marco.zabel@dynatrace.com>
Signed-off-by: marcozabel <marco.zabel@dynatrace.com>
Signed-off-by: marcozabel <marco.zabel@dynatrace.com>
Signed-off-by: marcozabel <marco.zabel@dynatrace.com>
|
| } else { | ||
| this.onEmit = onEmit; | ||
| } | ||
| this.attachment = new Attachment(onEmit, lock); |
There was a problem hiding this comment.
this.attachment is not guaranteed to be null here any more. If we want to ensure that, we need an AtomicReference and a cas operation
There was a problem hiding this comment.
+1 to this; we flagged the same race in our earlier review pass. volatile gives visibility but not atomicity. An AtomicReference with compareAndSet would be better I think, or just guarding attach() with a synchronized block since it's not on a hot path.
| * <p><strong>Note:</strong> Isolated API instances (per spec section 1.8) are experimental and | ||
| * subject to change. | ||
| */ | ||
| public OpenFeatureAPI() { |
There was a problem hiding this comment.
I see the OpenFeatureAPIFactory was removed in favor of a public constructor; can you share the reasoning? The factory-in-a-distinct-package approach felt closer to spec 1.8.3 (factory function in a distinct module/package/namespace for discoverability), and intentionally less discoverable than the singleton.
IIUC, a public constructor on OpenFeatureAPI itself sits right next to getInstance() in IntelliSense, which works against the "intentionally less discoverable" goal. Was there a specific issue with the factory class that prompted the removal? Happy to be convinced otherwise.



This PR
This PR introduces support for creating isolated OpenFeature API instances, each with their own providers, hooks, context, and event handling - enabling multi-tenant or side-by-side usage without shared global state.
Related Issues
Fixes #1925
Notes
Follow-up Tasks
How to test