Skip to content

feat: improve MultiProvider parity with JS SDK reference#113

Draft
jonathannorris wants to merge 4 commits into
mainfrom
feat/multi-provider-parity
Draft

feat: improve MultiProvider parity with JS SDK reference#113
jonathannorris wants to merge 4 commits into
mainfrom
feat/multi-provider-parity

Conversation

@jonathannorris
Copy link
Copy Markdown
Member

Summary

  • Implement allSettled-style initialization and context change: all child providers run to completion before throwing a MultiProviderAggregateError, matching the JS SDK's Promise.allSettled pattern
  • Add ComparisonStrategy for evaluating all providers and comparing results, with fallback provider and mismatch callback
  • Add per-provider hook execution with context isolation via @TaskLocal-based ProviderHookExecutionContext, propagating client metadata and invocation hints
  • Replace DispatchQueue.sync with NSLock to avoid cooperative thread pool starvation in async task groups
  • Extract shared makeUniqueProviderNames utility, removing duplication between MultiProvider and ComparisonStrategy

Implementation

File Change
MultiProvider.swift allSettled init/context pattern, NSLock, MultiProviderAggregateError, shared name dedup, UpperCamelCase types
comparisonStrategy.swift New ComparisonStrategy, uses shared makeUniqueProviderNames, identity-only provider matching
providerHookExecutionContext.swift New @TaskLocal storage for client metadata and hints during provider hook execution
OpenFeatureClient.swift Propagate client metadata and hook hints via ProviderHookExecutionContextStorage
MultiProviderTests.swift 17 tests covering all strategies, hooks, events, tracking, aggregate errors, and name dedup

Notes

  • ComparisonStrategy evaluates providers sequentially because the Strategy protocol is synchronous. The JS SDK uses parallel evaluation (runMode = 'parallel'). Parallel evaluation would require making the protocol async.
  • ProviderHookExecutionContext is only populated when evaluations go through OpenFeatureClient. Direct MultiProvider usage will have nil client metadata — provider hooks still run but without client context.

Related Issues

Closes #109
Supersedes #110

- Replace fail-fast initialization with allSettled pattern: initialize and
  onContextSet now run all child providers to completion before throwing a
  MultiProviderAggregateError, matching the JS SDK's Promise.allSettled behavior
- Replace DispatchQueue.sync with NSLock to avoid cooperative thread pool
  starvation when called from async task groups
- Extract shared makeUniqueProviderNames utility, removing duplicate in
  ComparisonStrategy
- Add ComparisonStrategy for evaluating all providers and comparing results,
  with fallback provider and mismatch callback support
- Add per-provider hook execution with context isolation via TaskLocal-based
  ProviderHookExecutionContext, propagating client metadata and hints
- Rename types to UpperCamelCase (ChildProviderRecord, ContextMutatingHook,
  HookContextCapturingHook, NamedProviderMetadata)
- Remove fragile type+name fallback in ComparisonStrategy.providersMatch,
  using only reference identity

Closes #109

Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, 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 significantly enhances the MultiProvider functionality to achieve greater parity with the JavaScript SDK. It introduces robust error handling during initialization and context changes, allowing all providers to attempt their operations before reporting aggregated failures. A new comparison strategy provides advanced evaluation capabilities, and a refined hook execution mechanism ensures proper context isolation for individual providers. These changes collectively improve the reliability, flexibility, and consistency of the OpenFeature Swift SDK's multi-provider capabilities.

Highlights

  • Enhanced MultiProvider Initialization and Context Changes: Implemented an 'allSettled'-style initialization and context change mechanism for MultiProvider, ensuring all child providers complete their operations before aggregating and throwing a MultiProviderAggregateError if any fail, mirroring the JavaScript SDK's Promise.allSettled behavior.
  • Introduced Comparison Strategy: Added a new ComparisonStrategy that evaluates all registered providers and compares their results. In cases of disagreement, it returns the result from a designated fallback provider and invokes an onMismatch callback.
  • Per-Provider Hook Execution with Context Isolation: Enabled per-provider hook execution with isolated contexts using @TaskLocal-based ProviderHookExecutionContext. This propagates client metadata and invocation hints to individual provider hooks, ensuring proper context for each evaluation.
  • Improved Concurrency Handling: Replaced DispatchQueue.sync with NSLock to prevent cooperative thread pool starvation in asynchronous task groups, enhancing the stability and performance of concurrent operations.
  • Utility for Unique Provider Naming: Extracted a shared makeUniqueProviderNames utility to deduplicate provider names by appending numerical suffixes (e.g., '-1', '-2') when collisions occur, ensuring consistent naming across MultiProvider and ComparisonStrategy.
Changelog
  • Sources/OpenFeature/OpenFeatureClient.swift
    • Updated flag resolution to propagate hook hints and client metadata using a new execution context.
  • Sources/OpenFeature/Provider/MultiProvider/MultiProvider.swift
    • Refactored MultiProvider to support allSettled-style initialization and context changes, introducing aggregate error handling.
    • Added per-provider hook execution with context isolation.
    • Implemented provider event observation and status aggregation.
    • Deduplicated provider names for better management.
  • Sources/OpenFeature/Provider/MultiProvider/comparisonStrategy.swift
    • Added ComparisonStrategy for evaluating multiple providers, comparing their results, and handling mismatches with a fallback provider and callback.
  • Sources/OpenFeature/providerHookExecutionContext.swift
    • Introduced ProviderHookExecutionContext and ProviderHookExecutionContextStorage to manage client metadata and hook hints using TaskLocal for context isolation.
  • Tests/OpenFeatureTests/MultiProviderTests.swift
    • Expanded test suite for MultiProvider, covering new features like event observation, hook execution, tracking, error handling, comparison strategy, and provider name deduplication.
Activity
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
Copy Markdown

@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 significantly enhances the MultiProvider by aligning it with the JS SDK. The changes include allSettled-style error handling for initialization and context changes, a new ComparisonStrategy, and improved hook execution with context propagation. The implementation is solid and well-tested. I've provided a few suggestions to refine the code for conciseness and clarity.

Comment on lines +107 to +113
var collected: [(String, Error?)] = []
for await result in group {
collected.append(result)
}
return collected.compactMap { name, error in
error.map { (providerName: name, error: $0) }
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The error collection logic can be made more concise and efficient by using a for await loop with pattern matching, which avoids creating an intermediate array collected that includes non-error results.

Suggested change
var collected: [(String, Error?)] = []
for await result in group {
collected.append(result)
}
return collected.compactMap { name, error in
error.map { (providerName: name, error: $0) }
}
var errors: [(providerName: String, error: Error)] = []
for await (name, error) in group {
if let error {
errors.append((providerName: name, error: error))
}
}
return errors

Comment on lines +150 to +156
var collected: [(String, Error?)] = []
for await result in group {
collected.append(result)
}
return collected.compactMap { name, error in
error.map { (providerName: name, error: $0) }
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Similar to the initialize method, the error collection logic here can be simplified to be more concise and avoid creating an intermediate array.

Suggested change
var collected: [(String, Error?)] = []
for await result in group {
collected.append(result)
}
return collected.compactMap { name, error in
error.map { (providerName: name, error: $0) }
}
var errors: [(providerName: String, error: Error)] = []
for await (name, error) in group {
if let error {
errors.append((providerName: name, error: error))
}
}
return errors

return fallbackResolution
}

return firstResolution ?? fallbackResolution
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The ?? fallbackResolution is redundant. firstResolution is guaranteed to be non-nil at this point because:

  1. The function returns early if providers is empty.
  2. If providers is not empty, firstResolution is set on the first successful evaluation.
  3. If all evaluations fail with an error, encounteredError will be true, and the function will throw ComparisonStrategyError.providerErrors, so this line is not reached.

Force-unwrapping is safe here and makes the intent clearer that firstResolution is expected to exist.

Suggested change
return firstResolution ?? fallbackResolution
return firstResolution!

…ength lint

Move event/status tracking, hook execution, and logger-enabled evaluation
methods into separate extension files to keep each file under the 350-line
type_body_length threshold.

Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
…lint

Move event aggregation and status tracking into a standalone
MultiProviderStatusTracker class (matching JS SDK's StatusTracker pattern),
reducing the MultiProvider type body to ~332 lines (under the 350-line
error threshold).

Also simplify error collection in initialize/onContextSet using direct
for-await pattern, and consolidate evaluation methods with a shared
evaluateStrategy helper.

Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
…h lint

The swiftlint error was in the test file (MultiProviderTests at 488 lines),
not the source. Split into MultiProviderStrategyTests (evaluation/strategy
tests) and MultiProviderFeatureTests (events, hooks, tracking, lifecycle).

Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
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.

[Multi-provider] Gaps identified relative to js-sdk reference implementation

1 participant