Skip to content

Conversation

@sameerank
Copy link

@sameerank sameerank commented Dec 11, 2025

This PR

Fixes the behavior of set_provider_and_wait and implements eventing to trigger event handlers on changes to provider state.

What was wrong?

Both set_provider and set_provider_and_wait initialize synchronously. By fixing set_provider to be non-blocking, it is also useful to have eventing, so the SDK can be used to trigger handlers according to background provider updates.

The Ruby SDK was missing the provider lifecycle event system defined in the OpenFeature specification, which other SDKs use for monitoring provider state changes and enabling event-driven workflows.

What this PR does:

  • Makes set_provider non-blocking - returns immediately and initializes in a background thread, enabling async operation
  • Implements provider lifecycle events (PROVIDER_READY, PROVIDER_ERROR, etc.) as defined in the Events specification to enable event-driven workflows
  • Adds provider state management - tracks provider states (NOT_READY, READY, ERROR, FATAL, STALE) as defined in the Provider Status specification
  • Enables background provider monitoring - providers can emit events for configuration changes, connection issues, etc.
  • Breaking change: Removed timeout parameter from set_provider_and_wait to align with other OpenFeature SDKs (Go and Java don't support timeouts)

Key changes:

  1. Event System Foundation:

  2. Provider Interface (opt-in via mixin, based on Go SDK patterns):

  3. Provider Initialization:

    • Blocking pattern: Provider init() methods are expected to block until ready
    • OpenFeature::SDK.set_provider(provider) - runs init in background thread (non-blocking API call)
    • OpenFeature::SDK.set_provider_and_wait(provider) - runs init synchronously and blocks until complete (Req 1.1.2.4)
    • Events are emitted automatically by SDK after init completion and can be used by consumers via handlers
  4. Error Handling and Logging:

    • Exception handling (rescues StandardError only, not all exceptions)
    • ProviderInitializationError exception for set_provider_and_wait failures with structured error details (provider, error_code, original_error)
    • Provider events use hash-based error details ({error_code:, message:}) similar to Java's ProviderEventDetails
    • Configurable logger support (config.logger = my_logger) - Note: Go SDK deprecated SetLogger, Java uses SLF4J facade
    • Events include error codes and messages only (not full exceptions) - matching Go's event structure and Java's pattern
  5. Testing:

    • Specification requirement tests for traceability
    • 100% test coverage + Standard Ruby linting

Related Issues

Fixes #51

Notes

  • This implementation mostly follows the Go SDK but adapted for Ruby (opt-in event support via mixins, similar to Go's interface approach) with some difference, e.g. Ruby uses callbacks for event handlers vs Go's interface-based approach.
  • Ruby allows user-configurable logging while Go only logs panics internally and Java delegates to SLF4J
  • Changes are mostly backward compatible - providers don't need modifications unless they want event capabilities or were using the timeout parameter in set_provider_and_wait

Follow-up Tasks

  • Documentation updates/migration guide for events support in the Ruby SDK
  • Consider adding client-level event handlers (currently only API-level) as described in Req 5.2.1
  • Something I noticed: RFC 2119 compliance cleanup in specification tests (key words like MUST, SHOULD, MAY, etc. are typically in caps)

How to test

  1. Basic functionality:
# Async initialization (non-blocking)
OpenFeature::SDK.set_provider(MyProvider.new)  # Returns immediately

# Blocking initialization
OpenFeature::SDK.set_provider_and_wait(MyProvider.new)  # Blocks until ready
  1. Event handlers:
OpenFeature::SDK.add_handler(OpenFeature::SDK::ProviderEvent::PROVIDER_READY) do |event|
  puts "Provider #{event[:provider_name]} is ready!"
end
  1. Run the test suite:
bundle exec rspec
# 447 / 447 LOC (100.0%) covered.
  1. Example provider:
# Provider using EventHandler
class CustomProvider
  include OpenFeature::SDK::Provider::EventHandler

  def init(evaluation_context)
    # Initialize provider synchronously - blocks until ready
    fetch_configuration  # This should block
    # Note: SDK automatically emits PROVIDER_READY when init completes
    
    # Start background process to monitor for runtime changes
    Thread.new do
      monitor_for_config_changes
      if configuration_changed?
        emit_event(OpenFeature::SDK::ProviderEvent::PROVIDER_CONFIGURATION_CHANGED,
                   message: "Configuration updated")
      end
    end
  end
end

@gemini-code-assist
Copy link

Summary of Changes

Hello @sameerank, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a critical issue where the set_provider_and_wait method did not actually wait for providers to be ready, leading to potential race conditions and incorrect flag evaluations. It introduces a robust, OpenFeature-compliant provider lifecycle event system, enabling asynchronous provider initialization and reliable state management. This change ensures that the SDK correctly handles provider readiness and errors, improving stability and adherence to the specification.

Highlights

  • Provider Event System: Introduced a comprehensive event system for providers, including EventEmitter for thread-safe pub/sub, ProviderEvent constants, and EventToStateMapper for state transitions, aligning with OpenFeature specifications.
  • Asynchronous Provider Initialization: Refactored set_provider to be non-blocking, initializing providers in background threads. This allows for asynchronous operations without blocking the main thread.
  • Corrected set_provider_and_wait Behavior: Fixed set_provider_and_wait to properly block until a PROVIDER_READY or PROVIDER_ERROR event is received, ensuring providers are ready before returning, fulfilling a critical OpenFeature requirement.
  • Provider State Management: Implemented ProviderStateRegistry to track provider states (NOT_READY, READY, ERROR, FATAL, STALE) based on emitted events, providing better visibility into provider lifecycle.
  • New Provider Interfaces: Added opt-in mixins for provider interfaces: StateHandler (init/shutdown), EventHandler (event emission), and ContextAwareStateHandler (timeout support for init/shutdown), inspired by the Go SDK.
  • Backward Compatibility: Ensured that existing providers continue to function without modification, while new interfaces allow for enhanced event-driven capabilities.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request is a significant and well-executed enhancement that introduces a provider eventing system, aligning the Ruby SDK more closely with the OpenFeature specification. The core improvement is fixing set_provider_and_wait to correctly block until a provider is ready, which is achieved through a robust, thread-safe event mechanism using EventEmitter, ProviderStateRegistry, and new provider interfaces (EventHandler, StateHandler). The changes are thoughtfully designed to be backward-compatible, and the comprehensive new test suite, including specification compliance tests, is excellent.

My feedback focuses on a few areas to further improve robustness and maintainability: refining exception handling to be more specific, enhancing debuggability by preserving original error information in event payloads, and cleaning up a small amount of unused code.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request is a fantastic and significant improvement. It correctly implements the provider eventing system as specified by OpenFeature, fixing the broken behavior of set_provider_and_wait. The introduction of EventEmitter, EventHandler, and state management is well-structured and thread-safe. The changes are also backward compatible, which is a huge plus. The test coverage for the new asynchronous logic is thorough. I've found a few minor but important issues related to overly broad exception handling (rescue => e) and a small redundancy in event payload creation, which I've commented on. Once these are addressed, this PR will be in excellent shape.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This is an excellent pull request that introduces a provider eventing system, aligning the Ruby SDK more closely with the OpenFeature specification. The implementation correctly fixes the behavior of set_provider_and_wait and makes set_provider non-blocking, which is a great improvement for providers requiring asynchronous initialization. The code is well-structured, with clear separation of concerns into new modules and classes for eventing and state management. The test coverage is comprehensive and thoughtful, especially the tests for backward compatibility and specification compliance.

I have identified a few areas for improvement, primarily focusing on enhancing thread safety and encapsulation. Specifically, I've pointed out a potential race condition in the error handling of set_provider_and_wait and suggested a way to improve how the logger is managed. These changes will make the implementation more robust in concurrent environments. Overall, this is a very strong contribution.

- Add ProviderEvent module with event type constants (PROVIDER_READY, etc.)
- Add ProviderState module with state constants (NOT_READY, READY, ERROR, etc.)
- Implement EventEmitter class for thread-safe pub-sub event handling
- Add EventToStateMapper for mapping events to provider states
- Include comprehensive test coverage for all components

This introduces the event infrastructure needed for OpenFeature spec-compliant
provider lifecycle management. The implementation is additive only, maintaining
full backward compatibility with existing provider behavior.

Signed-off-by: Sameeran Kunche <[email protected]>
Add mixins and classes to support provider lifecycle management:
- StateHandler: Basic provider lifecycle interface with init/shutdown
- EventHandler: Event emission capability for providers
- ContextAwareStateHandler: Timeout support for initialization
- ProviderStateRegistry: Thread-safe provider state tracking
- EventAwareNoOpProvider: Example implementation demonstrating interfaces

These interfaces enable providers to emit lifecycle events and support
both synchronous and asynchronous initialization patterns while maintaining
backward compatibility with existing providers.

Signed-off-by: Sameeran Kunche <[email protected]>
- Fix context_aware_state_handler_spec to expect both positional and keyword arguments
- Add newlines to end of provider files for consistency
- Add arm64-darwin-24 platform support to Gemfile.lock

Signed-off-by: Sameeran Kunche <[email protected]>
…ycle

Refactor provider lifecycle to be truly asynchronous:
- set_provider now returns immediately and initializes providers in background
- set_provider_and_wait uses event system to block until initialization completes
- Providers emit PROVIDER_READY/PROVIDER_ERROR events on initialization
- Event handlers enable non-blocking application startup patterns
- Maintains backward compatibility with existing providers

The implementation follows OpenFeature specification requirements:
- setProvider is non-blocking (returns immediately)
- setProviderAndWait blocks until provider is ready or errors
- Provider state transitions are tracked via events
- Failed initialization properly restores previous provider state

Added comprehensive test coverage for async behavior, error handling,
event emission, and backward compatibility scenarios.

Signed-off-by: Sameeran Kunche <[email protected]>
…fecycle

Add specification tests for requirements implemented in this branch:
- Requirement 1.1.2.4: set_provider_and_wait blocking behavior
- Requirement 2.4.2.1: Provider initialization error handling
- Requirement 5.x: Provider lifecycle events and handlers

The tests follow Ruby SDK's existing pattern of using RSpec contexts
to organize tests by specification requirements. This provides clear
traceability between implementation and specification compliance.

Also exposed add_handler/remove_handler methods on the SDK API to
support event specification tests.

Signed-off-by: Sameeran Kunche <[email protected]>
- Remove verbose method documentation from event_emitter.rb
- Remove detailed parameter descriptions from provider modules
- Remove implementation comments from configuration.rb
- Keep only essential comments explaining non-obvious behavior
- Maintain brief class/module descriptions where helpful

Follows minimalist Ruby documentation style used throughout the codebase

Signed-off-by: Sameeran Kunche <[email protected]>
- Replace bare rescue with rescue StandardError in configuration.rb
- Add ProviderInitializationFailure class for structured error details
- Pass error codes and messages in events (not full exceptions)
- Add configurable logger support to Configuration and EventEmitter
- Remove unused state_from_error and fatal_error? methods

These changes address all issues identified by the Gemini code review bot
while maintaining alignment with other OpenFeature SDKs (Go and Java).

Signed-off-by: Sameeran Kunche <[email protected]>
…l value

- ContextAwareStateHandler was just a placeholder that only included StateHandler
- Other SDKs (Go, Java) don't expose timeout methods on provider interfaces
- Timeouts are controlled at the API level (set_provider_and_wait), not provider level
- Providers can implement their own internal timeout logic if needed
- Also removed unused state_from_error tests that were causing failures

Signed-off-by: Sameeran Kunche <[email protected]>
- Replace overly broad 'rescue => e' with 'rescue StandardError => e' in 3 files
  to avoid catching system-level exceptions like Interrupt and SystemExit
- Remove redundant provider merge in event_handler.rb since provider is already
  passed as first argument to dispatch_event
- Update tests to match the simplified event dispatching

Signed-off-by: Sameeran Kunche <[email protected]>
- Fix race conditions in set_provider_and_wait by adding revert_provider_if_current
  method that only reverts if the current provider matches the one being set
- Improve encapsulation by adding attr_writer :logger to EventEmitter instead of
  using instance_variable_set
- Add logging for provider shutdown errors for better debugging visibility
- Update test coverage in PR description to 98.73% (468/474 LOC)

Signed-off-by: Sameeran Kunche <[email protected]>
@sameerank sameerank force-pushed the feat/add-provider-eventing branch from d5d4c34 to 6eefe78 Compare December 11, 2025 10:59
Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This is an excellent and comprehensive pull request that addresses a critical issue with set_provider_and_wait and brings the Ruby SDK much closer to the OpenFeature specification for provider eventing. The introduction of an asynchronous, event-driven provider lifecycle is a significant improvement.

I'm impressed by the thoroughness of the implementation, including:

  • Thread-safe event emitter and state management.
  • Correct handling of asynchronous initialization and error conditions.
  • Thoughtful approach to backward compatibility, allowing old providers to function without modification.
  • Extensive test coverage, including tests for concurrency, which gives high confidence in these complex changes.

I have one suggestion regarding the creation of the event dispatcher in Configuration#set_provider to improve code clarity and maintainability.

Overall, this is a high-quality contribution that significantly enhances the SDK's functionality and robustness.

- Replace dynamic Object creation with singleton methods with a proper
  private inner class
- Improves code clarity, maintainability, and performance
- Follows Ruby idioms for encapsulation
- Suggested by Gemini code review

Signed-off-by: Sameeran Kunche <[email protected]>
Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a comprehensive provider eventing system, which is a significant and well-executed feature. It successfully fixes the behavior of set_provider_and_wait to be truly blocking and aligns the SDK with the OpenFeature specification for provider events. The implementation includes a thread-safe event emitter, new provider mixins for lifecycle management, and asynchronous provider initialization, all supported by a thorough test suite. I've identified a couple of critical scoping issues that would cause runtime errors and a minor opportunity for code cleanup. Overall, this is an excellent contribution to the SDK.

…tests

- Remove EventAwareNoOpProvider as it doesn't exist in other OpenFeature SDKs
- Rename provider_backward_compatibility_spec.rb to provider_compatibility_spec.rb
- Reorganize tests into three logical sections:
  - Providers Without Event Capabilities
  - Mixed Provider Usage
  - Provider Interface Detection
- Update all references and imports accordingly
- Maintain 99.13% test coverage

Signed-off-by: Sameeran Kunche <[email protected]>
Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This is a fantastic pull request that brings the Ruby SDK much closer to the OpenFeature specification for provider events and lifecycle. The implementation of an async set_provider and a truly blocking set_provider_and_wait using a thread-safe event system is well-executed. The code is clean, modular, and impressively well-tested, covering backward compatibility, thread safety, and specification requirements.

I've found two potential race conditions with high severity that should be addressed:

  1. In Configuration#set_provider_and_wait, there's a race condition that could lead to incorrect provider state restoration on initialization failure.
  2. In Provider::EventHandler#emit_event, a race condition could lead to a NoMethodError if a provider is detached concurrently.

After addressing these concurrency issues, this PR will be a very strong and valuable addition to the SDK. Great work on the thoroughness and quality of this change.

- Fix race condition in set_provider_and_wait where stale provider could be restored
  - Modified set_provider to return the old provider atomically
  - set_provider_and_wait now uses the atomically-retrieved old provider

- Fix race condition in EventHandler#emit_event
  - Store @event_dispatcher in local variable before check and use
  - Prevents NoMethodError if detach is called between check and method call

These changes ensure thread-safe operation under concurrent access patterns.

Signed-off-by: Sameeran Kunche <[email protected]>
Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This is an excellent and comprehensive implementation of the provider eventing system, aligning the Ruby SDK more closely with the OpenFeature specification. The changes are well-structured, thread-safe, and maintain backward compatibility. The introduction of EventEmitter, StateHandler, and EventHandler provides a solid foundation for asynchronous providers. The new test suites are thorough and cover many edge cases, which is great to see.

I've found one critical issue related to handling nil providers during shutdown and have one suggestion to improve the maintainability of the new test files. Overall, this is a very strong contribution.

Critical fixes:
- Fix NoMethodError when old_provider is nil during shutdown
- Use safe navigation operators (&.) for nil provider handling
- Add test coverage for setting provider to domain with no previous provider

API improvements:
- Refactor API class to follow OpenFeature SDK patterns
- Replace long def_delegators list with explicit methods
- Add clear_all_handlers method for improved test cleanup
- Align with Go SDK's individual function approach vs bulk delegation

All tests passing with 98.74% coverage maintained.

Signed-off-by: Sameeran Kunche <[email protected]>
Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a comprehensive event handling and provider state management system to the OpenFeature SDK. The set_provider method is now asynchronous, initializing providers in a separate thread, while a new set_provider_and_wait method provides blocking initialization with timeout and error handling, including reverting to the previous provider on failure. This is achieved through new EventEmitter and ProviderStateRegistry classes, along with EventHandler and StateHandler mixins for providers to implement event emission and lifecycle methods. The API now exposes methods for managing event handlers and configuring the logger. Review comments suggest enhancing error and warning messages in Configuration by including the provider's class name for improved debugging, and raise a concern about the fragility of relying on init method arity for argument handling.

Address Gemini code review feedback:

Error message improvements:
- Include provider class names in all error messages for better debugging
- 'Provider #{provider.class.name} initialization failed: #{message}'
- 'Provider #{provider.class.name} initialization timed out after #{timeout} seconds'

Init parameter handling improvements:
- Replace fragile arity check with robust try/fallback approach
- Always attempt init(evaluation_context) first (OpenFeature standard)
- Fallback to init() only for legacy providers with 0-arity methods
- Properly handle edge cases like providers expecting 2+ parameters
- Update InMemoryProvider to accept optional evaluation_context parameter

This ensures backward compatibility while aligning with OpenFeature spec
and providing clear error messages for debugging multi-provider scenarios.

Signed-off-by: Sameeran Kunche <[email protected]>
Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This is a substantial and well-executed feature addition. It correctly implements the provider eventing system according to the OpenFeature specification, which fixes the behavior of set_provider_and_wait. The use of Thread for async initialization, Queue for synchronization, and Mutex for protecting shared state is well-handled. The new modules and classes (EventEmitter, EventHandler, ProviderStateRegistry, etc.) are well-structured and promote good design patterns. Backward compatibility for existing providers is also thoughtfully implemented. The test coverage is excellent, with new unit tests, async-specific tests, and specification compliance tests. I have a couple of suggestions for minor refactoring to improve robustness and simplify the code, but overall this is a high-quality contribution.

- Replace fragile error message matching with robust method parameter inspection
- Simplify EventToStateMapper by removing EventDetails class and working with hashes directly
- Standardize on symbol keys for event details since they are generated internally
- Remove unnecessary string key support to reduce complexity

Signed-off-by: Sameeran Kunche <[email protected]>
@sameerank sameerank force-pushed the feat/add-provider-eventing branch from 65ee77b to 2611c40 Compare December 11, 2025 17:56
Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This is an excellent pull request that implements a comprehensive provider eventing system, correctly fixing the behavior of set_provider_and_wait. The changes are well-structured, thoroughly tested, and align with the OpenFeature specification. The use of background threads for initialization, Queue for synchronization, and careful handling of backward compatibility are all well-executed. My feedback is minor, focusing on improving consistency by using defined constants instead of string literals in a couple of places.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a comprehensive and well-designed provider eventing system, aligning the Ruby SDK more closely with the OpenFeature specification. The changes correctly implement asynchronous provider initialization, fix the behavior of set_provider_and_wait to be truly blocking, and add robust state and event management. The implementation demonstrates strong attention to thread safety, backward compatibility, and thorough testing. The code is of high quality, and the new features are a significant improvement to the SDK.

@sameerank sameerank marked this pull request as ready for review December 12, 2025 02:14
@sameerank sameerank requested a review from a team as a code owner December 12, 2025 02:14
Remove unnecessary inline comments that simply restate obvious constant
names and method purposes. Add proper OpenFeature specification URL
reference for provider states.

Changes:
- Remove redundant comments from ProviderEvent constants
- Remove redundant comments from ProviderState constants
- Remove unnecessary section headers in API class
- Add OpenFeature spec URL for provider states documentation

Code is now cleaner and self-documenting while maintaining valuable
specification references.

Signed-off-by: Sameeran Kunche <[email protected]>
josecolella
josecolella previously approved these changes Dec 12, 2025
@codecov
Copy link

codecov bot commented Dec 12, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (88a0498) to head (e5ba150).

Additional details and impacted files
@@             Coverage Diff             @@
##             main      #207      +/-   ##
===========================================
+ Coverage   99.61%   100.00%   +0.38%     
===========================================
  Files          17        23       +6     
  Lines         259       446     +187     
===========================================
+ Hits          258       446     +188     
+ Misses          1         0       -1     

☔ View full report in Codecov by Sentry.
📢 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.

Copy link
Member

@beeme1mr beeme1mr left a comment

Choose a reason for hiding this comment

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

Hey @sameerank, thanks for the PR. I quickly reviewed the change and left a comment on the readme. I'm not very experienced with Ruby and the PR is quite large so I'm going to see if one of my colleagues can help review. Thanks again!

Copy link
Member

Choose a reason for hiding this comment

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

Please update the feature table with the new status and update the eventing section in the readme.

Copy link
Author

Choose a reason for hiding this comment

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

Updated!

I was initially going to use the ⚠️ symbol because PROVIDER_CONTEXT_CHANGED and PROVIDER_RECONCILING are not implemented as outlined in Req 5.3.5. However, I see that eventing is marked as ✅ in the Java or Go SDKs so it seems those are optional

Choose a reason for hiding this comment

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

CONTEXT_CHANGED and RECONCILING only make sense in static-context (client) paradigm. Server SDKs don't need to support these.

Events | OpenFeature:

Providers built to conform to the static context paradigm feature two additional events: PROVIDER_RECONCILING and PROVIDER_CONTEXT_CHANGED.

And section 5.3.4 is conditioned on static-context paradigm.

Copy link
Member

Choose a reason for hiding this comment

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

Correct!

@beeme1mr beeme1mr requested a review from a team December 12, 2025 20:04
- Fix string literal style preferences (single -> double quotes)
- Correct hash syntax and spacing formatting
- Remove trailing whitespace and useless assignments
- Ensure all files comply with Standard Ruby configuration
- Resolve 550+ lint violations identified in CI logs

All 52 files now pass Standard Ruby linting with 0 offenses detected.
This ensures consistency with CI pipeline linting configuration.

Signed-off-by: Sameeran Kunche <[email protected]>
- Mark eventing feature as completed (✅) in features table
- Add eventing section with API-level event handler examples
- Document EventHandler mixin for provider event emission
- Show proper handler management with variable references for removal
- Clarify original_error behavior for ProviderInitializationError
- Align documentation with OpenFeature specification and other SDKs

Provides users with clear guidance on implementing event-driven
provider lifecycle management in their applications.

Signed-off-by: Sameeran Kunche <[email protected]>
- Move clear_all_handlers in API class to private section
- Move handler_count and total_handler_count in Configuration to private
- Update tests to use send() for accessing private testing methods
- Ensure ProviderEventDispatcher remains internal implementation detail

These methods are testing utilities not part of the OpenFeature
specification and should not be exposed in the public API.
Maintains 100% test coverage and functionality while providing
cleaner public interface.

Signed-off-by: Sameeran Kunche <[email protected]>
- Remove StateHandler module and related tests
- Update provider compatibility test to reflect duck typing usage
- Update PR description to remove StateHandler references
- No functional changes - duck typing already used in SDK

StateHandler provided no real value since Ruby's duck typing makes
interfaces optional. Providers can implement init/shutdown directly
without any mixin. This simplifies the PR while maintaining full
functionality and backward compatibility.

Signed-off-by: Sameeran Kunche <[email protected]>
@toddbaert toddbaert self-requested a review December 15, 2025 20:02
@toddbaert
Copy link
Member

@sameerank Hey! I'm also not the best with Ruby, but I authored a good deal of the specification, so I will do by best to review this at least from an "OpenFeature semantics" perspective. Expect a review from me tomorrow, and many thanks!

@toddbaert toddbaert requested a review from maxveldink December 15, 2025 20:04
@sameerank sameerank force-pushed the feat/add-provider-eventing branch from 95d63f0 to e346c45 Compare December 16, 2025 05:55
…trol

Replace event-based flow control in set_provider_and_wait with direct
blocking initialization following Go/Java SDK patterns.

- Add wait_for_init parameter to set_provider_internal
- Remove timeout and event handler cleanup from set_provider_and_wait
- Extract init_provider helper for unified initialization logic
- Preserve event emission in both sync and async paths
- Remove unused private methods

Reduces complexity by 118 lines while maintaining 100% test coverage.

Signed-off-by: Sameeran Kunche <[email protected]>
@sameerank sameerank force-pushed the feat/add-provider-eventing branch from e346c45 to e5ba150 Compare December 16, 2025 05:56
Copy link

@dd-oleksii dd-oleksii left a comment

Choose a reason for hiding this comment

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

I've left a couple of nits and corner cases but looks good overall.

Comment on lines +145 to +146
# Note: original_error is only present for timeout errors, nil for provider event errors
puts "Original error: #{e.original_error}" if e.original_error

Choose a reason for hiding this comment

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

minor: I think it makes sense to always set original_error for ergonomics and semantics (it's a bit surprising that it is not set when there is an underlying error). The fact that it's now nilable is arguably a breaking change as well.

<!-- TODO: code example of a PROVIDER_CONFIGURATION_CHANGED event for the client and a PROVIDER_STALE event for the API -->
def init(evaluation_context)
# Start background process to monitor for configuration changes
# Note: SDK automatically emits PROVIDER_READY when init completes successfully

Choose a reason for hiding this comment

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

minor: this note seems inaccurate. PROVIDER_READY is only emitted automatically iff the provider does not include the EventHandler mixin. If the mixin is used, the provider must emit its own PROVIDER_READY event. (Which this example doesn't seem to do?)

Copy link
Member

Choose a reason for hiding this comment

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

Whether or not the provider implements events, the SDK should automatically emit READY/ERROR events when the init function succeeds/fails (raises exception in this case). More here: https://github.com/open-feature/ruby-sdk/pull/207/changes#r2624429319

Comment on lines +12 to +22
case event_type
when ProviderEvent::PROVIDER_READY, ProviderEvent::PROVIDER_CONFIGURATION_CHANGED
ProviderState::READY
when ProviderEvent::PROVIDER_STALE
ProviderState::STALE
when ProviderEvent::PROVIDER_ERROR
state_from_error_event(event_details)
else
ProviderState::NOT_READY
end
end

Choose a reason for hiding this comment

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

major(spec): according Events | OpenFeature, PROVIDER_CONFIGURATION_CHANGED must not update the state. The default shall also be "no state changes."

def set_provider_and_wait(provider, domain: nil)
@provider_mutex.synchronize do
old_provider = @providers[domain]
set_provider_internal(provider, domain: domain, wait_for_init: true)

Choose a reason for hiding this comment

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

nitpick: given how set_provider_internal is used (always acquiring mutex for a single call), it almost looks like this method should acquire the mutex internally. This would also make it easier to read that method (don't need to look back at all call sites to confirm that modifying @providers is safe).

It might also use a shorter synchronize block (e.g. calling init_provider inside the synchronize block seems like it can be problematic if provider blocks).

Comment on lines -49 to +59
def set_provider_and_wait(provider, domain: nil, timeout: 30)
def set_provider_and_wait(provider, domain: nil)

Choose a reason for hiding this comment

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

major: removing the timeout parameter is a breaking change. Is this intended?

Copy link
Member

@toddbaert toddbaert Dec 16, 2025

Choose a reason for hiding this comment

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

Very good observation! It is a breaking change, but it's a divergence from the spec. I would actually recommend removing it. We instead prefer to have providers handle this themselves, with some kind of timeout, ideally specified in their options/constructors.

Simply adding a ! to the title (feat!: ...) will mark this change as breaking, which is fine since this is a <1.0 SDK.

include OpenFeature::SDK::Provider::EventHandler

define_method :init do |_evaluation_context|
sleep(0.05) # Simulate some initialization time

Choose a reason for hiding this comment

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

minor: small sleeps in tests add up rather quickly. It's best if we can avoid sleeps and instead use async communication or time mocking.

Comment on lines +10 to +16
def attach(event_dispatcher)
@event_dispatcher = event_dispatcher
end

def detach
@event_dispatcher = nil
end

Choose a reason for hiding this comment

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

minor: attach/detach seem like internal methods (we don't expect users calling them) — shall we make them private or mark them internal?

|| [Logging](#logging) | Integrate with popular logging packages. |
|| [Domains](#domains) | Logically bind clients with providers. |
| | [Eventing](#eventing) | React to state changes in the provider or flag management system. |
| | [Eventing](#eventing) | React to state changes in the provider or flag management system. |

Choose a reason for hiding this comment

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

major(spec): one thing that seems to be missing is client-level event handlers. The user shall be able to attach event handler to specific clients instead of globally.

Copy link
Member

@toddbaert toddbaert Dec 16, 2025

Choose a reason for hiding this comment

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

Correct: https://openfeature.dev/specification/sections/events#requirement-521

A corollary to this is "domain scoping" - a client bound to domain x should not get events originating from a provider from domain y. Additionally, late binding to domains should be observed. For example, initially a client bound to domain x will use the default provider, but if a new provider is bound to domain x, that becomes the provider from which the client sources its events.

See 5.1.2 and 5.1.3

Comment on lines +41 to +43
def add_handler(event_type, handler)
@event_emitter.add_handler(event_type, handler)
end

Choose a reason for hiding this comment

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

major(spec):

Events | OpenFeature:

Handlers attached after the provider is already in the associated state, MUST run immediately.

Copy link
Member

Choose a reason for hiding this comment

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

Ya I noticed this as well due to a skipped test case: #207 (comment)

end
rescue => e
dispatch_provider_event(provider, ProviderEvent::PROVIDER_ERROR,
error_code: Provider::ErrorCode::PROVIDER_FATAL,

Choose a reason for hiding this comment

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

major: PROVIDER_FATAL seems too harsh as it signal irrecoverable error. The provider might still be trying to initialize and emit PROVIDER_READY later, so non-fatal error is a safer choice.

Comment on lines +90 to +96
if wait_for_init
init_provider(provider, context_for_init, raise_on_error: true)
else
Thread.new do
init_provider(provider, context_for_init, raise_on_error: false)
end
end

Choose a reason for hiding this comment

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

minor: does it make sense to always initialize provider in a new thread and for set_provider_and_wait to wait for PROVIDER_READY event? Otherwise, set_provider_and_wait does not wait for async-initializing providers (which is its major reasons to exist).

Comment on lines +109 to +116
unless provider.is_a?(Provider::EventHandler)
dispatch_provider_event(provider, ProviderEvent::PROVIDER_READY)
end
rescue => e
dispatch_provider_event(provider, ProviderEvent::PROVIDER_ERROR,
error_code: Provider::ErrorCode::PROVIDER_FATAL,
message: e.message)

Copy link
Member

@toddbaert toddbaert Dec 16, 2025

Choose a reason for hiding this comment

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

The emission of the READY/ERROR events should occur regardless of whether or not the provider is capable of emitting its own events.

The SDK should always emit either an init or error depending on the normal/abnormal termination of the provider's init method. If the provider has no init method, the SDK should just emit READY at the same point the method would have been called.

This is outlined here in section 5.3.

A bit more background:

The provider can "spontaneously" emit events (for instance, an error event if a persistent connection is lost) but the SDK emits events that correspond to the provider lifecycle and actions the application integrator has initiated (such as the READY event expected after a provider is set and initialized).

Choose a reason for hiding this comment

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

How should this work with asynchronous provider initialization?

I'm not too familiar with Ruby concurrency model, so sorry if my question does not make sense. My understanding is that set_provider should not block the thread (even if initialization takes a while) but set_provider_and_wait should. In languages that have promises, the SDK would wait for the initialization promise to resolve before emitting event, which may be much later that initialization function return.

Ruby doesn't have promises, so the options I see are:

  1. wait for provider event before declaring it ready
  2. let the provider block in initialization method
  3. the current PR is a mix of the two, depending on whether provider is able to emit events

From what I gathered, the second option is preferred because we can't rule out a method blocking the thread and blocking seems very common in Ruby, so it's likely that many providers already implement it this way. I think it makes sense to ask initialize to always block before it's ready/error and always emit events after initialize returns. Now, if I'm correct that we want set_provider to be non-blocking, this means that we need to initialize provider in a new Thread, which seems suboptimal but it's not too bad.

Does this match your expectations?

Comment on lines +214 to +221
context "Requirement 5.3.3" do
specify "Handlers attached after the provider is already in the associated state, MUST run immediately" do
# NOTE: This requirement is about handlers running immediately when attached after a provider
# is already in the associated state (e.g., READY). This feature is not yet implemented
# in the Ruby SDK. Handlers are only triggered by state transitions, not by current state.
skip "Immediate handler execution for current state not yet implemented"
end
end
Copy link
Member

@toddbaert toddbaert Dec 16, 2025

Choose a reason for hiding this comment

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

I think we need to implement this as well before a release. If this PR is getting too big, I don't mind if it gets done in a separate one, but I don't want it to linger un-implemented too long.

@toddbaert
Copy link
Member

@sameerank excellent work, this is great progress.

I've left some feedback. The main things missing that I can see are around automatic event emission at provider init, and missing client event handlers (as well as the associated domain scoping, which can be tricky to implement).

Please check the Java and JS SDKs for tests and implementation around these, as they may be helpful.

Thanks so much for your efforts! ❤️

@toddbaert
Copy link
Member

@dd-oleksii Thanks for your detailed reviews as well 🙏

@toddbaert toddbaert changed the title feat: add provider eventing fea!t: add provider eventing Dec 16, 2025
@toddbaert toddbaert changed the title fea!t: add provider eventing feat!: add provider eventing Dec 16, 2025
@toddbaert toddbaert changed the title feat!: add provider eventing feat!: add provider eventing, remove setProvider timeout Dec 16, 2025
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.

Implement events

5 participants