feat: add GCP Secret Manager OpenFeature provider#1772
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a new OpenFeature provider for Google Cloud Secret Manager, enabling feature flags to be managed as GCP secrets. The changes include the core provider implementation with caching and value conversion, along with comprehensive unit and integration tests, and a sample application. Feedback suggests adding an initial entry to the new module's CHANGELOG.md, refining the spotbugs-exclusions.xml to only include relevant exclusions, and improving the testability of FlagCache by injecting a Clock instance instead of relying on Instant.now() and Thread.sleep().
1c0fd8d to
0d10a01
Compare
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces a new OpenFeature provider for Google Cloud Secret Manager, enabling feature flags to be stored and managed as secrets. The implementation includes a TTL-based in-memory cache to optimize API usage and a converter to handle various OpenFeature data types. Feedback identifies several improvement opportunities, including fixing duplicate entries in the changelog, addressing a race condition and a potential 'thundering herd' issue in the caching logic, and strengthening input validation for the secret version configuration.
| // Thread A: re-insert fresh value for the same key | ||
| () -> sharedCache.put("key", "new-value"), | ||
| // Thread B: get() triggers expiry-removal of the old entry | ||
| () -> sharedCache.get("key")); |
There was a problem hiding this comment.
I believe this should either return nothing or the new value, and we should assert that
There was a problem hiding this comment.
This may be an outdated comment now based on new quote. Thread A's value should not have expired regardless of the order so it should stay as get-check-remove now synchronized.
|
The build pipeline currently fails for two reasons: |
bcefaa7 to
6af66e7
Compare
Signed off commits to fix DCO |
74e41c0 to
1d65ba5
Compare
|
I hope all comments have been addressed now. Please let me know if there's anything outstanding. |
| } | ||
| }; | ||
|
|
||
| FlagCache cache = new FlagCache(Duration.ofSeconds(30), 100, clock); |
There was a problem hiding this comment.
Why do we create a cache, clock and now when all of that is already done in the setup method?
|
Good catch — removed the duplicate |
d01ff47 to
ea481a2
Compare
|
Re: #1771 (cc @aepfli @chrfwow @mahpatil) Looking at #1771 + #1772 side by side, I think there's been a small misunderstanding of @aepfli's earlier feedback. IIUC he asked for the PR to be split for review hygiene ("a pull request is small, and provides just one additional feature"), not necessarily for two separate Maven modules. The split-PRs ask has been honoured; the split-modules part was an additional interpretation. @chrfwow already raised the same question on #1771. Posting here too since this is where active review is happening, and IMO it's much cheaper to settle before #1772 merges than after. Let's keep 2 PRs but land them in one module.
@mahpatil - to keep things reviewable, suggest stacking the branches: rework this PR (#1772) to add a single Suggested module name: @aepfli - was your earlier ask only about the PR split, or did you also intend two artifacts? Happy to discuss if there's a packaging reason I'm missing. |
This is a bit frustrating as I have been receiving feedback on the PR and now requested to refactor again. @aepfli please finalize with @chrfwow what you would want, I don't want to be keep coming back and incorporating comments, wasting everyones time. I feel the process very slow even though we are using gemini and im using claude, this PR has been going back and forth for a while. |
|
@mahpatil first of all, I want to apologise. Reading back through the thread, I can absolutely see how Let me try to clear up the misunderstanding, because I think @toddbaert read my intent correctly. When I The technique that makes "split the PR" and "share the code" coexist is what is usually called stacked Concretely, for your case I think Todd's suggestion is exactly right: rework #1772 to add a single Again, I am sorry for the back-and-forth — your contribution is genuinely appreciated and we want to |
6078c87 to
a5b85cd
Compare
Signed-off-by: Ayushman Gaur <ayushmangaur2017@gmail.com> Signed-off-by: Mahesh Patil <maheshfinity@gmail.com>
…pen-feature#1780) Signed-off-by: OpenFeature Bot <109696520+openfeaturebot@users.noreply.github.com> Signed-off-by: Mahesh Patil <maheshfinity@gmail.com>
- TTL-based in-memory FlagCache with Clock injection for deterministic testing - Thread-safe cache: synchronized get/remove block eliminates check-then-act race - Double-checked locking in fetchWithCache prevents thundering herd on GCP API - FlagValueConverter uses Boolean.parseBoolean for boolean flag values - secretVersion validation added to GcpSecretManagerProviderOptions - VmLens concurrent cache test covering concurrent get/put/expiry scenarios - Tests for negative and exponential number formats in FlagValueConverterTest - Integration test null guard for provider Signed-off-by: Mahesh Patil <maheshfinity@gmail.com>
- Move all setup (clock, cache, expired entry) inside the VmLens loop
so each interleaving starts with a clean, deterministic state
- Replace the conditional assertion with an unconditional check:
assertThat(cache.get("key")).isPresent().hasValue("new-value")
After Thread A's put() completes, the new value must always be present;
the previous if-present guard would silently pass a correctness bug
- Remove unused outer cache/controllableClock/now variables and the
redundant sharedCache/freshClock inside the loop
Signed-off-by: Mahesh Patil <maheshfinity@gmail.com>
… test" This reverts commit 58cf2a6. Signed-off-by: Mahesh Patil <maheshfinity@gmail.com>
The previous implementation had several structural problems:
- clock, cache (and an unused outer cache) were recreated inside the
VmLens while-loop. VmLens needs stable object references across
iterations to correctly track which shared memory locations are
concurrently accessed; recreating objects each iteration means VmLens
sees different heap addresses and cannot build a reliable access model.
- now.set(now.get().plusSeconds(31)) inside the loop kept advancing the
clock monotonically, so state was never cleanly reset between
interleavings and could allow "new-value" entries to expire.
- The assertion was too weak: if (result.isPresent()) { ... } would
silently pass even when the cache is empty, hiding the exact bug the
test is meant to catch.
Fixed by:
- Creating clock and cache once outside the loop (stable references)
- Resetting state inside the loop to fixed instants (t0/t1) via
cache.clear() + now.set(t0) before each interleaving
- Using an unconditional assertion:
assertThat(cache.get("key")).isPresent().hasValue("new-value")
After Runner.runParallel returns, Thread A's put() has completed so
"new-value" must always survive Thread B's expiry-removal.
Signed-off-by: Mahesh Patil <maheshfinity@gmail.com>
Adds getOnTimedOutEntryWhileConcurrentInsertNeverReturnsStaleValue following the FlagdProviderCTest pattern: shared state prepared once before the interleaving loop in @beforeeach, only Runner.runParallel inside the loop, and assertions embedded in the parallel lambdas. Verifies that get() on a timed-out entry concurrent with a put() of the same key never returns the stale value — result must be empty or the freshly inserted value. Signed-off-by: Mahesh Patil <maheshfinity@gmail.com>
1. Per review comments so secret and parameter manager both live in single provider 2. Common code/scaffolding will be part of initial PR along with Secret Manager provider. Signed-off-by: Mahesh Patil <maheshfinity@gmail.com>
…oseNewEntry Signed-off-by: Mahesh Patil <maheshfinity@gmail.com>
…pen-feature#1781) Signed-off-by: Thomas Poignant <thomas.poignant@gofeatureflag.org> Signed-off-by: Mahesh Patil <maheshfinity@gmail.com>
…g 1.1.2 (open-feature#1697) Signed-off-by: OpenFeature Bot <109696520+openfeaturebot@users.noreply.github.com> Signed-off-by: Mahesh Patil <maheshfinity@gmail.com>
Signed-off-by: Mahesh Patil <maheshfinity@gmail.com>
Signed-off-by: Mahesh Patil <maheshfinity@gmail.com>
a09edbf to
4db90cd
Compare
Signed-off-by: Mahesh Patil <17205424+mahpatil@users.noreply.github.com>
Signed-off-by: Mahesh Patil <maheshfinity@gmail.com>
Signed-off-by: Mahesh Patil <maheshfinity@gmail.com>
Signed-off-by: Mahesh Patil <maheshfinity@gmail.com>
Signed-off-by: Mahesh Patil <maheshfinity@gmail.com>
Signed-off-by: Mahesh Patil <maheshfinity@gmail.com>
Signed-off-by: Mahesh Patil <maheshfinity@gmail.com>
Signed-off-by: Mahesh Patil <maheshfinity@gmail.com>
Signed-off-by: Mahesh Patil <maheshfinity@gmail.com>
Signed-off-by: Mahesh Patil <maheshfinity@gmail.com>
…/java-sdk-contrib into feat/gcp-secret-manager
okay thanks for clarifying. I have now consolidated this into single provider for GCP. Do we also want to have single factory and tests to instantiate & test both the providers? Happy to incorporate that here or when merging Parameter manager. |
| @Override | ||
| public void initialize(EvaluationContext evaluationContext) throws Exception { | ||
| options.validate(); | ||
| if (client == null) { |
There was a problem hiding this comment.
when would the client not be null here?
There was a problem hiding this comment.
Is that the only comment? How about rest of the design/refactor? Or do we need a call to review these changes.
There was a problem hiding this comment.
When using this constructor? client might be null
public GcpSecretManagerProvider(GcpProviderOptions options) {
this.options = options;
}
Summary
samples/gcp-secret-manager-sample/with setup/teardown scriptsProvider Details
providers/gcp-secret-managerGcpSecretManagerProviderTest plan
FlagCache,FlagValueConverter, andGcpSecretManagerProviderGcpSecretManagerProviderIntegrationTest) requires a real GCP project — setGCP_PROJECT_IDenv var to runsamples/gcp-secret-manager-sample/README.mdto run end-to-end