Skip to content

Commit bc8842b

Browse files
committed
chore(spanner): add LatencyTracker interface and default implementation
Adds an internal LatencyTracker interface and a default implementation that allows the client to track the latency of requests. This can be used for automatic replica selection and load balancing.
1 parent b7e34d2 commit bc8842b

4 files changed

Lines changed: 225 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ nosetests.xml
5555
.pydevproject
5656
*.iml
5757
.idea
58+
.vscode
5859
.settings
5960
.DS_Store
6061
.classpath
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.spanner.spi.v1;
18+
19+
import com.google.api.core.BetaApi;
20+
import com.google.api.core.InternalApi;
21+
import com.google.common.base.Preconditions;
22+
import javax.annotation.concurrent.GuardedBy;
23+
24+
/**
25+
* Implementation of {@link LatencyTracker} using Exponentially Weighted Moving Average (EWMA).
26+
*
27+
* <p>Formula: $S_{i+1} = \alpha * new\_latency + (1 - \alpha) * S_i$
28+
*
29+
* <p>This class is thread-safe.
30+
*/
31+
@InternalApi
32+
@BetaApi
33+
public class EwmaLatencyTracker implements LatencyTracker {
34+
35+
public static final double DEFAULT_ALPHA = 0.05;
36+
37+
private final double alpha;
38+
private final Object lock = new Object();
39+
40+
@GuardedBy("lock")
41+
private double score;
42+
43+
@GuardedBy("lock")
44+
private boolean initialized = false;
45+
46+
/** Creates a new tracker with the default alpha value of 0.05. */
47+
public EwmaLatencyTracker() {
48+
this(DEFAULT_ALPHA);
49+
}
50+
51+
/**
52+
* Creates a new tracker with the specified alpha value.
53+
*
54+
* @param alpha the smoothing factor, must be in the range (0, 1]
55+
*/
56+
public EwmaLatencyTracker(double alpha) {
57+
Preconditions.checkArgument(alpha > 0.0 && alpha <= 1.0, "alpha must be in (0, 1]");
58+
this.alpha = alpha;
59+
}
60+
61+
@Override
62+
public double getScore() {
63+
synchronized (lock) {
64+
return score;
65+
}
66+
}
67+
68+
@Override
69+
public void update(long latencyMillis) {
70+
synchronized (lock) {
71+
if (!initialized) {
72+
score = latencyMillis;
73+
initialized = true;
74+
} else {
75+
score = alpha * latencyMillis + (1 - alpha) * score;
76+
}
77+
}
78+
}
79+
80+
@Override
81+
public void recordError(long penaltyMillis) {
82+
// Treat the error as a sample with high latency (penalty)
83+
update(penaltyMillis);
84+
}
85+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.spanner.spi.v1;
18+
19+
import com.google.api.core.BetaApi;
20+
import com.google.api.core.InternalApi;
21+
22+
/**
23+
* Interface for tracking latency scores of Spanner servers.
24+
*
25+
* <p>Implementations must be thread-safe as instances may be shared across multiple concurrent
26+
* operations.
27+
*/
28+
@InternalApi
29+
@BetaApi
30+
public interface LatencyTracker {
31+
32+
/**
33+
* Returns the current latency score.
34+
*
35+
* @return the latency score, where lower is better.
36+
*/
37+
double getScore();
38+
39+
/**
40+
* Updates the latency score with a new observation.
41+
*
42+
* @param latencyMillis the observed latency in milliseconds.
43+
*/
44+
void update(long latencyMillis);
45+
46+
/**
47+
* Records an error and applies a latency penalty.
48+
*
49+
* @param penaltyMillis the penalty in milliseconds to apply.
50+
*/
51+
void recordError(long penaltyMillis);
52+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.spanner.spi.v1;
18+
19+
import static org.junit.Assert.assertEquals;
20+
import static org.junit.Assert.assertThrows;
21+
22+
import org.junit.Test;
23+
import org.junit.runner.RunWith;
24+
import org.junit.runners.JUnit4;
25+
26+
@RunWith(JUnit4.class)
27+
public class EwmaLatencyTrackerTest {
28+
29+
@Test
30+
public void testInitialization() {
31+
EwmaLatencyTracker tracker = new EwmaLatencyTracker();
32+
tracker.update(100);
33+
assertEquals(100.0, tracker.getScore(), 0.001);
34+
}
35+
36+
@Test
37+
public void testEwmaCalculation() {
38+
double alpha = 0.5;
39+
EwmaLatencyTracker tracker = new EwmaLatencyTracker(alpha);
40+
41+
tracker.update(100); // Initial score = 100
42+
assertEquals(100.0, tracker.getScore(), 0.001);
43+
44+
tracker.update(200); // Score = 0.5 * 200 + 0.5 * 100 = 150
45+
assertEquals(150.0, tracker.getScore(), 0.001);
46+
47+
tracker.update(300); // Score = 0.5 * 300 + 0.5 * 150 = 225
48+
assertEquals(225.0, tracker.getScore(), 0.001);
49+
}
50+
51+
@Test
52+
public void testDefaultAlpha() {
53+
EwmaLatencyTracker tracker = new EwmaLatencyTracker();
54+
tracker.update(100);
55+
tracker.update(200);
56+
57+
double expected =
58+
EwmaLatencyTracker.DEFAULT_ALPHA * 200 + (1 - EwmaLatencyTracker.DEFAULT_ALPHA) * 100;
59+
assertEquals(expected, tracker.getScore(), 0.001);
60+
}
61+
62+
@Test
63+
public void testRecordError() {
64+
EwmaLatencyTracker tracker = new EwmaLatencyTracker(0.5);
65+
tracker.update(100);
66+
67+
tracker.recordError(10000); // Score = 0.5 * 10000 + 0.5 * 100 = 5050
68+
assertEquals(5050.0, tracker.getScore(), 0.001);
69+
}
70+
71+
@Test
72+
public void testInvalidAlpha() {
73+
assertThrows(IllegalArgumentException.class, () -> new EwmaLatencyTracker(0.0));
74+
assertThrows(IllegalArgumentException.class, () -> new EwmaLatencyTracker(1.1));
75+
assertThrows(IllegalArgumentException.class, () -> new EwmaLatencyTracker(-0.1));
76+
}
77+
78+
@Test
79+
public void testAlphaOne() {
80+
EwmaLatencyTracker tracker = new EwmaLatencyTracker(1.0);
81+
tracker.update(100);
82+
assertEquals(100.0, tracker.getScore(), 0.001);
83+
84+
tracker.update(200);
85+
assertEquals(200.0, tracker.getScore(), 0.001);
86+
}
87+
}

0 commit comments

Comments
 (0)