Skip to content

Commit c50bb2e

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 c50bb2e

4 files changed

Lines changed: 230 additions & 1 deletion

File tree

java-spanner/.gitignore

Lines changed: 0 additions & 1 deletion
This file was deleted.
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 initialized ? score : Double.MAX_VALUE;
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: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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 testUninitializedScore() {
38+
EwmaLatencyTracker tracker = new EwmaLatencyTracker();
39+
assertEquals(Double.MAX_VALUE, tracker.getScore(), 0.001);
40+
}
41+
42+
@Test
43+
public void testEwmaCalculation() {
44+
double alpha = 0.5;
45+
EwmaLatencyTracker tracker = new EwmaLatencyTracker(alpha);
46+
47+
tracker.update(100); // Initial score = 100
48+
assertEquals(100.0, tracker.getScore(), 0.001);
49+
50+
tracker.update(200); // Score = 0.5 * 200 + 0.5 * 100 = 150
51+
assertEquals(150.0, tracker.getScore(), 0.001);
52+
53+
tracker.update(300); // Score = 0.5 * 300 + 0.5 * 150 = 225
54+
assertEquals(225.0, tracker.getScore(), 0.001);
55+
}
56+
57+
@Test
58+
public void testDefaultAlpha() {
59+
EwmaLatencyTracker tracker = new EwmaLatencyTracker();
60+
tracker.update(100);
61+
tracker.update(200);
62+
63+
double expected =
64+
EwmaLatencyTracker.DEFAULT_ALPHA * 200 + (1 - EwmaLatencyTracker.DEFAULT_ALPHA) * 100;
65+
assertEquals(expected, tracker.getScore(), 0.001);
66+
}
67+
68+
@Test
69+
public void testRecordError() {
70+
EwmaLatencyTracker tracker = new EwmaLatencyTracker(0.5);
71+
tracker.update(100);
72+
73+
tracker.recordError(10000); // Score = 0.5 * 10000 + 0.5 * 100 = 5050
74+
assertEquals(5050.0, tracker.getScore(), 0.001);
75+
}
76+
77+
@Test
78+
public void testInvalidAlpha() {
79+
assertThrows(IllegalArgumentException.class, () -> new EwmaLatencyTracker(0.0));
80+
assertThrows(IllegalArgumentException.class, () -> new EwmaLatencyTracker(1.1));
81+
assertThrows(IllegalArgumentException.class, () -> new EwmaLatencyTracker(-0.1));
82+
}
83+
84+
@Test
85+
public void testAlphaOne() {
86+
EwmaLatencyTracker tracker = new EwmaLatencyTracker(1.0);
87+
tracker.update(100);
88+
assertEquals(100.0, tracker.getScore(), 0.001);
89+
90+
tracker.update(200);
91+
assertEquals(200.0, tracker.getScore(), 0.001);
92+
}
93+
}

0 commit comments

Comments
 (0)