Skip to content

Reentrant provider events reach client callbacks out of order #332

Open

Description

@leoromanovsky

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions