Skip to content

Commit 54a5ad5

Browse files
committed
Implementation of Evaluate (percent condition only) (#684)
Add types for server template; implementation of evaluate and percent condition operator
1 parent 98eccf1 commit 54a5ad5

File tree

5 files changed

+1027
-55
lines changed

5 files changed

+1027
-55
lines changed

remoteconfig/condition_evaluator.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// Copyright 2025 Google Inc. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package remoteconfig
16+
17+
import (
18+
"crypto/sha256"
19+
"fmt"
20+
"log"
21+
"math/big"
22+
)
23+
24+
type conditionEvaluator struct {
25+
evaluationContext map[string]any
26+
conditions []namedCondition
27+
}
28+
29+
const (
30+
maxConditionRecursionDepth = 10
31+
randomizationID = "randomizationID"
32+
rootNestingLevel = 0
33+
totalMicroPercentiles = 100_000_000
34+
)
35+
36+
const (
37+
lessThanOrEqual = "LESS_OR_EQUAL"
38+
greaterThan = "GREATER_THAN"
39+
between = "BETWEEN"
40+
)
41+
42+
func (ce *conditionEvaluator) evaluateConditions() map[string]bool {
43+
evaluatedConditions := make(map[string]bool)
44+
for _, condition := range ce.conditions {
45+
evaluatedConditions[condition.Name] = ce.evaluateCondition(condition.Condition, rootNestingLevel)
46+
}
47+
return evaluatedConditions
48+
}
49+
50+
func (ce *conditionEvaluator) evaluateCondition(condition *oneOfCondition, nestingLevel int) bool {
51+
if nestingLevel >= maxConditionRecursionDepth {
52+
log.Println("Maximum recursion depth is exceeded.")
53+
return false
54+
}
55+
56+
if condition.Boolean != nil {
57+
return *condition.Boolean
58+
} else if condition.OrCondition != nil {
59+
return ce.evaluateOrCondition(condition.OrCondition, nestingLevel+1)
60+
} else if condition.AndCondition != nil {
61+
return ce.evaluateAndCondition(condition.AndCondition, nestingLevel+1)
62+
} else if condition.Percent != nil {
63+
return ce.evaluatePercentCondition(condition.Percent)
64+
}
65+
log.Println("Unknown condition type encountered.")
66+
return false
67+
}
68+
69+
func (ce *conditionEvaluator) evaluateOrCondition(orCondition *orCondition, nestingLevel int) bool {
70+
for _, condition := range orCondition.Conditions {
71+
result := ce.evaluateCondition(&condition, nestingLevel+1)
72+
// short-circuit evaluation, return true if any of the conditions return true
73+
if result {
74+
return true
75+
}
76+
}
77+
return false
78+
}
79+
80+
func (ce *conditionEvaluator) evaluateAndCondition(andCondition *andCondition, nestingLevel int) bool {
81+
for _, condition := range andCondition.Conditions {
82+
result := ce.evaluateCondition(&condition, nestingLevel+1)
83+
// short-circuit evaluation, return false if any of the conditions return false
84+
if !result {
85+
return false
86+
}
87+
}
88+
return true
89+
}
90+
91+
func (ce *conditionEvaluator) evaluatePercentCondition(percentCondition *percentCondition) bool {
92+
if rid, ok := ce.evaluationContext[randomizationID].(string); ok {
93+
if percentCondition.PercentOperator == "" {
94+
log.Println("Missing percent operator for percent condition.")
95+
return false
96+
}
97+
instanceMicroPercentile := computeInstanceMicroPercentile(percentCondition.Seed, rid)
98+
switch percentCondition.PercentOperator {
99+
case lessThanOrEqual:
100+
return instanceMicroPercentile <= percentCondition.MicroPercent
101+
case greaterThan:
102+
return instanceMicroPercentile > percentCondition.MicroPercent
103+
case between:
104+
return instanceMicroPercentile > percentCondition.MicroPercentRange.MicroPercentLowerBound && instanceMicroPercentile <= percentCondition.MicroPercentRange.MicroPercentUpperBound
105+
default:
106+
log.Printf("Unknown percent operator: %s\n", percentCondition.PercentOperator)
107+
return false
108+
}
109+
}
110+
log.Println("Missing or invalid randomizationID (requires a string value) for percent condition.")
111+
return false
112+
}
113+
114+
func computeInstanceMicroPercentile(seed string, randomizationID string) uint32 {
115+
seedPrefix := ""
116+
if len(seed) > 0 {
117+
seedPrefix = fmt.Sprintf("%s.", seed)
118+
}
119+
stringToHash := fmt.Sprintf("%s%s", seedPrefix, randomizationID)
120+
121+
hash := sha256.New()
122+
hash.Write([]byte(stringToHash))
123+
// Calculate the final SHA-256 hash as a byte slice (32 bytes).
124+
hashBytes := hash.Sum(nil)
125+
126+
hashBigInt := new(big.Int).SetBytes(hashBytes)
127+
// Convert the hash bytes to a big.Int. The "0x" prefix is implicit in the conversion from hex to big.Int.
128+
instanceMicroPercentileBigInt := new(big.Int).Mod(hashBigInt, big.NewInt(totalMicroPercentiles))
129+
// Can safely convert to uint32 since the range of instanceMicroPercentile is 0 to 100_000_000; range of uint32 is 0 to 4_294_967_295.
130+
return uint32(instanceMicroPercentileBigInt.Int64())
131+
}

0 commit comments

Comments
 (0)