Skip to content

Add fractional evaluation support#8

Merged
liamhughes merged 7 commits intomainfrom
liamhughes/devex-82-add-fractional-evaluation-support-to-java-client-library
Apr 7, 2026
Merged

Add fractional evaluation support#8
liamhughes merged 7 commits intomainfrom
liamhughes/devex-82-add-fractional-evaluation-support-to-java-client-library

Conversation

@liamhughes
Copy link
Copy Markdown
Contributor

@liamhughes liamhughes commented Apr 1, 2026

Background

We are implementing client-side percentage rollout evaluation to our three OpenFeature provider libraries.

The .NET provider had this added in OctopusDeploy/openfeature-provider-dotnet#45.

This PR adds this to the Java library.

Resolves https://linear.app/octopus/issue/DEVEX-82/add-fractional-evaluation-support-to-java-client-library

Changes

  • Added Apache Commons Codec for its Murmur3 hashing algorithm
  • Updated OctopusContext to have the same logical flow as the .NET equivalent, including implementing getNormalizeNumber to hash the evaluationKey + targetingKey for percentage rollout.
  • Extensive tests in OctopusContextTests replace existing tests and these now align with the .NET equivalents to ensure consistent evaluation.
  • Re-enabled the specification tests and updated submodule to latest.

Notes for review

  • Claude ported the tests over from .NET. I have checked them, but would be worth double checking them.
  • I have chosen Apache Commons Codec over Google's Guava for library size, given they both have good reputations. We could use lesser known targeted libraries or copy+paste an open source implementation of the algorithm directly into our repo, but this feels like a good balance to me.
  • I am not convinced of the Reason options chosen. It may be good to talk these through. Or perhaps ignore them until we do reasons properly and across our feature toggles ecosystem.

@liamhughes liamhughes changed the base branch from main to liamhughes/devex-136-use-new-evaluations-endpoint April 1, 2026 02:54
Base automatically changed from liamhughes/devex-136-use-new-evaluations-endpoint to main April 1, 2026 05:15
@liamhughes liamhughes force-pushed the liamhughes/devex-82-add-fractional-evaluation-support-to-java-client-library branch from 1cd8040 to 4f02169 Compare April 1, 2026 05:16
@liamhughes liamhughes force-pushed the liamhughes/devex-82-add-fractional-evaluation-support-to-java-client-library branch from 4f02169 to a53abd3 Compare April 2, 2026 00:29
@@ -26,10 +28,10 @@ byte[] getContentHash() {
}

ProviderEvaluation<Boolean> evaluate(String slug, Boolean defaultValue, EvaluationContext evaluationContext) {
Copy link
Copy Markdown
Contributor Author

@liamhughes liamhughes Apr 2, 2026

Choose a reason for hiding this comment

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

This method mostly matches the .NET equivalent logically, though with a different shape. I propose that we align them further prior to the next piece of work in this area.

if (rolloutPercentage < 100) {
return ProviderEvaluation.<Boolean>builder()
.value(false)
.reason(Reason.TARGETING_MATCH.toString())
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@liamhughes liamhughes changed the title Liamhughes/devex 82 add fractional evaluation support to java client library Add fractional evaluation support Apr 2, 2026
return null;
}
List<FeatureToggleEvaluation> evaluations = OctopusObjectMapper.INSTANCE.readValue(httpResponse.body(), new TypeReference<>(){});
var evaluations = OctopusObjectMapper.INSTANCE.readValue(httpResponse.body(), new TypeReference<List<FeatureToggleEvaluation>>(){});
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I lied in my last PR. Well, not that I lied, but I did think we were on Java 8 rather than 11.

@liamhughes liamhughes requested a review from Copilot April 2, 2026 04:30
@liamhughes liamhughes marked this pull request as ready for review April 2, 2026 04:31
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds client-side fractional (percentage) rollout evaluation to the Java OpenFeature provider, aligning behavior with the .NET provider and re-enabling the spec fixture suite.

Changes:

  • Introduced MurmurHash3-based bucketing (evaluationKey + targetingKey) to support percentage rollouts.
  • Updated evaluation flow in OctopusContext to incorporate rollout checks before segment matching.
  • Reworked/expanded tests (including re-enabling spec fixture tests) to validate cross-SDK-consistent bucketing and evaluation behavior.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/main/java/com/octopus/openfeature/provider/OctopusContext.java Implements normalized bucketing via MurmurHash3 and integrates rollout gating into flag evaluation flow.
src/test/java/com/octopus/openfeature/provider/OctopusContextTests.java Replaces prior tests with extensive rollout + segment evaluation cases and cross-SDK bucketing vectors.
src/test/java/com/octopus/openfeature/provider/SpecificationTests.java Re-enables parameterized specification fixture tests.
src/test/java/com/octopus/openfeature/provider/FeatureToggleEvaluationDeserializationTests.java Tightens TypeReference typing for toggle list deserialization.
src/main/java/com/octopus/openfeature/provider/OctopusClient.java Tightens TypeReference typing when deserializing toggle evaluations.
pom.xml Adds Apache Commons Codec dependency for MurmurHash3.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

// MurmurHash3 32-bit, seed 0. hash32x86 processes tail bytes in little-endian order,
// matching the reference C spec and equivalent to .NET's MurmurHash.Create32() +
// BinaryPrimitives.ReadUInt32LittleEndian().
int hash = MurmurHash3.hash32x86(bytes, 0, bytes.length, 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.

👍

);
var subject = new OctopusContext(toggles);
assertThrows(FlagNotFoundError.class, () -> subject.evaluate("This is clearly not a slug!", true, null));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Should we also assert here that it's resolving to the default value?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is a side effect of the Java library throwing exceptions instead of returning ProviderEvaluation: it is relying on the OpenFeature SDK to catch the exception and returning the default value.

As these are unit tests for OctopusContext I don't think we can assert the default value.

There is an equivalent specification test disabled when default value is false and slug is not a slug that I think we can lean on instead.

);
var subject = new OctopusContext(toggles);
assertThrows(FlagNotFoundError.class, () -> subject.evaluate("anotherfeature", true, null));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same question here :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same answer too. :)

var result = subject.evaluate("Enabled-Feature", false, null);
void whenTargetingKeyFallsWithinRolloutPercentage_AndFeatureIsNotToggledForSegments_ResolvesToTrue() {
// "evaluation-key:targeting-key" is known to hash to bucket 13
// rollout=13 → bucket(13) <= rollout(13) → within → true
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Maybe we could simplify this comment for the benefit our futures selves... 😜 E.g.

// "evaluation-key:targeting-key" is known to hash to bucket 13.
// Rollout is to 13%. Therefore bucket is <= rollout percentage.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

You could probably say the same for my comments on the .NET version too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I didn't love these either. Let me see if I can improve.

// implementations in other Octopus OpenFeature provider libraries (e.g. .NET). The expected values
// are derived from the reference MurmurHash3 little-endian algorithm and are duplicated verbatim
// across all libraries. DO NOT modify the input arguments or expected values — doing so would mask
// a real divergence in evaluation behaviour between libraries and defeat the purpose of this test.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

👍

Copy link
Copy Markdown

@caitlynstocker caitlynstocker left a comment

Choose a reason for hiding this comment

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

Have picked thought this and it all makes sense to me 😄 Has it been manually tested as well? If so, I've added a few tiny comments but otherwise I'm confident to approve it.

@liamhughes
Copy link
Copy Markdown
Contributor Author

Has it been manually tested as well?

@caitlynstocker, I have done a smoke test against a local instance of OctoToggle. That, combined with the specification tests, gives me reasonable confidence.

@liamhughes liamhughes merged commit 5353210 into main Apr 7, 2026
5 checks passed
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.

3 participants