Skip to content

Commit 2a85446

Browse files
feat: Leader Aware Routing in Connection API (#2308)
* feat: Leader Aware Routing in Connection API (cherry picked from commit 83ded36) * Update google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java Co-authored-by: Knut Olav Løite <[email protected]> * add verbose documentation * address review comments * change assertThat to assertEquals/assertNotEquals within testGetSpanner method * add credentials url * add credentials url --------- Co-authored-by: Knut Olav Løite <[email protected]>
1 parent 441c1b0 commit 2a85446

File tree

4 files changed

+93
-13
lines changed

4 files changed

+93
-13
lines changed

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

+24
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
private static final boolean DEFAULT_DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE = false;
176177
private static final boolean DEFAULT_TRACK_SESSION_LEAKS = true;
177178
private static final boolean DEFAULT_TRACK_CONNECTION_LEAKS = true;
@@ -186,6 +187,8 @@ public String[] getValidValues() {
186187
public static final String AUTOCOMMIT_PROPERTY_NAME = "autocommit";
187188
/** Name of the 'readonly' connection property. */
188189
public static final String READONLY_PROPERTY_NAME = "readonly";
190+
/** Name of the 'routeToLeader' connection property. */
191+
public static final String ROUTE_TO_LEADER_PROPERTY_NAME = "routeToLeader";
189192
/** Name of the 'retry aborts internally' connection property. */
190193
public static final String RETRY_ABORTS_INTERNALLY_PROPERTY_NAME = "retryAbortsInternally";
191194
/** Name of the 'credentials' connection property. */
@@ -241,6 +244,10 @@ public String[] getValidValues() {
241244
READONLY_PROPERTY_NAME,
242245
"Should the connection start in read-only mode (true/false)",
243246
DEFAULT_READONLY),
247+
ConnectionProperty.createBooleanProperty(
248+
ROUTE_TO_LEADER_PROPERTY_NAME,
249+
"Should read/write transactions and partitioned DML be routed to leader region (true/false)",
250+
DEFAULT_ROUTE_TO_LEADER),
244251
ConnectionProperty.createBooleanProperty(
245252
RETRY_ABORTS_INTERNALLY_PROPERTY_NAME,
246253
"Should the connection automatically retry Aborted errors (true/false)",
@@ -462,6 +469,8 @@ private boolean isValidUri(String uri) {
462469
* created on the emulator if any of them do not yet exist. Any existing instance or
463470
* database on the emulator will remain untouched. No other configuration is needed in
464471
* order to connect to the emulator than setting this property.
472+
* <li>routeToLeader (boolean): Sets the routeToLeader flag to route requests to leader (true)
473+
* or any region (false) in read/write transactions and Partitioned DML. Default is true.
465474
* </ul>
466475
*
467476
* @param uri The URI of the Spanner database to connect to.
@@ -586,6 +595,7 @@ public static Builder newBuilder() {
586595

587596
private final boolean autocommit;
588597
private final boolean readOnly;
598+
private final boolean routeToLeader;
589599
private final boolean retryAbortsInternally;
590600
private final List<StatementExecutionInterceptor> statementExecutionInterceptors;
591601
private final SpannerOptionsConfigurator configurator;
@@ -678,6 +688,7 @@ private ConnectionOptions(Builder builder) {
678688

679689
this.autocommit = parseAutocommit(this.uri);
680690
this.readOnly = parseReadOnly(this.uri);
691+
this.routeToLeader = parseRouteToLeader(this.uri);
681692
this.retryAbortsInternally = parseRetryAbortsInternally(this.uri);
682693
this.statementExecutionInterceptors =
683694
Collections.unmodifiableList(builder.statementExecutionInterceptors);
@@ -762,6 +773,11 @@ static boolean parseReadOnly(String uri) {
762773
return value != null ? Boolean.parseBoolean(value) : DEFAULT_READONLY;
763774
}
764775

776+
static boolean parseRouteToLeader(String uri) {
777+
String value = parseUriProperty(uri, ROUTE_TO_LEADER_PROPERTY_NAME);
778+
return value != null ? Boolean.parseBoolean(value) : DEFAULT_ROUTE_TO_LEADER;
779+
}
780+
765781
@VisibleForTesting
766782
static boolean parseRetryAbortsInternally(String uri) {
767783
String value = parseUriProperty(uri, RETRY_ABORTS_INTERNALLY_PROPERTY_NAME);
@@ -1089,6 +1105,14 @@ public boolean isReadOnly() {
10891105
return readOnly;
10901106
}
10911107

1108+
/**
1109+
* Whether read/write transactions and partitioned DML are preferred to be routed to the leader
1110+
* region.
1111+
*/
1112+
public boolean isRouteToLeader() {
1113+
return routeToLeader;
1114+
}
1115+
10921116
/**
10931117
* The initial retryAbortsInternally value for connections created by this {@link
10941118
* ConnectionOptions}

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

+9-2
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

+24
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ public class ConnectionOptionsTest {
4949
private static final String FILE_TEST_PATH =
5050
ConnectionOptionsTest.class.getResource("test-key.json").getFile();
5151
private static final String DEFAULT_HOST = "https://spanner.googleapis.com";
52+
private static final String TEST_PROJECT = "test-project-123";
53+
private static final String TEST_INSTANCE = "test-instance-123";
54+
private static final String TEST_DATABASE = "test-database-123";
5255

5356
@Test
5457
public void testBuildWithURIWithDots() {
@@ -149,6 +152,27 @@ public void testBuildWithAutoConfigEmulator() {
149152
assertTrue(options.isUsePlainText());
150153
}
151154

155+
@Test
156+
public void testBuildWithRouteToLeader() {
157+
final String BASE_URI =
158+
"cloudspanner:/projects/test-project-123/instances/test-instance-123/databases/test-database-123";
159+
ConnectionOptions.Builder builder = ConnectionOptions.newBuilder();
160+
builder.setUri(BASE_URI + "?routeToLeader=false");
161+
builder.setCredentialsUrl(FILE_TEST_PATH);
162+
ConnectionOptions options = builder.build();
163+
assertEquals(options.getHost(), DEFAULT_HOST);
164+
assertEquals(options.getProjectId(), TEST_PROJECT);
165+
assertEquals(options.getInstanceId(), TEST_INSTANCE);
166+
assertEquals(options.getDatabaseName(), TEST_DATABASE);
167+
assertFalse(options.isRouteToLeader());
168+
169+
// Test for default behavior for routeToLeader property.
170+
builder = ConnectionOptions.newBuilder().setUri(BASE_URI);
171+
builder.setCredentialsUrl(FILE_TEST_PATH);
172+
options = builder.build();
173+
assertTrue(options.isRouteToLeader());
174+
}
175+
152176
@Test
153177
public void testBuildWithAutoConfigEmulatorAndHost() {
154178
ConnectionOptions.Builder builder = ConnectionOptions.newBuilder();

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

+36-11
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ public class SpannerPoolTest {
6565

6666
private ConnectionOptions options5 = mock(ConnectionOptions.class);
6767
private ConnectionOptions options6 = mock(ConnectionOptions.class);
68+
private ConnectionOptions options7 = mock(ConnectionOptions.class);
69+
private ConnectionOptions options8 = mock(ConnectionOptions.class);
6870

6971
private SpannerPool createSubjectAndMocks() {
7072
return createSubjectAndMocks(0L, Ticker.systemTicker());
@@ -93,6 +95,10 @@ Spanner createSpanner(SpannerPoolKey key, ConnectionOptions options) {
9395
// ConnectionOptions with no specific credentials.
9496
when(options5.getProjectId()).thenReturn("test-project-3");
9597
when(options6.getProjectId()).thenReturn("test-project-3");
98+
when(options7.getProjectId()).thenReturn("test-project-3");
99+
when(options7.isRouteToLeader()).thenReturn(true);
100+
when(options8.getProjectId()).thenReturn("test-project-3");
101+
when(options8.isRouteToLeader()).thenReturn(false);
96102

97103
return pool;
98104
}
@@ -111,40 +117,43 @@ public void testGetSpanner() {
111117
// assert equal
112118
spanner1 = pool.getSpanner(options1, connection1);
113119
spanner2 = pool.getSpanner(options1, connection2);
114-
assertThat(spanner1).isEqualTo(spanner2);
120+
assertEquals(spanner1, spanner2);
115121
spanner1 = pool.getSpanner(options2, connection1);
116122
spanner2 = pool.getSpanner(options2, connection2);
117-
assertThat(spanner1).isEqualTo(spanner2);
123+
assertEquals(spanner1, spanner2);
118124
spanner1 = pool.getSpanner(options3, connection1);
119125
spanner2 = pool.getSpanner(options3, connection2);
120-
assertThat(spanner1).isEqualTo(spanner2);
126+
assertEquals(spanner1, spanner2);
121127
spanner1 = pool.getSpanner(options4, connection1);
122128
spanner2 = pool.getSpanner(options4, connection2);
123-
assertThat(spanner1).isEqualTo(spanner2);
129+
assertEquals(spanner1, spanner2);
124130
// Options 5 and 6 both use default credentials.
125131
spanner1 = pool.getSpanner(options5, connection1);
126132
spanner2 = pool.getSpanner(options6, connection2);
127-
assertThat(spanner1).isEqualTo(spanner2);
133+
assertEquals(spanner1, spanner2);
128134

129135
// assert not equal
130136
spanner1 = pool.getSpanner(options1, connection1);
131137
spanner2 = pool.getSpanner(options2, connection2);
132-
assertThat(spanner1).isNotEqualTo(spanner2);
138+
assertNotEquals(spanner1, spanner2);
133139
spanner1 = pool.getSpanner(options1, connection1);
134140
spanner2 = pool.getSpanner(options3, connection2);
135-
assertThat(spanner1).isNotEqualTo(spanner2);
141+
assertNotEquals(spanner1, spanner2);
136142
spanner1 = pool.getSpanner(options1, connection1);
137143
spanner2 = pool.getSpanner(options4, connection2);
138-
assertThat(spanner1).isNotEqualTo(spanner2);
144+
assertNotEquals(spanner1, spanner2);
139145
spanner1 = pool.getSpanner(options2, connection1);
140146
spanner2 = pool.getSpanner(options3, connection2);
141-
assertThat(spanner1).isNotEqualTo(spanner2);
147+
assertNotEquals(spanner1, spanner2);
142148
spanner1 = pool.getSpanner(options2, connection1);
143149
spanner2 = pool.getSpanner(options4, connection2);
144-
assertThat(spanner1).isNotEqualTo(spanner2);
150+
assertNotEquals(spanner1, spanner2);
145151
spanner1 = pool.getSpanner(options3, connection1);
146152
spanner2 = pool.getSpanner(options4, connection2);
147-
assertThat(spanner1).isNotEqualTo(spanner2);
153+
assertNotEquals(spanner1, spanner2);
154+
spanner1 = pool.getSpanner(options7, connection1);
155+
spanner2 = pool.getSpanner(options8, connection2);
156+
assertNotEquals(spanner1, spanner2);
148157
}
149158

150159
@Test
@@ -460,14 +469,30 @@ public void testSpannerPoolKeyEquality() {
460469
.setUri("cloudspanner:/projects/p/instances/i/databases/d")
461470
.setCredentials(NoCredentials.getInstance())
462471
.build();
472+
// Not passing in routeToLeader in Connection URI is equivalent to passing it as true,
473+
// as routeToLeader is true by default.
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);
472497
}
473498
}

0 commit comments

Comments
 (0)