From 2c9da0da95d2cfc02e0cc30b817385284fa70af7 Mon Sep 17 00:00:00 2001 From: Rahul Yadav Date: Thu, 8 Jan 2026 08:45:46 +0530 Subject: [PATCH 1/3] feat: add ChannelFinder server interfaces This commit adds the server abstraction interfaces for location-aware routing: - ChannelFinderServer: Interface representing a Spanner server endpoint with address, health check, and channel access - ChannelFinderServerFactory: Factory interface for creating and caching server connections - GrpcChannelFinderServerFactory: gRPC implementation that creates and manages gRPC channels for different server endpoints These interfaces enable the client to maintain connections to multiple Spanner servers and route requests directly to the appropriate server based on key location information. This is part of the experimental location-aware routing for improved latency. --- .../spanner/spi/v1/ChannelFinderServer.java | 28 ++++++ .../spi/v1/ChannelFinderServerFactory.java | 24 +++++ .../v1/GrpcChannelFinderServerFactory.java | 98 +++++++++++++++++++ 3 files changed, 150 insertions(+) create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/ChannelFinderServer.java create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/ChannelFinderServerFactory.java create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GrpcChannelFinderServerFactory.java diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/ChannelFinderServer.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/ChannelFinderServer.java new file mode 100644 index 0000000000..27a0b5d31a --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/ChannelFinderServer.java @@ -0,0 +1,28 @@ +/* + * 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 io.grpc.ManagedChannel; + +/** Represents a Spanner server endpoint for location-aware routing. */ +public interface ChannelFinderServer { + String getAddress(); + + boolean isHealthy(); + + ManagedChannel getChannel(); // Added to get the underlying channel for RPC calls +} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/ChannelFinderServerFactory.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/ChannelFinderServerFactory.java new file mode 100644 index 0000000000..c81cf82c0d --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/ChannelFinderServerFactory.java @@ -0,0 +1,24 @@ +/* + * 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; + +/** Factory for creating and caching server connections for location-aware routing. */ +public interface ChannelFinderServerFactory { + ChannelFinderServer defaultServer(); + + ChannelFinderServer create(String address); +} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GrpcChannelFinderServerFactory.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GrpcChannelFinderServerFactory.java new file mode 100644 index 0000000000..8c120f0773 --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GrpcChannelFinderServerFactory.java @@ -0,0 +1,98 @@ +/* + * 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.gax.grpc.GrpcTransportChannel; +import com.google.api.gax.grpc.InstantiatingGrpcChannelProvider; +import io.grpc.ManagedChannel; +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +class GrpcChannelFinderServerFactory implements ChannelFinderServerFactory { + private final InstantiatingGrpcChannelProvider.Builder channelBuilder; + private final Map servers = new ConcurrentHashMap<>(); + private final GrpcChannelFinderServer defaultServer; + + public GrpcChannelFinderServerFactory(InstantiatingGrpcChannelProvider.Builder channelBuilder) + throws IOException { + this.channelBuilder = channelBuilder; + // The "default" server will use the original endpoint from the builder. + this.defaultServer = + new GrpcChannelFinderServer(this.channelBuilder.getEndpoint(), channelBuilder.build()); + this.servers.put(this.defaultServer.getAddress(), this.defaultServer); + } + + @Override + public ChannelFinderServer defaultServer() { + return defaultServer; + } + + @Override + public ChannelFinderServer create(String address) { + return servers.computeIfAbsent( + address, + addr -> { + try { + // Modify the builder to use the new address + synchronized (channelBuilder) { + InstantiatingGrpcChannelProvider.Builder newBuilder = + channelBuilder.setEndpoint(addr); + return new GrpcChannelFinderServer(addr, newBuilder.build()); + } + } catch (IOException e) { + throw new RuntimeException("Failed to create channel for address: " + addr, e); + } + }); + } + + static class GrpcChannelFinderServer implements ChannelFinderServer { + private final String address; + private final ManagedChannel channel; + + public GrpcChannelFinderServer(String address, InstantiatingGrpcChannelProvider provider) + throws IOException { + this.address = address; + // It's assumed that getTransportChannel() returns a ManagedChannel or can be cast to one. + // For this example, GrpcTransportChannel is used as in KeyAwareChannel. + GrpcTransportChannel transportChannel = (GrpcTransportChannel) provider.getTransportChannel(); + this.channel = (ManagedChannel) transportChannel.getChannel(); + } + + // Constructor for the default server that already has a channel + public GrpcChannelFinderServer(String address, ManagedChannel channel) { + this.address = address; + this.channel = channel; + } + + @Override + public String getAddress() { + return address; + } + + @Override + public boolean isHealthy() { + // A simple health check. In a real scenario, this might involve a ping or other checks. + return !channel.isShutdown() && !channel.isTerminated(); + } + + @Override + public ManagedChannel getChannel() { + return channel; + } + } +} From 34a715d65d7f9e70d6765c36380842096eff06ae Mon Sep 17 00:00:00 2001 From: Rahul Yadav Date: Thu, 8 Jan 2026 11:41:33 +0530 Subject: [PATCH 2/3] feat: add KeyRangeCache for location-aware routing This commit adds the KeyRangeCache class that maps key ranges to specific server locations for routing decisions. Key features: - TabletEntry class for tablet metadata (UID, server address, incarnation) - ServerEntry class for server connection management - Key range to tablet mapping with efficient lookup - Lazy server initialization for on-demand connections - Integration with ChannelFinderServer interfaces This is part of the experimental location-aware routing for improved latency. --- .../cloud/spanner/spi/v1/KeyRangeCache.java | 588 ++++++++++++++++++ 1 file changed, 588 insertions(+) create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/KeyRangeCache.java diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/KeyRangeCache.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/KeyRangeCache.java new file mode 100644 index 0000000000..dde8d8dad6 --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/KeyRangeCache.java @@ -0,0 +1,588 @@ +/* + * 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.protobuf.ByteString; +import com.google.spanner.v1.CacheUpdate; +import com.google.spanner.v1.Group; +import com.google.spanner.v1.Range; +import com.google.spanner.v1.RoutingHint; +import com.google.spanner.v1.Tablet; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.NavigableMap; +import java.util.Objects; +import java.util.TreeMap; + +/** + * Cache for routing information. - Tablets are stored directly within Groups - Groups are updated + * atomically with their tablets - Ranges reference groups + */ +public final class KeyRangeCache { + + private final ChannelFinderServerFactory serverFactory; + + // Map keyed by limit_key, value contains start_key and group reference + private final NavigableMap ranges = + new TreeMap<>(ByteString.unsignedLexicographicalComparator()); + + // Groups indexed by group_uid + private final Map groups = new HashMap<>(); + + // Servers indexed by address - shared across all tablets + private final Map servers = new HashMap<>(); + + public KeyRangeCache(ChannelFinderServerFactory serverFactory) { + this.serverFactory = Objects.requireNonNull(serverFactory); + } + + private static class ServerEntry { + final ChannelFinderServer server; + int refs = 1; + + ServerEntry(ChannelFinderServer server) { + this.server = server; + } + + String debugString() { + return server.getAddress() + "#" + refs; + } + } + + /** + * Represents a single tablet within a Group. Tablets are stored directly in the Group, not in a + * separate cache. + */ + private class CachedTablet { + long tabletUid = 0; + ByteString incarnation = ByteString.EMPTY; + String serverAddress = ""; + int distance = 0; + boolean skip = false; + Tablet.Role role = Tablet.Role.ROLE_UNSPECIFIED; + String location = ""; + + // Lazily initialized server connection + ChannelFinderServer server = null; + + CachedTablet() {} + + /** Updates tablet from proto, ignoring updates that are too old. */ + void update(Tablet tabletIn) { + // Check incarnation - only update if newer + if (tabletUid > 0 + && ByteString.unsignedLexicographicalComparator() + .compare(incarnation, tabletIn.getIncarnation()) + > 0) { + return; + } + + tabletUid = tabletIn.getTabletUid(); + incarnation = tabletIn.getIncarnation(); + distance = tabletIn.getDistance(); + skip = tabletIn.getSkip(); + role = tabletIn.getRole(); + location = tabletIn.getLocation(); + + // Only reset server if address changed + if (!serverAddress.equals(tabletIn.getServerAddress())) { + serverAddress = tabletIn.getServerAddress(); + server = null; // Will be lazily initialized + } + } + + /** Returns true if tablet should be skipped (unhealthy, marked skip, or no address). */ + boolean shouldSkip(RoutingHint.Builder hintBuilder) { + if (skip || serverAddress.isEmpty()) { + addSkippedTablet(hintBuilder); + return true; + } + // Check server health + if (server != null && !server.isHealthy()) { + addSkippedTablet(hintBuilder); + return true; + } + return false; + } + + private void addSkippedTablet(RoutingHint.Builder hintBuilder) { + RoutingHint.SkippedTablet.Builder skipped = hintBuilder.addSkippedTabletUidBuilder(); + skipped.setTabletUid(tabletUid); + skipped.setIncarnation(incarnation); + } + + /** Picks this tablet for the request and returns the server. */ + ChannelFinderServer pick(RoutingHint.Builder hintBuilder) { + hintBuilder.setTabletUid(tabletUid); + if (server == null && !serverAddress.isEmpty()) { + // Lazy server initialization + ServerEntry entry = findOrInsertServer(serverAddress); + server = entry.server; + } + return server; + } + + String debugString() { + return tabletUid + + ":" + + serverAddress + + "@" + + incarnation + + "(location=" + + location + + ",role=" + + role + + ",distance=" + + distance + + (skip ? ",skip" : "") + + ")"; + } + } + + /** Represents a paxos group with its tablets. Tablets are stored directly in the group. */ + private class CachedGroup { + final long groupUid; + ByteString generation = ByteString.EMPTY; + List tablets = new ArrayList<>(); + int leaderIndex = -1; + int refs = 1; + + CachedGroup(long groupUid) { + this.groupUid = groupUid; + } + + /** Updates group from proto, including its tablets. */ + void update(Group groupIn) { + System.out.println("DEBUG [BYPASS]: Group.update for group " + groupUid + + ", incoming tablets: " + groupIn.getTabletsCount() + + ", leader_index: " + groupIn.getLeaderIndex()); + + // Only update leader if generation is newer + if (ByteString.unsignedLexicographicalComparator() + .compare(groupIn.getGeneration(), generation) + > 0) { + generation = groupIn.getGeneration(); + + // Update leader index + if (groupIn.getLeaderIndex() >= 0 && groupIn.getLeaderIndex() < groupIn.getTabletsCount()) { + leaderIndex = groupIn.getLeaderIndex(); + System.out.println("DEBUG [BYPASS]: Set leader_index to " + leaderIndex); + } else { + leaderIndex = -1; + System.out.println("DEBUG [BYPASS]: No valid leader, set to -1"); + } + } + + // Update tablet locations. Optimize for typical case where tablets haven't changed. + if (tablets.size() == groupIn.getTabletsCount()) { + boolean mismatch = false; + for (int t = 0; t < groupIn.getTabletsCount(); t++) { + if (tablets.get(t).tabletUid != groupIn.getTablets(t).getTabletUid()) { + mismatch = true; + break; + } + } + if (!mismatch) { + // Same tablets, just update them in place + System.out.println("DEBUG [BYPASS]: Tablets unchanged, updating in place"); + for (int t = 0; t < groupIn.getTabletsCount(); t++) { + tablets.get(t).update(groupIn.getTablets(t)); + } + return; + } + } + + // Tablets changed - rebuild the list, reusing existing tablets where possible + System.out.println("DEBUG [BYPASS]: Rebuilding tablet list"); + Map tabletsByUid = new HashMap<>(); + for (CachedTablet tablet : tablets) { + tabletsByUid.put(tablet.tabletUid, tablet); + } + + List newTablets = new ArrayList<>(groupIn.getTabletsCount()); + for (int t = 0; t < groupIn.getTabletsCount(); t++) { + Tablet tabletIn = groupIn.getTablets(t); + CachedTablet tablet = tabletsByUid.get(tabletIn.getTabletUid()); + if (tablet == null) { + tablet = new CachedTablet(); + System.out.println("DEBUG [BYPASS]: Created new tablet for uid " + tabletIn.getTabletUid()); + } + tablet.update(tabletIn); + System.out.println("DEBUG [BYPASS]: Tablet[" + t + "]: uid=" + tablet.tabletUid + + ", server=" + tablet.serverAddress + + ", distance=" + tablet.distance); + newTablets.add(tablet); + } + tablets = newTablets; + System.out.println("DEBUG [BYPASS]: Group " + groupUid + " now has " + tablets.size() + " tablets"); + } + + /** Fills routing hint with tablet information and returns the server. */ + ChannelFinderServer fillRoutingHint(boolean preferLeader, RoutingHint.Builder hintBuilder) { + System.out.println("DEBUG [BYPASS]: Group.fillRoutingHint - preferLeader: " + preferLeader + + ", tablets count: " + tablets.size()); + + // Try leader first if preferred + if (preferLeader && hasLeader()) { + CachedTablet leaderTablet = leader(); + System.out.println("DEBUG [BYPASS]: Trying leader tablet: uid=" + leaderTablet.tabletUid + + ", address=" + leaderTablet.serverAddress + + ", skip=" + leaderTablet.skip); + if (!leaderTablet.shouldSkip(hintBuilder)) { + ChannelFinderServer server = leaderTablet.pick(hintBuilder); + System.out.println("DEBUG [BYPASS]: Leader tablet picked, server: " + + (server != null ? server.getAddress() : "null")); + return server; + } + } + + // Try other tablets in order (they're ordered by distance) + for (int i = 0; i < tablets.size(); i++) { + CachedTablet tablet = tablets.get(i); + System.out.println("DEBUG [BYPASS]: Trying tablet[" + i + "]: uid=" + tablet.tabletUid + + ", address=" + tablet.serverAddress + + ", distance=" + tablet.distance + + ", skip=" + tablet.skip); + if (!tablet.shouldSkip(hintBuilder)) { + ChannelFinderServer server = tablet.pick(hintBuilder); + System.out.println("DEBUG [BYPASS]: Tablet[" + i + "] picked, server: " + + (server != null ? server.getAddress() : "null")); + return server; + } + } + + System.out.println("DEBUG [BYPASS]: No suitable tablet found in group"); + return null; + } + + boolean hasLeader() { + return leaderIndex >= 0 && leaderIndex < tablets.size(); + } + + CachedTablet leader() { + return tablets.get(leaderIndex); + } + + String debugString() { + StringBuilder sb = new StringBuilder(); + sb.append(groupUid).append(":["); + for (int i = 0; i < tablets.size(); i++) { + sb.append(tablets.get(i).debugString()); + if (hasLeader() && i == leaderIndex) { + sb.append(" (leader)"); + } + if (i < tablets.size() - 1) { + sb.append(", "); + } + } + sb.append("]@").append(generation.toStringUtf8()); + sb.append("#").append(refs); + return sb.toString(); + } + } + + /** Represents a cached range with its group and split information. */ + private static class CachedRange { + final ByteString startKey; + CachedGroup group = null; + long splitId = 0; + ByteString generation; + + CachedRange(ByteString startKey, CachedGroup group, long splitId, ByteString generation) { + this.startKey = startKey; + this.group = group; + this.splitId = splitId; + this.generation = generation; + } + + String debugString() { + return (group != null ? group.groupUid : "null_group") + + "," + + splitId + + "@" + + (generation.isEmpty() ? "" : generation.toStringUtf8()); + } + } + + private ServerEntry findOrInsertServer(String address) { + ServerEntry entry = servers.get(address); + if (entry == null) { + entry = new ServerEntry(serverFactory.create(address)); + servers.put(address, entry); + } else { + entry.refs++; + } + return entry; + } + + private void unref(ServerEntry serverEntry) { + if (serverEntry == null) { + return; + } + if (--serverEntry.refs == 0) { + servers.remove(serverEntry.server.getAddress()); + } + } + + private CachedGroup findGroup(long groupUid) { + CachedGroup group = groups.get(groupUid); + if (group != null) { + group.refs++; + } + return group; + } + + /** Finds or inserts a group and updates it with proto data. */ + private CachedGroup findOrInsertGroup(Group groupIn) { + CachedGroup group = groups.get(groupIn.getGroupUid()); + if (group == null) { + group = new CachedGroup(groupIn.getGroupUid()); + groups.put(groupIn.getGroupUid(), group); + } else { + group.refs++; + } + group.update(groupIn); + return group; + } + + private void unref(CachedGroup group) { + if (group == null) { + return; + } + if (--group.refs == 0) { + groups.remove(group.groupUid); + } + } + + private void replaceRangeIfNewer(Range rangeIn) { + ByteString startKey = rangeIn.getStartKey(); + ByteString limitKey = rangeIn.getLimitKey(); + + List affectedLimitKeys = new ArrayList<>(); + boolean newerBlockingRangeExists = false; + + // Find overlapping ranges + for (Map.Entry entry : ranges.tailMap(startKey, false).entrySet()) { + ByteString existingLimit = entry.getKey(); + CachedRange existingRange = entry.getValue(); + ByteString existingStart = existingRange.startKey; + + if (ByteString.unsignedLexicographicalComparator().compare(existingStart, limitKey) >= 0) { + break; + } + + if (isNewerOrSame(rangeIn, existingRange, existingLimit)) { + affectedLimitKeys.add(existingLimit); + } else { + newerBlockingRangeExists = true; + break; + } + } + + if (newerBlockingRangeExists) { + return; + } + + for (ByteString keyToRemove : affectedLimitKeys) { + CachedRange removed = ranges.remove(keyToRemove); + if (removed == null) { + continue; + } + + if (ByteString.unsignedLexicographicalComparator().compare(limitKey, keyToRemove) < 0) { + CachedRange tailPart = + new CachedRange(limitKey, removed.group, removed.splitId, removed.generation); + if (tailPart.group != null) { + tailPart.group.refs++; + } + ranges.put(keyToRemove, tailPart); + } + + if (ByteString.unsignedLexicographicalComparator().compare(removed.startKey, startKey) < 0) { + ranges.put(startKey, removed); + } else { + if (removed.group != null) { + unref(removed.group); + } + } + } + + CachedRange newCachedRange = + new CachedRange( + startKey, + findGroup(rangeIn.getGroupUid()), + rangeIn.getSplitId(), + rangeIn.getGeneration()); + ranges.put(limitKey, newCachedRange); + } + + private boolean isNewerOrSame( + Range rangeIn, CachedRange existingCachedRange, ByteString existingMapKeyLimit) { + int genCompare = + ByteString.unsignedLexicographicalComparator() + .compare(rangeIn.getGeneration(), existingCachedRange.generation); + if (genCompare > 0) { + return true; + } + if (genCompare == 0) { + return rangeIn.getStartKey().equals(existingCachedRange.startKey) + && rangeIn.getLimitKey().equals(existingMapKeyLimit); + } + return false; + } + + /** Applies cache updates. Tablets are processed inside group updates. */ + public void addRanges(CacheUpdate cacheUpdate) { + System.out.println("DEBUG [BYPASS]: addRanges called with " + + cacheUpdate.getGroupCount() + " groups, " + + cacheUpdate.getRangeCount() + " ranges"); + + // Insert all groups. Tablets are processed inside findOrInsertGroup -> Group.update() + List newGroups = new ArrayList<>(); + for (Group groupIn : cacheUpdate.getGroupList()) { + System.out.println("DEBUG [BYPASS]: Processing group " + groupIn.getGroupUid() + + " with " + groupIn.getTabletsCount() + " tablets"); + newGroups.add(findOrInsertGroup(groupIn)); + } + + // Process ranges + for (Range rangeIn : cacheUpdate.getRangeList()) { + System.out.println("DEBUG [BYPASS]: Processing range for group " + rangeIn.getGroupUid() + + ", split_id=" + rangeIn.getSplitId()); + replaceRangeIfNewer(rangeIn); + } + + // Unref the groups we acquired (ranges hold their own refs) + for (CachedGroup g : newGroups) { + unref(g); + } + + System.out.println("DEBUG [BYPASS]: After addRanges - ranges: " + ranges.size() + + ", groups: " + groups.size() + ", servers: " + servers.size()); + } + + /** Fills routing hint and returns the server to use. */ + public ChannelFinderServer fillRoutingInfo( + String sessionUri, boolean preferLeader, RoutingHint.Builder hintBuilder) { + System.out.println("DEBUG [BYPASS]: fillRoutingInfo called, ranges in cache: " + ranges.size() + + ", groups in cache: " + groups.size()); + + if (hintBuilder.getKey().isEmpty()) { + System.out.println("DEBUG [BYPASS]: No key in hint, using default server"); + return serverFactory.defaultServer(); + } + + ByteString requestKey = hintBuilder.getKey(); + ByteString requestLimitKey = hintBuilder.getLimitKey(); + + // Find range containing the key + Map.Entry entry = ranges.higherEntry(requestKey); + + CachedRange targetRange = null; + ByteString targetRangeLimitKey = null; + + if (entry != null) { + ByteString rangeLimit = entry.getKey(); + CachedRange range = entry.getValue(); + + // Check if key is within this range + if (ByteString.unsignedLexicographicalComparator().compare(requestKey, range.startKey) >= 0) { + targetRange = range; + targetRangeLimitKey = rangeLimit; + System.out.println("DEBUG [BYPASS]: Found range for key, group_uid: " + + (range.group != null ? range.group.groupUid : "null")); + } + } + + if (targetRange == null) { + System.out.println("DEBUG [BYPASS]: No range found for key, using default server"); + return serverFactory.defaultServer(); + } + + // For point reads (empty limit_key), check if key is in the split + // For range reads, check if the whole range is covered + if (!requestLimitKey.isEmpty()) { + // Range read - check if limit is within the split + if (ByteString.unsignedLexicographicalComparator() + .compare(requestLimitKey, targetRangeLimitKey) + > 0) { + // Range extends beyond this split + System.out.println("DEBUG [BYPASS]: Range extends beyond split, using default server"); + return serverFactory.defaultServer(); + } + } + + if (targetRange.group == null) { + System.out.println("DEBUG [BYPASS]: Range has no group, using default server"); + return serverFactory.defaultServer(); + } + + // Fill in routing hint with range/group/split info + hintBuilder.setGroupUid(targetRange.group.groupUid); + hintBuilder.setSplitId(targetRange.splitId); + hintBuilder.setKey(targetRange.startKey); + hintBuilder.setLimitKey(targetRangeLimitKey); + + System.out.println("DEBUG [BYPASS]: Group " + targetRange.group.groupUid + + " has " + targetRange.group.tablets.size() + " tablets" + + ", hasLeader: " + targetRange.group.hasLeader() + + ", leaderIndex: " + targetRange.group.leaderIndex); + + // Let the group pick the tablet + ChannelFinderServer server = targetRange.group.fillRoutingHint(preferLeader, hintBuilder); + if (server != null) { + System.out.println("DEBUG [BYPASS]: Group returned server: " + server.getAddress()); + return server; + } + + System.out.println("DEBUG [BYPASS]: Group returned no server, using default"); + return serverFactory.defaultServer(); + } + + public void clear() { + for (CachedRange range : ranges.values()) { + if (range.group != null) { + unref(range.group); + } + } + ranges.clear(); + groups.clear(); + servers.clear(); + } + + public String debugString() { + StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : ranges.entrySet()) { + CachedRange cachedRange = entry.getValue(); + sb.append("Range[") + .append(cachedRange.startKey.toStringUtf8()) + .append("-") + .append(entry.getKey().toStringUtf8()) + .append("]: "); + sb.append(cachedRange.debugString()).append("\n"); + } + for (CachedGroup g : groups.values()) { + sb.append(g.debugString()).append("\n"); + } + for (ServerEntry s : servers.values()) { + sb.append(s.debugString()).append("\n"); + } + return sb.toString(); + } +} From 2ce0069f15e8b5191471f30a6a595e6ea927c87c Mon Sep 17 00:00:00 2001 From: cloud-java-bot Date: Thu, 8 Jan 2026 06:16:47 +0000 Subject: [PATCH 3/3] chore: generate libraries at Thu Jan 8 06:14:13 UTC 2026 --- .../cloud/spanner/spi/v1/KeyRangeCache.java | 133 +++++++++++++----- 1 file changed, 94 insertions(+), 39 deletions(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/KeyRangeCache.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/KeyRangeCache.java index dde8d8dad6..a16d6555e5 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/KeyRangeCache.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/KeyRangeCache.java @@ -169,9 +169,13 @@ private class CachedGroup { /** Updates group from proto, including its tablets. */ void update(Group groupIn) { - System.out.println("DEBUG [BYPASS]: Group.update for group " + groupUid - + ", incoming tablets: " + groupIn.getTabletsCount() - + ", leader_index: " + groupIn.getLeaderIndex()); + System.out.println( + "DEBUG [BYPASS]: Group.update for group " + + groupUid + + ", incoming tablets: " + + groupIn.getTabletsCount() + + ", leader_index: " + + groupIn.getLeaderIndex()); // Only update leader if generation is newer if (ByteString.unsignedLexicographicalComparator() @@ -221,33 +225,49 @@ void update(Group groupIn) { CachedTablet tablet = tabletsByUid.get(tabletIn.getTabletUid()); if (tablet == null) { tablet = new CachedTablet(); - System.out.println("DEBUG [BYPASS]: Created new tablet for uid " + tabletIn.getTabletUid()); + System.out.println( + "DEBUG [BYPASS]: Created new tablet for uid " + tabletIn.getTabletUid()); } tablet.update(tabletIn); - System.out.println("DEBUG [BYPASS]: Tablet[" + t + "]: uid=" + tablet.tabletUid - + ", server=" + tablet.serverAddress - + ", distance=" + tablet.distance); + System.out.println( + "DEBUG [BYPASS]: Tablet[" + + t + + "]: uid=" + + tablet.tabletUid + + ", server=" + + tablet.serverAddress + + ", distance=" + + tablet.distance); newTablets.add(tablet); } tablets = newTablets; - System.out.println("DEBUG [BYPASS]: Group " + groupUid + " now has " + tablets.size() + " tablets"); + System.out.println( + "DEBUG [BYPASS]: Group " + groupUid + " now has " + tablets.size() + " tablets"); } /** Fills routing hint with tablet information and returns the server. */ ChannelFinderServer fillRoutingHint(boolean preferLeader, RoutingHint.Builder hintBuilder) { - System.out.println("DEBUG [BYPASS]: Group.fillRoutingHint - preferLeader: " + preferLeader - + ", tablets count: " + tablets.size()); + System.out.println( + "DEBUG [BYPASS]: Group.fillRoutingHint - preferLeader: " + + preferLeader + + ", tablets count: " + + tablets.size()); // Try leader first if preferred if (preferLeader && hasLeader()) { CachedTablet leaderTablet = leader(); - System.out.println("DEBUG [BYPASS]: Trying leader tablet: uid=" + leaderTablet.tabletUid - + ", address=" + leaderTablet.serverAddress - + ", skip=" + leaderTablet.skip); + System.out.println( + "DEBUG [BYPASS]: Trying leader tablet: uid=" + + leaderTablet.tabletUid + + ", address=" + + leaderTablet.serverAddress + + ", skip=" + + leaderTablet.skip); if (!leaderTablet.shouldSkip(hintBuilder)) { ChannelFinderServer server = leaderTablet.pick(hintBuilder); - System.out.println("DEBUG [BYPASS]: Leader tablet picked, server: " - + (server != null ? server.getAddress() : "null")); + System.out.println( + "DEBUG [BYPASS]: Leader tablet picked, server: " + + (server != null ? server.getAddress() : "null")); return server; } } @@ -255,14 +275,24 @@ ChannelFinderServer fillRoutingHint(boolean preferLeader, RoutingHint.Builder hi // Try other tablets in order (they're ordered by distance) for (int i = 0; i < tablets.size(); i++) { CachedTablet tablet = tablets.get(i); - System.out.println("DEBUG [BYPASS]: Trying tablet[" + i + "]: uid=" + tablet.tabletUid - + ", address=" + tablet.serverAddress - + ", distance=" + tablet.distance - + ", skip=" + tablet.skip); + System.out.println( + "DEBUG [BYPASS]: Trying tablet[" + + i + + "]: uid=" + + tablet.tabletUid + + ", address=" + + tablet.serverAddress + + ", distance=" + + tablet.distance + + ", skip=" + + tablet.skip); if (!tablet.shouldSkip(hintBuilder)) { ChannelFinderServer server = tablet.pick(hintBuilder); - System.out.println("DEBUG [BYPASS]: Tablet[" + i + "] picked, server: " - + (server != null ? server.getAddress() : "null")); + System.out.println( + "DEBUG [BYPASS]: Tablet[" + + i + + "] picked, server: " + + (server != null ? server.getAddress() : "null")); return server; } } @@ -449,22 +479,32 @@ private boolean isNewerOrSame( /** Applies cache updates. Tablets are processed inside group updates. */ public void addRanges(CacheUpdate cacheUpdate) { - System.out.println("DEBUG [BYPASS]: addRanges called with " - + cacheUpdate.getGroupCount() + " groups, " - + cacheUpdate.getRangeCount() + " ranges"); + System.out.println( + "DEBUG [BYPASS]: addRanges called with " + + cacheUpdate.getGroupCount() + + " groups, " + + cacheUpdate.getRangeCount() + + " ranges"); // Insert all groups. Tablets are processed inside findOrInsertGroup -> Group.update() List newGroups = new ArrayList<>(); for (Group groupIn : cacheUpdate.getGroupList()) { - System.out.println("DEBUG [BYPASS]: Processing group " + groupIn.getGroupUid() - + " with " + groupIn.getTabletsCount() + " tablets"); + System.out.println( + "DEBUG [BYPASS]: Processing group " + + groupIn.getGroupUid() + + " with " + + groupIn.getTabletsCount() + + " tablets"); newGroups.add(findOrInsertGroup(groupIn)); } // Process ranges for (Range rangeIn : cacheUpdate.getRangeList()) { - System.out.println("DEBUG [BYPASS]: Processing range for group " + rangeIn.getGroupUid() - + ", split_id=" + rangeIn.getSplitId()); + System.out.println( + "DEBUG [BYPASS]: Processing range for group " + + rangeIn.getGroupUid() + + ", split_id=" + + rangeIn.getSplitId()); replaceRangeIfNewer(rangeIn); } @@ -473,16 +513,24 @@ public void addRanges(CacheUpdate cacheUpdate) { unref(g); } - System.out.println("DEBUG [BYPASS]: After addRanges - ranges: " + ranges.size() - + ", groups: " + groups.size() + ", servers: " + servers.size()); + System.out.println( + "DEBUG [BYPASS]: After addRanges - ranges: " + + ranges.size() + + ", groups: " + + groups.size() + + ", servers: " + + servers.size()); } /** Fills routing hint and returns the server to use. */ public ChannelFinderServer fillRoutingInfo( String sessionUri, boolean preferLeader, RoutingHint.Builder hintBuilder) { - System.out.println("DEBUG [BYPASS]: fillRoutingInfo called, ranges in cache: " + ranges.size() - + ", groups in cache: " + groups.size()); - + System.out.println( + "DEBUG [BYPASS]: fillRoutingInfo called, ranges in cache: " + + ranges.size() + + ", groups in cache: " + + groups.size()); + if (hintBuilder.getKey().isEmpty()) { System.out.println("DEBUG [BYPASS]: No key in hint, using default server"); return serverFactory.defaultServer(); @@ -505,8 +553,9 @@ public ChannelFinderServer fillRoutingInfo( if (ByteString.unsignedLexicographicalComparator().compare(requestKey, range.startKey) >= 0) { targetRange = range; targetRangeLimitKey = rangeLimit; - System.out.println("DEBUG [BYPASS]: Found range for key, group_uid: " - + (range.group != null ? range.group.groupUid : "null")); + System.out.println( + "DEBUG [BYPASS]: Found range for key, group_uid: " + + (range.group != null ? range.group.groupUid : "null")); } } @@ -539,10 +588,16 @@ public ChannelFinderServer fillRoutingInfo( hintBuilder.setKey(targetRange.startKey); hintBuilder.setLimitKey(targetRangeLimitKey); - System.out.println("DEBUG [BYPASS]: Group " + targetRange.group.groupUid - + " has " + targetRange.group.tablets.size() + " tablets" - + ", hasLeader: " + targetRange.group.hasLeader() - + ", leaderIndex: " + targetRange.group.leaderIndex); + System.out.println( + "DEBUG [BYPASS]: Group " + + targetRange.group.groupUid + + " has " + + targetRange.group.tablets.size() + + " tablets" + + ", hasLeader: " + + targetRange.group.hasLeader() + + ", leaderIndex: " + + targetRange.group.leaderIndex); // Let the group pick the tablet ChannelFinderServer server = targetRange.group.fillRoutingHint(preferLeader, hintBuilder);