-
Notifications
You must be signed in to change notification settings - Fork 141
chore: integrate location aware routing with RPCs #4296
Changes from 9 commits
ef80e7b
d2a346d
6356db6
feb3c7c
a487b8e
0a4d906
891aa32
734d730
7790a8a
72cafc8
a089244
dff3c61
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| /* | ||
| * Copyright 2026 Google LLC | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package com.google.cloud.spanner.spi.v1; | ||
|
|
||
| import com.google.api.core.InternalApi; | ||
| import com.google.api.gax.grpc.InstantiatingGrpcChannelProvider; | ||
| import java.io.IOException; | ||
|
|
||
| /** Factory for creating {@link ChannelEndpointCache} instances. */ | ||
| @InternalApi | ||
| public interface ChannelEndpointCacheFactory { | ||
| ChannelEndpointCache create(InstantiatingGrpcChannelProvider baseProvider) throws IOException; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| /* | ||
| * Copyright 2026 Google LLC | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package com.google.cloud.spanner.spi.v1; | ||
|
|
||
| import com.google.api.core.InternalApi; | ||
| import com.google.spanner.v1.CacheUpdate; | ||
| import com.google.spanner.v1.DirectedReadOptions; | ||
| import com.google.spanner.v1.ExecuteSqlRequest; | ||
| import com.google.spanner.v1.ReadRequest; | ||
| import com.google.spanner.v1.RoutingHint; | ||
| import com.google.spanner.v1.TransactionSelector; | ||
| import java.util.Objects; | ||
| import java.util.concurrent.atomic.AtomicLong; | ||
|
|
||
| /** | ||
| * Finds a server for a request using location-aware routing metadata. | ||
| * | ||
| * <p>This component is per-database and maintains both recipe and range caches. | ||
| */ | ||
| @InternalApi | ||
| public final class ChannelFinder { | ||
| private final Object updateLock = new Object(); | ||
| private final AtomicLong databaseId = new AtomicLong(); | ||
| private final KeyRecipeCache recipeCache = new KeyRecipeCache(); | ||
| private final KeyRangeCache rangeCache; | ||
| private final String databaseUri; | ||
|
|
||
| public ChannelFinder(ChannelEndpointCache endpointCache, String databaseUri) { | ||
| this.rangeCache = new KeyRangeCache(Objects.requireNonNull(endpointCache)); | ||
| this.databaseUri = Objects.requireNonNull(databaseUri); | ||
| } | ||
|
|
||
| void useDeterministicRandom() { | ||
| rangeCache.useDeterministicRandom(); | ||
| } | ||
|
|
||
| public void update(CacheUpdate update) { | ||
| synchronized (updateLock) { | ||
| long currentId = databaseId.get(); | ||
| if (currentId != update.getDatabaseId()) { | ||
| if (currentId != 0) { | ||
| recipeCache.clear(); | ||
| rangeCache.clear(); | ||
| } | ||
| databaseId.set(update.getDatabaseId()); | ||
| } | ||
| if (update.hasKeyRecipes()) { | ||
| recipeCache.addRecipes(update.getKeyRecipes()); | ||
| } | ||
| rangeCache.addRanges(update); | ||
| } | ||
| } | ||
|
|
||
| public ChannelEndpoint findServer(ReadRequest.Builder reqBuilder) { | ||
| recipeCache.computeKeys(reqBuilder); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method calls
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I decided this for performance reasons
Likely impact:
There are 2 fixes (in order of disruption):
I went ahead with 1? let me know if you want 2 |
||
| return fillRoutingHint( | ||
| reqBuilder.getTransaction(), | ||
| reqBuilder.getDirectedReadOptions(), | ||
| KeyRangeCache.RangeMode.COVERING_SPLIT, | ||
| reqBuilder.getRoutingHintBuilder()); | ||
| } | ||
|
|
||
| public ChannelEndpoint findServer(ExecuteSqlRequest.Builder reqBuilder) { | ||
| recipeCache.computeKeys(reqBuilder); | ||
| return fillRoutingHint( | ||
| reqBuilder.getTransaction(), | ||
| reqBuilder.getDirectedReadOptions(), | ||
| KeyRangeCache.RangeMode.PICK_RANDOM, | ||
| reqBuilder.getRoutingHintBuilder()); | ||
| } | ||
|
|
||
| private ChannelEndpoint fillRoutingHint( | ||
| TransactionSelector transactionSelector, | ||
| DirectedReadOptions directedReadOptions, | ||
| KeyRangeCache.RangeMode rangeMode, | ||
| RoutingHint.Builder hintBuilder) { | ||
| long id = databaseId.get(); | ||
| if (id == 0) { | ||
| return null; | ||
| } | ||
| hintBuilder.setDatabaseId(id); | ||
| return rangeCache.fillRoutingHint( | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this not be in a
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. databaseId changes only on rare cache resets from server updates(I am not able to think of any possible way when server will change it). We avoid locking this hot path; at worst a stale/mismatched hint is ignored and default routing is used. |
||
| preferLeader(transactionSelector), rangeMode, directedReadOptions, hintBuilder); | ||
| } | ||
|
|
||
| private static boolean preferLeader(TransactionSelector selector) { | ||
| switch (selector.getSelectorCase()) { | ||
| case BEGIN: | ||
| return !selector.getBegin().hasReadOnly() || selector.getBegin().getReadOnly().getStrong(); | ||
| case SINGLE_USE: | ||
| if (!selector.getSingleUse().hasReadOnly()) { | ||
| return true; | ||
| } | ||
| return selector.getSingleUse().getReadOnly().getStrong(); | ||
| case ID: | ||
| case SELECTOR_NOT_SET: | ||
| default: | ||
| return true; | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.