Skip to content

Commit d59c537

Browse files
committed
Add in flexible rollout strategy
1 parent 3b95bb1 commit d59c537

File tree

4 files changed

+185
-0
lines changed

4 files changed

+185
-0
lines changed

lib/murmur-1.0.0.jar

7.16 KB
Binary file not shown.
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
component implements="IStrategy" singleton {
2+
3+
property name="murmur3" inject="java:com.sangupta.murmur.Murmur3";
4+
5+
public boolean function isEnabled(
6+
required struct parameters,
7+
required struct context
8+
) {
9+
param arguments.parameters.stickiness = "default";
10+
var stickinessId = resolveStickiness( arguments.parameters.stickiness, arguments.context );
11+
if ( !len( stickinessId ) ) {
12+
return false;
13+
}
14+
param arguments.parameters.rollout = 100;
15+
var percentage = getPercentage( arguments.parameters.rollout );
16+
param arguments.parameters.groupId = "";
17+
var norm = getNormalizedNumber( stickinessId, arguments.parameters.groupId )
18+
return norm <= percentage;
19+
}
20+
21+
private string function resolveStickiness( required string stickinessKey, required struct context ) {
22+
switch ( arguments.stickinessKey ) {
23+
case "userId":
24+
return arguments.context.userId;
25+
case "sessionId":
26+
return arguments.context.sessionId
27+
case "random":
28+
return randRange( 1, 100 );
29+
case "default":
30+
default:
31+
if ( arguments.context.keyExists( "userId" ) && len( arguments.context.userId ) ) {
32+
return arguments.context.userId;
33+
}
34+
if ( arguments.context.keyExists( "sessionId" ) && len( arguments.context.sessionId ) ) {
35+
return arguments.context.sessionId;
36+
}
37+
return randRange( 1, 100 );
38+
}
39+
}
40+
41+
/**
42+
* Takes a numeric string value and converts it to a integer between 0 and 100.
43+
*
44+
* returns 0 if the string is not numeric.
45+
*
46+
* @param percentage - A numeric string value
47+
* @return a integer between 0 and 100
48+
*/
49+
private numeric function getPercentage( required any percentage ) {
50+
return clamp( 0, isNumeric( arguments.percentage ) ? arguments.percentage : 0, 100 );
51+
}
52+
53+
private numeric function clamp( required numeric low, required numeric actual, required numeric high ) {
54+
if ( arguments.actual < arguments.low ) {
55+
return arguments.low;
56+
}
57+
if ( arguments.actual > arguments.high ) {
58+
return arguments.high;
59+
}
60+
return arguments.actual;
61+
}
62+
63+
private numeric function getNormalizedNumber( required string identifier, required string groupId, numeric normalizer = 100 ) {
64+
var value = getStringBytes( "#arguments.groupId#:#arguments.identifier#" );
65+
var hash = calculateHash( value );
66+
return normalizeHash( hash, normalizer );
67+
}
68+
69+
private array function getStringBytes( required string identifier ) {
70+
return createObject( "java", "java.lang.String" ).init( arguments.identifier ).getBytes();
71+
}
72+
73+
private any function calculateHash( required array bytes ) {
74+
var hash = variables.murmur3.hash_x86_32( arguments.bytes, len( arguments.bytes ), 0 );
75+
return createObject( "java", "java.math.BigInteger" ).valueOf( hash );
76+
}
77+
78+
private numeric function normalizeHash( required any hash, required numeric normalizer ) {
79+
var normalizerBigInt = createObject( "java", "java.math.BigInteger" ).init( normalizer )
80+
return arguments.hash.remainder( normalizerBigInt ) + 1;
81+
}
82+
83+
}

tests/Application.cfc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ component {
1616
this.mappings[ "/coldbox" ] = testsPath & "resources/app/coldbox";
1717
this.mappings[ "/testbox" ] = rootPath & "/testbox";
1818

19+
this.javaSettings = {
20+
loadPaths = [
21+
expandPath( "../lib" )
22+
],
23+
loadColdFusionClassPath = true,
24+
reloadOnChange = false
25+
};
26+
1927
function onRequestStart() {
2028
setting requestTimeout="180";
2129
structDelete( application, "cbController" );
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
component extends="tests.resources.ModuleIntegrationSpec" {
2+
3+
property name="cache" inject="cachebox:default";
4+
5+
function beforeAll() {
6+
super.beforeAll();
7+
variables.strategy = getInstance( "FlexibleRolloutStrategy@unleashsdk" );
8+
}
9+
10+
function run() {
11+
describe( "FlexibleRolloutStrategy", function() {
12+
it( "always is enabled for 100 percent rollouts", function() {
13+
var result = variables.strategy.isEnabled(
14+
parameters = {
15+
"rollout": "100",
16+
"stickiness": "default",
17+
"groupId": "Feature.flexibleRollout.100"
18+
},
19+
context = {}
20+
);
21+
expect( result ).toBeTrue();
22+
} );
23+
24+
it( "never is enabled for 0 percent rollouts", function() {
25+
var result = variables.strategy.isEnabled(
26+
parameters = {
27+
"rollout": "0",
28+
"stickiness": "default",
29+
"groupId": "Feature.flexibleRollout.0"
30+
},
31+
context = {
32+
"sessionId": "147",
33+
"userId": "12"
34+
}
35+
);
36+
expect( result ).toBeFalse();
37+
} );
38+
39+
it( "should be enabled for userId=174 in rollout of 10", function() {
40+
var result = variables.strategy.isEnabled(
41+
parameters = {
42+
"rollout": "10",
43+
"stickiness": "default",
44+
"groupId": "Feature.flexibleRollout.10"
45+
},
46+
context = getTestContext( {
47+
"userId": "174"
48+
} )
49+
);
50+
expect( result ).toBeTrue();
51+
} );
52+
53+
it( "should be disabled for userId=499 in rollout of 10", function() {
54+
var result = variables.strategy.isEnabled(
55+
parameters = {
56+
"rollout": "10",
57+
"stickiness": "default",
58+
"groupId": "Feature.flexibleRollout.10"
59+
},
60+
context = getTestContext( {
61+
"userId": "499"
62+
} )
63+
);
64+
expect( result ).toBeFalse();
65+
} );
66+
67+
it( "should be disabled for sessionId=25 for a userId specific version", function() {
68+
var result = variables.strategy.isEnabled(
69+
parameters = {
70+
"rollout": "55",
71+
"stickiness": "userId",
72+
"groupId": "Feature.flexibleRollout.userId.55"
73+
},
74+
context = getTestContext( {
75+
"sessionId": "25"
76+
} )
77+
);
78+
expect( result ).toBeFalse();
79+
} );
80+
} );
81+
}
82+
83+
function getTestContext( struct overrides = {} ) {
84+
structAppend( arguments.overrides, {
85+
"appName": "unleashsdk-tests",
86+
"environment": "testing",
87+
"userId": "",
88+
"sessionId": "",
89+
"remoteAddress": CGI.REMOTE_ADDR
90+
}, false );
91+
return arguments.overrides;
92+
}
93+
94+
}

0 commit comments

Comments
 (0)