Skip to content

Commit 83ded36

Browse files
committed
feat: Leader Aware Routing in Connection API
1 parent c5d34ab commit 83ded36

File tree

4 files changed

+78
-2
lines changed

4 files changed

+78
-2
lines changed

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ public String[] getValidValues() {
172172
private static final RpcPriority DEFAULT_RPC_PRIORITY = null;
173173
private static final boolean DEFAULT_RETURN_COMMIT_STATS = false;
174174
private static final boolean DEFAULT_LENIENT = false;
175+
private static final boolean DEFAULT_ROUTE_TO_LEADER = true;
175176

176177
private static final String PLAIN_TEXT_PROTOCOL = "http:";
177178
private static final String HOST_PROTOCOL = "https:";
@@ -183,6 +184,8 @@ public String[] getValidValues() {
183184
public static final String AUTOCOMMIT_PROPERTY_NAME = "autocommit";
184185
/** Name of the 'readonly' connection property. */
185186
public static final String READONLY_PROPERTY_NAME = "readonly";
187+
/** Name of the 'routeToLeader' connection property. */
188+
public static final String ROUTE_TO_LEADER_PROPERTY_NAME = "routeToLeader";
186189
/** Name of the 'retry aborts internally' connection property. */
187190
public static final String RETRY_ABORTS_INTERNALLY_PROPERTY_NAME = "retryAbortsInternally";
188191
/** Name of the 'credentials' connection property. */
@@ -231,6 +234,10 @@ public String[] getValidValues() {
231234
READONLY_PROPERTY_NAME,
232235
"Should the connection start in read-only mode (true/false)",
233236
DEFAULT_READONLY),
237+
ConnectionProperty.createBooleanProperty(
238+
ROUTE_TO_LEADER_PROPERTY_NAME,
239+
"Should the RW/PDML requests be routed to leader region (true/false)",
240+
DEFAULT_ROUTE_TO_LEADER),
234241
ConnectionProperty.createBooleanProperty(
235242
RETRY_ABORTS_INTERNALLY_PROPERTY_NAME,
236243
"Should the connection automatically retry Aborted errors (true/false)",
@@ -426,6 +433,8 @@ private boolean isValidUri(String uri) {
426433
* created on the emulator if any of them do not yet exist. Any existing instance or
427434
* database on the emulator will remain untouched. No other configuration is needed in
428435
* order to connect to the emulator than setting this property.
436+
* <li>routeToLeader (boolean): Sets the routeToLeader flag to route requests to leader (true)
437+
* or any region (false) in RW/PDML transactions. Default is true.
429438
* </ul>
430439
*
431440
* @param uri The URI of the Spanner database to connect to.
@@ -547,6 +556,7 @@ public static Builder newBuilder() {
547556

548557
private final boolean autocommit;
549558
private final boolean readOnly;
559+
private final boolean routeToLeader;
550560
private final boolean retryAbortsInternally;
551561
private final List<StatementExecutionInterceptor> statementExecutionInterceptors;
552562
private final SpannerOptionsConfigurator configurator;
@@ -636,6 +646,7 @@ private ConnectionOptions(Builder builder) {
636646

637647
this.autocommit = parseAutocommit(this.uri);
638648
this.readOnly = parseReadOnly(this.uri);
649+
this.routeToLeader = parseRouteToLeader(this.uri);
639650
this.retryAbortsInternally = parseRetryAbortsInternally(this.uri);
640651
this.statementExecutionInterceptors =
641652
Collections.unmodifiableList(builder.statementExecutionInterceptors);
@@ -719,6 +730,11 @@ static boolean parseReadOnly(String uri) {
719730
return value != null ? Boolean.parseBoolean(value) : DEFAULT_READONLY;
720731
}
721732

733+
static boolean parseRouteToLeader(String uri) {
734+
String value = parseUriProperty(uri, ROUTE_TO_LEADER_PROPERTY_NAME);
735+
return value != null ? Boolean.parseBoolean(value) : DEFAULT_ROUTE_TO_LEADER;
736+
}
737+
722738
@VisibleForTesting
723739
static boolean parseRetryAbortsInternally(String uri) {
724740
String value = parseUriProperty(uri, RETRY_ABORTS_INTERNALLY_PROPERTY_NAME);
@@ -1025,6 +1041,10 @@ public boolean isAutocommit() {
10251041
public boolean isReadOnly() {
10261042
return readOnly;
10271043
}
1044+
/** Whether RW/PDML requests are preferred to be routed to the leader region. */
1045+
public boolean isRouteToLeader() {
1046+
return routeToLeader;
1047+
}
10281048

10291049
/**
10301050
* The initial retryAbortsInternally value for connections created by this {@link

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ static class SpannerPoolKey {
154154
private final boolean usePlainText;
155155
private final String userAgent;
156156
private final String databaseRole;
157+
private final boolean routeToLeader;
157158

158159
@VisibleForTesting
159160
static SpannerPoolKey of(ConnectionOptions options) {
@@ -179,6 +180,7 @@ private SpannerPoolKey(ConnectionOptions options) throws IOException {
179180
this.numChannels = options.getNumChannels();
180181
this.usePlainText = options.isUsePlainText();
181182
this.userAgent = options.getUserAgent();
183+
this.routeToLeader = options.isRouteToLeader();
182184
}
183185

184186
@Override
@@ -194,7 +196,8 @@ public boolean equals(Object o) {
194196
&& Objects.equals(this.numChannels, other.numChannels)
195197
&& Objects.equals(this.databaseRole, other.databaseRole)
196198
&& Objects.equals(this.usePlainText, other.usePlainText)
197-
&& Objects.equals(this.userAgent, other.userAgent);
199+
&& Objects.equals(this.userAgent, other.userAgent)
200+
&& Objects.equals(this.routeToLeader, other.routeToLeader);
198201
}
199202

200203
@Override
@@ -207,7 +210,8 @@ public int hashCode() {
207210
this.numChannels,
208211
this.usePlainText,
209212
this.databaseRole,
210-
this.userAgent);
213+
this.userAgent,
214+
this.routeToLeader);
211215
}
212216
}
213217

@@ -342,6 +346,9 @@ Spanner createSpanner(SpannerPoolKey key, ConnectionOptions options) {
342346
if (options.getChannelProvider() != null) {
343347
builder.setChannelProvider(options.getChannelProvider());
344348
}
349+
if (!options.isRouteToLeader()) {
350+
builder.disableLeaderAwareRouting();
351+
}
345352
if (key.usePlainText) {
346353
// Credentials may not be sent over a plain text channel.
347354
builder.setCredentials(NoCredentials.getInstance());

google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionOptionsTest.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import static com.google.common.truth.Truth.assertThat;
2020
import static org.junit.Assert.assertEquals;
21+
import static org.junit.Assert.assertFalse;
2122
import static org.junit.Assert.assertThrows;
2223
import static org.junit.Assert.assertTrue;
2324

@@ -30,6 +31,7 @@
3031
import com.google.auth.oauth2.ServiceAccountCredentials;
3132
import com.google.cloud.NoCredentials;
3233
import com.google.cloud.spanner.ErrorCode;
34+
import com.google.cloud.spanner.Spanner;
3335
import com.google.cloud.spanner.SpannerException;
3436
import com.google.cloud.spanner.SpannerOptions;
3537
import com.google.common.io.BaseEncoding;
@@ -148,6 +150,27 @@ public void testBuildWithAutoConfigEmulator() {
148150
assertTrue(options.isUsePlainText());
149151
}
150152

153+
@Test
154+
public void testBuildWithRouteToLeader() {
155+
ConnectionOptions.Builder builder = ConnectionOptions.newBuilder();
156+
builder.setUri(
157+
"cloudspanner:/projects/test-project-123/instances/test-instance-123/databases/test-database-123?routeToLeader=false");
158+
ConnectionOptions options = builder.build();
159+
assertEquals(options.getHost(), DEFAULT_HOST);
160+
assertEquals(options.getProjectId(), "test-project-123");
161+
assertEquals(options.getInstanceId(), "test-instance-123");
162+
assertEquals(options.getDatabaseName(), "test-database-123");
163+
assertFalse(options.isRouteToLeader());
164+
165+
// Test for default behavior for routeToLeader property.
166+
builder = ConnectionOptions
167+
.newBuilder()
168+
.setUri(
169+
"cloudspanner:/projects/test-project-123/instances/test-instance-123/databases/test-database-123");
170+
options = builder.build();
171+
assertTrue(options.isRouteToLeader());
172+
}
173+
151174
@Test
152175
public void testBuildWithAutoConfigEmulatorAndHost() {
153176
ConnectionOptions.Builder builder = ConnectionOptions.newBuilder();

google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/SpannerPoolTest.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818

1919
import static com.google.common.truth.Truth.assertThat;
2020
import static org.junit.Assert.assertEquals;
21+
import static org.junit.Assert.assertFalse;
2122
import static org.junit.Assert.assertNotEquals;
23+
import static org.junit.Assert.assertTrue;
2224
import static org.junit.Assert.fail;
2325
import static org.mockito.Mockito.mock;
2426
import static org.mockito.Mockito.never;
@@ -65,6 +67,8 @@ public class SpannerPoolTest {
6567

6668
private ConnectionOptions options5 = mock(ConnectionOptions.class);
6769
private ConnectionOptions options6 = mock(ConnectionOptions.class);
70+
private ConnectionOptions options7 = mock(ConnectionOptions.class);
71+
private ConnectionOptions options8 = mock(ConnectionOptions.class);
6872

6973
private SpannerPool createSubjectAndMocks() {
7074
return createSubjectAndMocks(0L, Ticker.systemTicker());
@@ -93,6 +97,10 @@ Spanner createSpanner(SpannerPoolKey key, ConnectionOptions options) {
9397
// ConnectionOptions with no specific credentials.
9498
when(options5.getProjectId()).thenReturn("test-project-3");
9599
when(options6.getProjectId()).thenReturn("test-project-3");
100+
when(options7.getProjectId()).thenReturn("test-project-3");
101+
when(options7.isRouteToLeader()).thenReturn(true);
102+
when(options8.getProjectId()).thenReturn("test-project-3");
103+
when(options8.isRouteToLeader()).thenReturn(false);
96104

97105
return pool;
98106
}
@@ -145,6 +153,9 @@ public void testGetSpanner() {
145153
spanner1 = pool.getSpanner(options3, connection1);
146154
spanner2 = pool.getSpanner(options4, connection2);
147155
assertThat(spanner1).isNotEqualTo(spanner2);
156+
spanner1 = pool.getSpanner(options7, connection1);
157+
spanner2 = pool.getSpanner(options8, connection2);
158+
assertNotEquals(spanner1, spanner2);
148159
}
149160

150161
@Test
@@ -460,14 +471,29 @@ public void testSpannerPoolKeyEquality() {
460471
.setUri("cloudspanner:/projects/p/instances/i/databases/d")
461472
.setCredentials(NoCredentials.getInstance())
462473
.build();
474+
ConnectionOptions options4 =
475+
ConnectionOptions.newBuilder()
476+
.setUri("cloudspanner:/projects/p/instances/i/databases/d?routeToLeader=true")
477+
.setCredentials(NoCredentials.getInstance())
478+
.build();
479+
ConnectionOptions options5 =
480+
ConnectionOptions.newBuilder()
481+
.setUri("cloudspanner:/projects/p/instances/i/databases/d?routeToLeader=false")
482+
.setCredentials(NoCredentials.getInstance())
483+
.build();
463484

464485
SpannerPoolKey key1 = SpannerPoolKey.of(options1);
465486
SpannerPoolKey key2 = SpannerPoolKey.of(options2);
466487
SpannerPoolKey key3 = SpannerPoolKey.of(options3);
488+
SpannerPoolKey key4 = SpannerPoolKey.of(options4);
489+
SpannerPoolKey key5 = SpannerPoolKey.of(options5);
467490

468491
assertNotEquals(key1, key2);
469492
assertEquals(key2, key3);
470493
assertNotEquals(key1, key3);
471494
assertNotEquals(key1, new Object());
495+
assertEquals(key3, key4);
496+
assertNotEquals(key4, key5);
497+
assertEquals(key2, key4);
472498
}
473499
}

0 commit comments

Comments
 (0)