Skip to content

fix: support binding a provider to multiple domains - #528

Open
erka wants to merge 2 commits into
mainfrom
rd/multi-bound
Open

fix: support binding a provider to multiple domains#528
erka wants to merge 2 commits into
mainfrom
rd/multi-bound

Conversation

@erka

@erka erka commented Aug 6, 2026

Copy link
Copy Markdown
Member

This PR

  • Allow the same provider instance to be bound as the default provider
    and/or to multiple domains. Reuse the existing initialized provider
    instead of initializing it again.

Related Issues

Fixes #527

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR deduplicates provider initialization and shutdown across default and named bindings. It strengthens provider identity comparison, closes initialization channels after errors, adds context-aware lifecycle test utilities, and expands coverage for multi-binding and shutdown errors.

Changes

Provider lifecycle handling

Layer / File(s) Summary
Provider identity comparison
openfeature/reference.go, openfeature/reference_test.go
Provider references now compare dynamic types, comparable values, and non-comparable values with reflect.DeepEqual.
Tracked registration and initialization
openfeature/event_executor.go, openfeature/openfeature_api.go
Default and named provider registration skips initialization for tracked providers. Initialization channels close after success or error.
Deduplicated provider shutdown
openfeature/openfeature_api.go
Shutdown handles StateHandler providers, avoids duplicate shutdown across bindings, invokes context-aware shutdown, and aggregates errors.
Lifecycle test coverage and utilities
openfeature/context_aware_test.go, openfeature/openfeature_test.go, openfeature/util_test.go
Shared context-aware test handlers and constructors cover multi-binding, cancellation, timeout, propagation, replacement, error handling, and concurrent operations.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant OpenFeatureAPI
  participant eventExecutor
  participant FeatureProvider
  Client->>OpenFeatureAPI: Register provider
  OpenFeatureAPI->>eventExecutor: Check provider tracking
  alt provider already tracked
    OpenFeatureAPI-->>Client: Return closed completion channel
  else provider is new
    OpenFeatureAPI->>FeatureProvider: Initialize
    FeatureProvider-->>OpenFeatureAPI: Return initialization result
    OpenFeatureAPI-->>Client: Close completion channel
  end
  Client->>OpenFeatureAPI: Shutdown
  OpenFeatureAPI->>FeatureProvider: Shutdown once per provider reference
  FeatureProvider-->>OpenFeatureAPI: Return shutdown result
Loading

Possibly related issues

Possibly related PRs

Suggested reviewers: sahidvelji

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the primary change: allowing one provider to bind to multiple domains.
Description check ✅ Passed The description directly explains provider reuse across the default provider and multiple domains.
Linked Issues check ✅ Passed The changes skip re-initialization for tracked providers and reuse existing state when binding additional domains, satisfying issue #527.
Out of Scope Changes check ✅ Passed The provider reference, shutdown, initialization, and test changes support safe multi-domain provider binding and are within scope.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Aug 6, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.73%. Comparing base (eebb3bd) to head (cb25dbf).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #528      +/-   ##
==========================================
+ Coverage   86.45%   86.73%   +0.28%     
==========================================
  Files          22       22              
  Lines        2119     2149      +30     
==========================================
+ Hits         1832     1864      +32     
+ Misses        242      241       -1     
+ Partials       45       44       -1     
Flag Coverage Δ
e2e 86.73% <100.00%> (+0.28%) ⬆️
unit 86.73% <100.00%> (+0.28%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

erka added 2 commits August 6, 2026 12:27
Allow the same provider instance to be bound as the default provider
and/or to multiple domains. Reuse the existing initialized provider
instead of initializing it again.

Signed-off-by: Roman Dmytrenko <rdmytrenko@gmail.com>
Signed-off-by: Roman Dmytrenko <rdmytrenko@gmail.com>
@erka
erka marked this pull request as ready for review August 6, 2026 12:50
@erka
erka requested review from a team as code owners August 6, 2026 12:50

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
openfeature/context_aware_test.go (1)

208-230: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Configure an actual slow shutdown.

slowShutdownProvider uses the default 10 ms shutdown delay. The 100 ms sleep therefore does not test replacement during a slow shutdown.

Proposed test fix
- slowShutdownProvider := newTestContextAwareProvider(t, 10*time.Millisecond)
+ slowShutdownProvider := newTestContextAwareProvider(t, 10*time.Millisecond, 5*time.Second)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openfeature/context_aware_test.go` around lines 208 - 230, Update the
“shutdown timeout handling” test to configure slowShutdownProvider with a
shutdown delay substantially longer than the 100 ms wait, while keeping
fastProvider unchanged. Use the existing newTestContextAwareProvider setup and
preserve the assertions that provider replacement succeeds while the previous
provider shuts down asynchronously.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@openfeature/openfeature_api.go`:
- Around line 226-232: Ensure reused providers wait for the original
initialization result instead of receiving an immediately closed success
channel: update the tracked-provider path in
openfeature/openfeature_api.go:226-232 and the equivalent named-provider path at
openfeature/openfeature_api.go:264-270 to store and return the existing pending
result, propagating initialization failures. Add a blocking or failing Init test
in openfeature/openfeature_test.go:258-282 that binds the same provider to a
second target before the first initialization completes.

In `@openfeature/reference.go`:
- Around line 24-41: Use one identity policy for non-comparable providers across
providerReference.equals, its coverage in openfeature/reference_test.go:85-100,
and the handling in openfeature/event_executor.go:385-393. Replace
reflect.DeepEqual-based state comparison with a stable identity mechanism, or
consistently treat non-comparable copyable providers as distinct, ensuring
registration tracking and shutdown do not conflate separate providers or
repeatedly reinitialize the same provider.

---

Outside diff comments:
In `@openfeature/context_aware_test.go`:
- Around line 208-230: Update the “shutdown timeout handling” test to configure
slowShutdownProvider with a shutdown delay substantially longer than the 100 ms
wait, while keeping fastProvider unchanged. Use the existing
newTestContextAwareProvider setup and preserve the assertions that provider
replacement succeeds while the previous provider shuts down asynchronously.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: edfca913-6a26-4e50-a752-331cfe237e5f

📥 Commits

Reviewing files that changed from the base of the PR and between eebb3bd and cb25dbf.

📒 Files selected for processing (7)
  • openfeature/context_aware_test.go
  • openfeature/event_executor.go
  • openfeature/openfeature_api.go
  • openfeature/openfeature_test.go
  • openfeature/reference.go
  • openfeature/reference_test.go
  • openfeature/util_test.go

Comment on lines +226 to +232
// If the provider is already tracked, it has already been initialized.
// Return an already-closed channel so callers can proceed without waiting.
if tracked {
errCh := make(chan error, 1)
close(errCh)
return errCh, nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Wait for the original initialization before reporting a reused provider as ready.

Registration adds the provider to event tracking before initNew completes. A second binding can therefore see tracked == true, receive a closed channel, and return nil while the original Init is still running or has failed.

  • openfeature/openfeature_api.go#L226-L232: Store and return the existing provider initialization result instead of a closed success channel.
  • openfeature/openfeature_api.go#L264-L270: Apply the same pending-initialization behavior for named providers.
  • openfeature/openfeature_test.go#L258-L282: Add a blocking or failing Init case that binds the same provider to the second target before the first initialization completes.
📍 Affects 2 files
  • openfeature/openfeature_api.go#L226-L232 (this comment)
  • openfeature/openfeature_api.go#L264-L270
  • openfeature/openfeature_test.go#L258-L282
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openfeature/openfeature_api.go` around lines 226 - 232, Ensure reused
providers wait for the original initialization result instead of receiving an
immediately closed success channel: update the tracked-provider path in
openfeature/openfeature_api.go:226-232 and the equivalent named-provider path at
openfeature/openfeature_api.go:264-270 to store and return the existing pending
result, propagating initialization failures. Add a blocking or failing Init test
in openfeature/openfeature_test.go:258-282 that binds the same provider to a
second target before the first initialization completes.

Comment thread openfeature/reference.go
Comment on lines +24 to 41
// equals reports whether pr and other refer to the same provider.
//
// For providers whose dynamic type is comparable, equality is determined using
// Go's == operator. For non-comparable provider types (such as those containing
// maps or slices), equality falls back to reflect.DeepEqual.
//
// The providers must have the same dynamic type to be considered equal.
func (pr providerReference) equals(other providerReference) bool {
if pr.kind == reflect.Pointer && other.kind == reflect.Pointer {
if pr.typeOf == nil {
return false
}
if pr.typeOf != reflect.TypeOf(other.featureProvider) {
return false
}
if pr.typeOf.Comparable() {
return pr.featureProvider == other.featureProvider
}
return reflect.DeepEqual(pr.featureProvider, other.featureProvider)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

According to Go's reflect.DeepEqual documentation, do two separately constructed struct values that contain maps with equal contents compare equal?

💡 Result:

Yes, according to Go's reflect.DeepEqual documentation and implementation, two separately constructed struct values containing maps with equal contents will compare as equal [1][2][3]. Go's reflect.DeepEqual function performs a deep comparison of the data structures [1][4]. For map types, the documentation and source code specify that two maps are considered deeply equal if [1][2]: 1. They are both nil or both non-nil [1][2]. 2. They have the same length [1][2]. 3. Either they are the same map object (sharing the same memory address) or their corresponding keys map to deeply equal values [2][5]. Because reflect.DeepEqual recursively traverses the fields of a struct, if two structs have identical types and their corresponding fields are deeply equal, the structs themselves are considered deeply equal [2]. Consequently, when two structs contain maps, the function will successfully compare the contents of those maps, verifying that they contain the same key-value pairs, rather than simply checking if they refer to the same map object in memory [2][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Files:\n'
git ls-files | rg '(^|/)openfeature/(reference|reference_test|event_executor|openfeature_api|openfeature_test)\.go$' || true

printf '\nreference.go:\n'
cat -n openfeature/reference.go

printf '\nnearby tests:\n'
cat -n openfeature/reference_test.go | sed -n '1,140p'

printf '\nevent_executor isTracked and subscription init shutdown locations:\n'
rg -n "isTracked|newProviderRef|activeSubscriptions|Shutdown|featureProvider" openfeature -g '*event_executor.go' -g '*reference.go' -g '*openfeature_api.go'

printf '\nevent_executor relevant sections:\n'
cat -n openfeature/event_executor.go | sed -n '340,420p'

Repository: open-feature/go-sdk

Length of output: 13076


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'openfeature_api relevant sections:\n'
cat -n openfeature/openfeature_api.go | sed -n '200,270p'
cat -n openfeature/openfeature_api.go | sed -n '290,330p'
cat -n openfeature/openfeature_api.go | sed -n '404,440p'

printf '\npickSet usages:\n'
rg -n "pickSet|activeSubscriptions|setProvider|SetProvider|Shutdown" openfeature -g '*.go'

printf '\nGo reflect.DeepEqual behavior probe:\n'
go version || true
cat > /tmp/deep_equal_probe.go <<'GO'
package main

import (
	"fmt"
	"reflect"
)

type provider struct {
	data map[string]string
}

func main() {
	p1 := provider{data: map[string]string{"k": "v"}}
	p2 := provider{data: map[string]string{"k": "v"}}
	fmt.Println("type comparable:", reflect.TypeOf(p1).Comparable())
	fmt.Println("reflect.DeepEqual:", reflect.DeepEqual(p1, p2))
}
GO
cd /tmp && go run deep_equal_probe.go

Repository: open-feature/go-sdk

Length of output: 37658


Use one instance-identity policy for non-comparable providers.

reflect.DeepEqual treats distinct providers with the same map contents as equal, which is state equality, not provider identity. This can skip shutdown for a different provider registered with the same content and makes isTracked always reinitialize non-comparable providers. Use a stable identity mechanism for non-comparable providers, or treat non-comparable copyable providers as distinct at all call sites (reference.go, reference_test.go, event_executor.go).

📍 Affects 3 files
  • openfeature/reference.go#L24-L41 (this comment)
  • openfeature/reference_test.go#L85-L100
  • openfeature/event_executor.go#L385-L393
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openfeature/reference.go` around lines 24 - 41, Use one identity policy for
non-comparable providers across providerReference.equals, its coverage in
openfeature/reference_test.go:85-100, and the handling in
openfeature/event_executor.go:385-393. Replace reflect.DeepEqual-based state
comparison with a stable identity mechanism, or consistently treat
non-comparable copyable providers as distinct, ensuring registration tracking
and shutdown do not conflate separate providers or repeatedly reinitialize the
same provider.

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.

SDK re-initializes a provider already bound to another domain, causing multi-Init

1 participant