Repository navigation
Description
Problem
Applications tracking provider availability through client event handlers can receive PROVIDER_READY followed by PROVIDER_ERROR after a provider has recovered. A similar sequence delivers PROVIDER_STALE before the preceding PROVIDER_READY.
This happens when a global event handler synchronously emits another provider event. The nested event reaches client handlers before the original event does. It reproduces with openfeature-sdk 0.5.1, 0.6.5, and current main at e602d1559af71324dcc11cc45462ba47075f32f9, using Ruby 4.0.1.
Related: #300 tracks provider lifecycle event ownership, and #137 tracks broader concurrency. This reproduction uses an already initialized provider and manually emitted events, so it also occurs independently of synthetic initialization events.
Reproduction
Save this as repro.rb and run it in an environment with openfeature-sdk installed. No Datadog dependency, network requests, or background threads are needed.
require "open_feature/sdk"
class EmittingProvider < OpenFeature::SDK::Provider::InMemoryProvider
include OpenFeature::SDK::Provider::EventEmitter
end
configuration = OpenFeature::SDK.configuration
provider = EmittingProvider.new
configuration.set_provider_and_wait(provider)
provider.emit_event(OpenFeature::SDK::ProviderEvent::PROVIDER_STALE)
client = OpenFeature::SDK::Client.new(provider: provider)
events = []
first, following = (ARGV.first == "ready_stale") ? [:READY, :STALE] : [:ERROR, :READY]
first_event = OpenFeature::SDK::ProviderEvent.const_get("PROVIDER_#{first}")
following_event = OpenFeature::SDK::ProviderEvent.const_get("PROVIDER_#{following}")
configuration.add_handler(first_event, ->(_) { provider.emit_event(following_event) })
client.add_handler(first_event, ->(_) { events << first })
client.add_handler(following_event, ->(_) { events << following })
events.clear # Ignore immediate callbacks during handler registration.
provider.emit_event(first_event)
puts "Expected: #{[first, following].inspect}; actual: #{events.inspect}"| Command | Expected callbacks | Actual callbacks |
|---|---|---|
ruby repro.rb |
[:ERROR, :READY] |
[:READY, :ERROR] |
ruby repro.rb ready_stale |
[:READY, :STALE] |
[:STALE, :READY] |
Proposed direction
In Configuration#dispatch_provider_event, state is updated before global handlers and then client handlers run. A global handler can recursively dispatch another event before the original client callbacks finish.
Finish delivery of the current event to global and client handlers before dispatching events emitted reentrantly on the same thread. Deferred events should preserve their emission order and payload, and must be invalidated when the associated provider registration ends through shutdown or replacement. This need not impose total ordering across independent threads.
A local fix and SDK-native regression tests are in preparation. The downstream report that led to this reproduction is DataDog/dd-trace-rb#6418.
Activity
josecolella commentedon Oct 22, 2026
Thanks for the clear repro. The ordering issue is confirmed on
main. There's a related one: a client handler that emits an event or callsadd_handler/remove_handlerraisesThreadError, because client handlers run while holding a non-reentrant mutex. You mentioned a local fix in progress. Is it close? If not, I'll take this as part of the #300 work (re-entrant dispatch queue, with handlers called outside the lock) and credit you withCo-authored-by. Your repro would become the regression test.josecolella commentedon Oct 23, 2026
@leoromanovsky #351 fixes this, following the direction you proposed:
Your
repro.rbfrom this issue now prints[:ERROR, :READY]and[:READY, :STALE]for the two orderings. The same cases are now specs inspec/specification/events_spec.rb.The squash commit will credit you as co-author. If your local patch differs, or the Datadog provider exercises a path the specs don't, I'd appreciate a review on #351.