Skip to content

Conversation

suvaidkhan
Copy link

This PR

  • Added Multiprovider class
  • Added Strategies
  • Added a new dependency for JSON

Related Issues

Resolves #1486

Follow-up Tasks

Multiprovider should be removed from the contrib codebase

@suvaidkhan suvaidkhan requested review from a team as code owners July 1, 2025 07:43
@suvaidkhan suvaidkhan force-pushed the suvaidkhan/add-multiprovider-support branch from 8710a52 to f07dd25 Compare July 1, 2025 07:46
Copy link
Contributor

@chrfwow chrfwow left a comment

Choose a reason for hiding this comment

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

Looks good so far!
I'm not sure if we want to add a json dependecy (which might also produce the License Compliance error) "just" so that we can build up the metadata name string.

private String metadataName;

/**
* Constructs a MultiProvider with the given list of FeatureProviders, using a default strategy.
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the comment should say what the default strategy is

json.put("name", NAME);
JSONObject providersMetadata = new JSONObject();
json.put("originalMetadata", providersMetadata);
ExecutorService initPool = Executors.newFixedThreadPool(INIT_THREADS_COUNT);
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
ExecutorService initPool = Executors.newFixedThreadPool(INIT_THREADS_COUNT);
ExecutorService initPool = Executors.newFixedThreadPool(Math.min(INIT_THREADS_COUNT, providers.size()));

throw new GeneralError("init failed");
}
}
metadataName = json.toString();
Copy link
Contributor

Choose a reason for hiding this comment

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

If the executer service is unused from this point onwards, we should shut it down

assertThrows(FlagNotFoundError.class, () -> finalMultiProvider1.getStringEvaluation("non-existing", "", null));

multiProvider.shutdown();
multiProvider = new MultiProvider(providers, new FirstSuccessfulStrategy());
Copy link
Contributor

Choose a reason for hiding this comment

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

There should probably be a test file with tests for each strategy. If you want to reduce code duplication by the flag setup, make those test classes extend some common test setup class

Copy link
Author

Choose a reason for hiding this comment

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

have created a BaseStrategyTest for the setup which is extended by the separate test classes for each strategy

Copy link

codecov bot commented Jul 1, 2025

Codecov Report

❌ Patch coverage is 96.38554% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.45%. Comparing base (e67f598) to head (84bc45a).
⚠️ Report is 85 commits behind head on main.

Files with missing lines Patch % Lines
...v/openfeature/sdk/multiprovider/MultiProvider.java 94.64% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main    #1500      +/-   ##
============================================
+ Coverage     92.97%   93.45%   +0.47%     
- Complexity      488      519      +31     
============================================
  Files            46       50       +4     
  Lines          1182     1253      +71     
  Branches        103      112       +9     
============================================
+ Hits           1099     1171      +72     
+ Misses           53       51       -2     
- Partials         30       31       +1     
Flag Coverage Δ
unittests 93.45% <96.38%> (+0.47%) ⬆️

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

☔ 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.

@suvaidkhan
Copy link
Author

Looks good so far! I'm not sure if we want to add a json dependecy (which might also produce the License Compliance error) "just" so that we can build up the metadata name string.

Should I construct the json using Strings?

@chrfwow
Copy link
Contributor

chrfwow commented Jul 3, 2025

Maybe it would be worth it.
@toddbaert what do you think?

@suvaidkhan
Copy link
Author

@chrfwow @toddbaert any updates on this?

Copy link
Member

@aepfli aepfli left a comment

Choose a reason for hiding this comment

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

Thank you, great effort. I am looking forward to having the multiprovider migrated. For me, the added dependency is currently a show stopper, because I think we can handle that with proper and simple data classes, even with better metadata support overall (not losing information from the sub providers). I added simple but untested code snippets to my review. Please let me know what you think about this approach.

@suvaidkhan
Copy link
Author

Thank you, great effort. I am looking forward to having the multiprovider migrated. For me, the added dependency is currently a show stopper, because I think we can handle that with proper and simple data classes, even with better metadata support overall (not losing information from the sub providers). I added simple but untested code snippets to my review. Please let me know what you think about this approach.

Looks good to me.
I'll add these changes and some UTs to make sure it's working correctly.

@suvaidkhan suvaidkhan force-pushed the suvaidkhan/add-multiprovider-support branch from 97f6dc9 to 1103949 Compare July 17, 2025 00:07
Copy link
Contributor

@chrfwow chrfwow left a comment

Choose a reason for hiding this comment

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

Looks good

@suvaidkhan suvaidkhan requested review from aepfli and chrfwow July 18, 2025 23:24
@suvaidkhan
Copy link
Author

Hey @aepfli @chrfwow this is ready for review

Copy link
Member

@aepfli aepfli left a comment

Choose a reason for hiding this comment

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

Thank you, the basic implementation looks good to me. But i took a closer look at the two strategies. The strategies seem to be a little inconsistent in exception handling, logging and returning. I think we should normalize this a little for consistency (no need for more abstraction, but it should log the same way, it should handle all the possible exceptions the same way, etc).

@suvaidkhan suvaidkhan force-pushed the suvaidkhan/add-multiprovider-support branch from ff27ce8 to fc8c18f Compare August 11, 2025 17:14
@suvaidkhan suvaidkhan force-pushed the suvaidkhan/add-multiprovider-support branch from fc8c18f to 84bc45a Compare August 11, 2025 17:27
Copy link

Copy link
Member

@aepfli aepfli left a comment

Choose a reason for hiding this comment

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

Sorry for the delay @suvaidkhan - i was on vacation - thank you for incoporating all the changes, i will give other maintainers the chance to also take a look for 2 more days, and else i will merge it.

Thank you this contribution is highly appreciated!

@aepfli
Copy link
Member

aepfli commented Aug 26, 2025

/gemini review

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 multi-provider support, a significant and valuable feature. The implementation includes different provider evaluation strategies and a MultiProvider class to manage them. The code is well-structured and includes a good set of tests. I've identified a few issues, including a critical one in the initialize method related to resource handling and edge cases, as well as some medium-severity items around error reporting and immutability. I've provided detailed suggestions to address these points. Overall, this is a great addition, and with these fixes, it will be even more robust.

Comment on lines +80 to +104
public void initialize(EvaluationContext evaluationContext) throws Exception {
var metadataBuilder = MultiProviderMetadata.builder();
metadataBuilder.name(NAME);
HashMap<String, Metadata> providersMetadata = new HashMap<>();
ExecutorService executorService = Executors.newFixedThreadPool(Math.min(INIT_THREADS_COUNT, providers.size()));
Collection<Callable<Boolean>> tasks = new ArrayList<>(providers.size());
for (FeatureProvider provider : providers.values()) {
tasks.add(() -> {
provider.initialize(evaluationContext);
return true;
});
Metadata providerMetadata = provider.getMetadata();
providersMetadata.put(providerMetadata.getName(), providerMetadata);
}
metadataBuilder.originalMetadata(providersMetadata);
List<Future<Boolean>> results = executorService.invokeAll(tasks);
for (Future<Boolean> result : results) {
if (!result.get()) {
executorService.shutdown();
throw new GeneralError("init failed");
}
}
executorService.shutdown();
metadata = metadataBuilder.build();
}

Choose a reason for hiding this comment

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

critical

The initialize method has a few critical issues:

  1. Crash on empty provider list: If MultiProvider is created with an empty list of providers, providers.size() will be 0, and Executors.newFixedThreadPool(0) will throw an IllegalArgumentException.
  2. Resource leak: If any provider's initialize method throws an exception, the ExecutorService is not shut down, leading to a resource leak.
  3. Unreachable code: The if (!result.get()) block is unreachable because result.get() will either return true or throw an exception.
  4. Mutable state: The providersMetadata map is passed to the builder without being made unmodifiable, which could lead to modification of internal state.

I suggest refactoring the method to handle these issues.

    public void initialize(EvaluationContext evaluationContext) throws Exception {
        var metadataBuilder = MultiProviderMetadata.builder().name(NAME);
        HashMap<String, Metadata> providersMetadata = new HashMap<>();

        if (providers.isEmpty()) {
            metadataBuilder.originalMetadata(providersMetadata);
            metadata = metadataBuilder.build();
            return;
        }

        ExecutorService executorService =
                Executors.newFixedThreadPool(Math.min(INIT_THREADS_COUNT, providers.size()));
        try {
            Collection<Callable<Void>> tasks = new ArrayList<>(providers.size());
            for (FeatureProvider provider : providers.values()) {
                tasks.add(() -> {
                    provider.initialize(evaluationContext);
                    return null;
                });
                Metadata providerMetadata = provider.getMetadata();
                providersMetadata.put(providerMetadata.getName(), providerMetadata);
            }
            // Make the metadata map unmodifiable to protect internal state.
            metadataBuilder.originalMetadata(Collections.unmodifiableMap(providersMetadata));
            List<Future<Void>> results = executorService.invokeAll(tasks);
            for (Future<Void> result : results) {
                // This will re-throw any exception from the provider's initialize method,
                // wrapped in an ExecutionException.
                result.get();
            }
        } finally {
            executorService.shutdown();
        }
        metadata = metadataBuilder.build();
    }

Comment on lines +52 to +56
return ProviderEvaluation.<T>builder()
.errorMessage("No provider successfully responded")
.errorCode(ErrorCode.GENERAL)
.build();
}

Choose a reason for hiding this comment

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

medium

When all providers return FLAG_NOT_FOUND, the strategy currently returns a GENERAL error. It would be more accurate to return a FLAG_NOT_FOUND error code to reflect that the flag was not found in any of the providers. The error message could also be more specific.

Note that this change will require updating FirstMatchStrategyTest.shouldThrowFlagNotFoundWhenAllProvidersReturnFlagNotFound and MultiProviderTest.shouldUseDefaultFirstMatchStrategy to assert for FLAG_NOT_FOUND instead of GENERAL.

Suggested change
return ProviderEvaluation.<T>builder()
.errorMessage("No provider successfully responded")
.errorCode(ErrorCode.GENERAL)
.build();
}
return ProviderEvaluation.<T>builder()
.errorMessage("Flag not found in any provider")
.errorCode(ErrorCode.FLAG_NOT_FOUND)
.build();

Comment on lines +11 to +21
@Data
@Builder
public class MultiProviderMetadata implements Metadata {
String name;
Map<String, Metadata> originalMetadata;

@Override
public String getName() {
return name;
}
}

Choose a reason for hiding this comment

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

medium

The MultiProviderMetadata class is currently mutable due to Lombok's @Data annotation, which generates setters. This could allow external code to modify the metadata of the MultiProvider, which is generally undesirable. To ensure immutability, I recommend using @Value instead of @Data. @Value creates an immutable class with private final fields and getters. The explicit getName() override also becomes unnecessary as @Value will generate it.

@lombok.Value
@Builder
public class MultiProviderMetadata implements Metadata {
    String name;
    Map<String, Metadata> originalMetadata;
}

import lombok.SneakyThrows;
import org.junit.jupiter.api.Test;

class MultiProviderTest extends BaseStrategyTest {

Choose a reason for hiding this comment

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

medium

The test suite for MultiProvider is missing a test case for initialization with an empty list of providers. This is an important edge case that currently causes a crash in the submitted code. Please add a test to cover this scenario and ensure it's handled gracefully (i.e., it should not throw an exception).

Here is an example of what such a test could look like:

    @Test
    void shouldInitializeWithEmptyProvidersList() {
        MultiProvider multiProvider = new MultiProvider(new ArrayList<>());
        assertDoesNotThrow(() -> multiProvider.initialize(null));
        assertNotNull(multiProvider.getMetadata());
        assertEquals(MultiProvider.getNAME(), multiProvider.getMetadata().getName());
    }

@aepfli
Copy link
Member

aepfli commented Aug 26, 2025

okay @suvaidkhan - as we now have gemini support, i executed the gemini review, and i think the initialization issue seems to be critical. We do have two options here - first we add the feedback for gemini now, and wait for your implementation before merging, or second option, we are merging this, and create an issue to address the gemini feedback regarding initialization. wdyt?

@suvaidkhan
Copy link
Author

okay @suvaidkhan - as we now have gemini support, i executed the gemini review, and i think the initialization issue seems to be critical. We do have two options here - first we add the feedback for gemini now, and wait for your implementation before merging, or second option, we are merging this, and create an issue to address the gemini feedback regarding initialization. wdyt?

I'm fine with both
But I think it makes more sense to fix the issue before merging

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.

Move multi-provider into SDK
3 participants