Skip to content

Commit 16e0dcd

Browse files
committed
chore(spanner): add epsilon-greedy strategy to ReplicaSelector
Add an epsilon-greedy strategy to the standard ReplicaSelector to ensure that all endpoints are used from time to time. This ensures that new endpoints that show up are also used.
1 parent c29b99f commit 16e0dcd

File tree

2 files changed

+48
-3
lines changed

2 files changed

+48
-3
lines changed

java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/PowerOfTwoReplicaSelector.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.google.api.core.BetaApi;
2020
import com.google.api.core.InternalApi;
2121
import com.google.common.base.MoreObjects;
22+
import com.google.common.base.Preconditions;
2223
import java.util.List;
2324
import java.util.Random;
2425
import java.util.concurrent.ThreadLocalRandom;
@@ -29,6 +30,19 @@
2930
@BetaApi
3031
public class PowerOfTwoReplicaSelector implements ReplicaSelector {
3132

33+
public static final double DEFAULT_EPSILON = 0.1;
34+
35+
private final double epsilon;
36+
37+
public PowerOfTwoReplicaSelector() {
38+
this(DEFAULT_EPSILON);
39+
}
40+
41+
public PowerOfTwoReplicaSelector(double epsilon) {
42+
Preconditions.checkArgument(epsilon >= 0.0 && epsilon <= 1.0, "epsilon must be in [0, 1]");
43+
this.epsilon = epsilon;
44+
}
45+
3246
@Override
3347
public ChannelEndpoint select(
3448
List<ChannelEndpoint> candidates, Function<ChannelEndpoint, Double> scoreLookup) {
@@ -40,6 +54,11 @@ public ChannelEndpoint select(
4054
}
4155

4256
Random random = ThreadLocalRandom.current();
57+
58+
// Epsilon-greedy exploration: with probability epsilon, pick a random candidate.
59+
if (random.nextDouble() < epsilon) {
60+
return candidates.get(random.nextInt(candidates.size()));
61+
}
4362
int index1 = random.nextInt(candidates.size());
4463
int index2 = random.nextInt(candidates.size() - 1);
4564
if (index2 >= index1) {

java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/PowerOfTwoReplicaSelectorTest.java

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ public void testSingleElement() {
7575

7676
@Test
7777
public void testTwoElementsPicksBetter() {
78-
PowerOfTwoReplicaSelector selector = new PowerOfTwoReplicaSelector();
78+
// Use epsilon=0.0 to test pure Po2RC behavior
79+
PowerOfTwoReplicaSelector selector = new PowerOfTwoReplicaSelector(0.0);
7980
ChannelEndpoint better = new TestEndpoint("better");
8081
ChannelEndpoint worse = new TestEndpoint("worse");
8182

@@ -92,7 +93,8 @@ public void testTwoElementsPicksBetter() {
9293

9394
@Test
9495
public void testThreeElementsNeverPicksWorst() {
95-
PowerOfTwoReplicaSelector selector = new PowerOfTwoReplicaSelector();
96+
// Use epsilon=0.0 to test pure Po2RC behavior
97+
PowerOfTwoReplicaSelector selector = new PowerOfTwoReplicaSelector(0.0);
9698
ChannelEndpoint best = new TestEndpoint("best");
9799
ChannelEndpoint middle = new TestEndpoint("middle");
98100
ChannelEndpoint worst = new TestEndpoint("worst");
@@ -112,7 +114,8 @@ public void testThreeElementsNeverPicksWorst() {
112114

113115
@Test
114116
public void testNullScoresTreatedAsMax() {
115-
PowerOfTwoReplicaSelector selector = new PowerOfTwoReplicaSelector();
117+
// Use epsilon=0.0 to test pure Po2RC behavior
118+
PowerOfTwoReplicaSelector selector = new PowerOfTwoReplicaSelector(0.0);
116119
ChannelEndpoint withScore = new TestEndpoint("withScore");
117120
ChannelEndpoint withoutScore = new TestEndpoint("withoutScore");
118121

@@ -125,4 +128,27 @@ public void testNullScoresTreatedAsMax() {
125128
assertEquals(withScore, selector.select(candidates, scores::get));
126129
}
127130
}
131+
132+
@Test
133+
public void testEpsilonExploration() {
134+
// Set epsilon to 1.0 to force 100% exploration
135+
PowerOfTwoReplicaSelector selector = new PowerOfTwoReplicaSelector(1.0);
136+
ChannelEndpoint best = new TestEndpoint("best");
137+
ChannelEndpoint worst = new TestEndpoint("worst");
138+
139+
Map<ChannelEndpoint, Double> scores = new HashMap<>();
140+
scores.put(best, 10.0);
141+
scores.put(worst, 20.0);
142+
143+
List<ChannelEndpoint> candidates = Arrays.asList(best, worst);
144+
145+
boolean pickedWorst = false;
146+
for (int i = 0; i < 100; i++) {
147+
if (selector.select(candidates, scores::get) == worst) {
148+
pickedWorst = true;
149+
break;
150+
}
151+
}
152+
assertTrue("Should occasionally pick worst with epsilon=1.0", pickedWorst);
153+
}
128154
}

0 commit comments

Comments
 (0)