Skip to content

Commit c29b99f

Browse files
authored
chore(spanner): add LatencyTracker interface and default implementation (#12729)
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 b8bf432 commit c29b99f

File tree

3 files changed

+249
-0
lines changed

3 files changed

+249
-0
lines changed
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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 java.time.Duration;
23+
import java.util.concurrent.TimeUnit;
24+
import javax.annotation.concurrent.GuardedBy;
25+
26+
/**
27+
* Implementation of {@link LatencyTracker} using Exponentially Weighted Moving Average (EWMA).
28+
*
29+
* <p>Formula: $S_{i+1} = \alpha * new\_latency + (1 - \alpha) * S_i$
30+
*
31+
* <p>This class is thread-safe.
32+
*/
33+
@InternalApi
34+
@BetaApi
35+
public class EwmaLatencyTracker implements LatencyTracker {
36+
37+
public static final double DEFAULT_ALPHA = 0.05;
38+
39+
private final double alpha;
40+
private final Object lock = new Object();
41+
42+
@GuardedBy("lock")
43+
private double score;
44+
45+
@GuardedBy("lock")
46+
private boolean initialized = false;
47+
48+
/** Creates a new tracker with the default alpha value of 0.05. */
49+
public EwmaLatencyTracker() {
50+
this(DEFAULT_ALPHA);
51+
}
52+
53+
/**
54+
* Creates a new tracker with the specified alpha value.
55+
*
56+
* @param alpha the smoothing factor, must be in the range (0, 1]
57+
*/
58+
public EwmaLatencyTracker(double alpha) {
59+
Preconditions.checkArgument(alpha > 0.0 && alpha <= 1.0, "alpha must be in (0, 1]");
60+
this.alpha = alpha;
61+
}
62+
63+
@Override
64+
public double getScore() {
65+
synchronized (lock) {
66+
return initialized ? score : Double.MAX_VALUE;
67+
}
68+
}
69+
70+
@Override
71+
public void update(Duration latency) {
72+
long latencyMicros;
73+
try {
74+
latencyMicros = TimeUnit.MICROSECONDS.convert(latency.toNanos(), TimeUnit.NANOSECONDS);
75+
} catch (ArithmeticException e) {
76+
// Duration is too large to fit in nanoseconds (292+ years).
77+
// Use Long.MAX_VALUE to give it the lowest possible priority.
78+
latencyMicros = Long.MAX_VALUE;
79+
}
80+
synchronized (lock) {
81+
if (!initialized) {
82+
score = latencyMicros;
83+
initialized = true;
84+
} else {
85+
score = alpha * latencyMicros + (1 - alpha) * score;
86+
}
87+
}
88+
}
89+
90+
@Override
91+
public void recordError(Duration penalty) {
92+
// Treat the error as a sample with high latency (penalty)
93+
update(penalty);
94+
}
95+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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 java.time.Duration;
22+
23+
/**
24+
* Interface for tracking latency scores of Spanner servers.
25+
*
26+
* <p>Implementations must be thread-safe as instances may be shared across multiple concurrent
27+
* operations.
28+
*/
29+
@InternalApi
30+
@BetaApi
31+
public interface LatencyTracker {
32+
33+
/**
34+
* Returns the current latency score.
35+
*
36+
* @return the latency score, where lower is better.
37+
*/
38+
double getScore();
39+
40+
/**
41+
* Updates the latency score with a new observation.
42+
*
43+
* @param latency the observed latency.
44+
*/
45+
void update(Duration latency);
46+
47+
/**
48+
* Records an error and applies a latency penalty.
49+
*
50+
* @param penalty the penalty to apply.
51+
*/
52+
void recordError(Duration penalty);
53+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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 java.time.Duration;
23+
import org.junit.Test;
24+
import org.junit.runner.RunWith;
25+
import org.junit.runners.JUnit4;
26+
27+
@RunWith(JUnit4.class)
28+
public class EwmaLatencyTrackerTest {
29+
30+
@Test
31+
public void testInitialization() {
32+
EwmaLatencyTracker tracker = new EwmaLatencyTracker();
33+
tracker.update(Duration.ofNanos(100 * 1000));
34+
assertEquals(100.0, tracker.getScore(), 0.001);
35+
}
36+
37+
@Test
38+
public void testUninitializedScore() {
39+
EwmaLatencyTracker tracker = new EwmaLatencyTracker();
40+
assertEquals(Double.MAX_VALUE, tracker.getScore(), 0.001);
41+
}
42+
43+
@Test
44+
public void testOverflowScore() {
45+
EwmaLatencyTracker tracker = new EwmaLatencyTracker();
46+
tracker.update(Duration.ofSeconds(Long.MAX_VALUE));
47+
assertEquals((double) Long.MAX_VALUE, tracker.getScore(), 0.001);
48+
}
49+
50+
@Test
51+
public void testEwmaCalculation() {
52+
double alpha = 0.5;
53+
EwmaLatencyTracker tracker = new EwmaLatencyTracker(alpha);
54+
55+
tracker.update(Duration.ofNanos(100 * 1000)); // Initial score = 100
56+
assertEquals(100.0, tracker.getScore(), 0.001);
57+
58+
tracker.update(Duration.ofNanos(200 * 1000)); // Score = 0.5 * 200 + 0.5 * 100 = 150
59+
assertEquals(150.0, tracker.getScore(), 0.001);
60+
61+
tracker.update(Duration.ofNanos(300 * 1000)); // Score = 0.5 * 300 + 0.5 * 150 = 225
62+
assertEquals(225.0, tracker.getScore(), 0.001);
63+
}
64+
65+
@Test
66+
public void testDefaultAlpha() {
67+
EwmaLatencyTracker tracker = new EwmaLatencyTracker();
68+
tracker.update(Duration.ofNanos(100 * 1000));
69+
tracker.update(Duration.ofNanos(200 * 1000));
70+
71+
double expected =
72+
EwmaLatencyTracker.DEFAULT_ALPHA * 200 + (1 - EwmaLatencyTracker.DEFAULT_ALPHA) * 100;
73+
assertEquals(expected, tracker.getScore(), 0.001);
74+
}
75+
76+
@Test
77+
public void testRecordError() {
78+
EwmaLatencyTracker tracker = new EwmaLatencyTracker(0.5);
79+
tracker.update(Duration.ofNanos(100 * 1000));
80+
81+
tracker.recordError(Duration.ofNanos(10000 * 1000)); // Score = 0.5 * 10000 + 0.5 * 100 = 5050
82+
assertEquals(5050.0, tracker.getScore(), 0.001);
83+
}
84+
85+
@Test
86+
public void testInvalidAlpha() {
87+
assertThrows(IllegalArgumentException.class, () -> new EwmaLatencyTracker(0.0));
88+
assertThrows(IllegalArgumentException.class, () -> new EwmaLatencyTracker(1.1));
89+
assertThrows(IllegalArgumentException.class, () -> new EwmaLatencyTracker(-0.1));
90+
}
91+
92+
@Test
93+
public void testAlphaOne() {
94+
EwmaLatencyTracker tracker = new EwmaLatencyTracker(1.0);
95+
tracker.update(Duration.ofNanos(100 * 1000));
96+
assertEquals(100.0, tracker.getScore(), 0.001);
97+
98+
tracker.update(Duration.ofNanos(200 * 1000));
99+
assertEquals(200.0, tracker.getScore(), 0.001);
100+
}
101+
}

0 commit comments

Comments
 (0)