Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion java-spanner/.gitignore

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.cloud.spanner.spi.v1;

import com.google.api.core.BetaApi;
import com.google.api.core.InternalApi;
import com.google.common.base.MoreObjects;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.Function;

/** Implementation of {@link ReplicaSelector} using the "Power of 2 Random Choices" strategy. */
@InternalApi
@BetaApi
public class PowerOfTwoReplicaSelector implements ReplicaSelector {

@Override
public ChannelEndpoint select(
List<ChannelEndpoint> candidates, Function<ChannelEndpoint, Double> scoreLookup) {
if (candidates == null || candidates.isEmpty()) {
return null;
}
if (candidates.size() == 1) {
return candidates.get(0);
}

Random random = ThreadLocalRandom.current();
int index1 = random.nextInt(candidates.size());
int index2 = random.nextInt(candidates.size() - 1);
if (index2 >= index1) {
index2++;
}
Comment thread
olavloite marked this conversation as resolved.

ChannelEndpoint c1 = candidates.get(index1);
ChannelEndpoint c2 = candidates.get(index2);

Double score1 = scoreLookup.apply(c1);
Double score2 = scoreLookup.apply(c2);

// Handle null scores by treating them as Double.MAX_VALUE (lowest priority)
double s1 = MoreObjects.firstNonNull(score1, Double.MAX_VALUE);
double s2 = MoreObjects.firstNonNull(score2, Double.MAX_VALUE);

return s1 <= s2 ? c1 : c2;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.cloud.spanner.spi.v1;

import com.google.api.core.BetaApi;
import com.google.api.core.InternalApi;
import java.util.List;
import java.util.function.Function;

/** Interface for selecting a replica from a list of candidates. */
@InternalApi
@BetaApi
public interface ReplicaSelector {

/**
* Selects a replica from the given list of candidates.
*
* @param candidates the list of eligible candidates.
* @param scoreLookup a function to look up the latency score for a candidate.
* @return the selected candidate, or null if the list is empty.
*/
ChannelEndpoint select(
List<ChannelEndpoint> candidates, Function<ChannelEndpoint, Double> scoreLookup);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.cloud.spanner.spi.v1;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public class PowerOfTwoReplicaSelectorTest {

private static class TestEndpoint implements ChannelEndpoint {
private final String address;

TestEndpoint(String address) {
this.address = address;
}

@Override
public String getAddress() {
return address;
}

@Override
public boolean isHealthy() {
return true;
}

@Override
public boolean isTransientFailure() {
return false;
}

@Override
public io.grpc.ManagedChannel getChannel() {
return null;
}
}

@Test
public void testEmptyList() {
PowerOfTwoReplicaSelector selector = new PowerOfTwoReplicaSelector();
assertNull(selector.select(null, endpoint -> 1.0));
assertNull(selector.select(Arrays.asList(), endpoint -> 1.0));
}

@Test
public void testSingleElement() {
PowerOfTwoReplicaSelector selector = new PowerOfTwoReplicaSelector();
ChannelEndpoint endpoint = new TestEndpoint("a");
assertEquals(endpoint, selector.select(Arrays.asList(endpoint), e -> 1.0));
}

@Test
public void testTwoElementsPicksBetter() {
PowerOfTwoReplicaSelector selector = new PowerOfTwoReplicaSelector();
ChannelEndpoint better = new TestEndpoint("better");
ChannelEndpoint worse = new TestEndpoint("worse");

Map<ChannelEndpoint, Double> scores = new HashMap<>();
scores.put(better, 10.0);
scores.put(worse, 20.0);

List<ChannelEndpoint> candidates = Arrays.asList(better, worse);

for (int i = 0; i < 100; i++) {
assertEquals(better, selector.select(candidates, scores::get));
}
}

@Test
public void testThreeElementsNeverPicksWorst() {
PowerOfTwoReplicaSelector selector = new PowerOfTwoReplicaSelector();
ChannelEndpoint best = new TestEndpoint("best");
ChannelEndpoint middle = new TestEndpoint("middle");
ChannelEndpoint worst = new TestEndpoint("worst");

Map<ChannelEndpoint, Double> scores = new HashMap<>();
scores.put(best, 10.0);
scores.put(middle, 20.0);
scores.put(worst, 30.0);

List<ChannelEndpoint> candidates = Arrays.asList(best, middle, worst);

for (int i = 0; i < 100; i++) {
ChannelEndpoint selected = selector.select(candidates, scores::get);
assertTrue("Should not pick worst", selected != worst);
}
}

@Test
public void testNullScoresTreatedAsMax() {
PowerOfTwoReplicaSelector selector = new PowerOfTwoReplicaSelector();
ChannelEndpoint withScore = new TestEndpoint("withScore");
ChannelEndpoint withoutScore = new TestEndpoint("withoutScore");

Map<ChannelEndpoint, Double> scores = new HashMap<>();
scores.put(withScore, 100.0);

List<ChannelEndpoint> candidates = Arrays.asList(withScore, withoutScore);

for (int i = 0; i < 100; i++) {
assertEquals(withScore, selector.select(candidates, scores::get));
}
}
}
Loading