Skip to content

Conversation

@rahul2393
Copy link
Contributor

Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly:

  • Make sure to open an issue as a bug/issue before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea
  • Ensure the tests and linter pass
  • Code coverage does not decrease (if any source code was changed)
  • Appropriate docs were updated (if necessary)

Fixes #<issue_number_goes_here> ☕️

If you write sample code, please follow the samples format.

This commit adds the foundational SsFormat class that provides sortable
string format (ssformat) encoding utilities. This encoding is used by
Spanner for key ordering and routing.

Key features:
- Composite tag encoding for interleaved tables
- Signed/unsigned integer encoding (increasing/decreasing)
- String and bytes encoding with proper escaping
- Double encoding with proper sign handling
- Timestamp and UUID encoding
- Null value markers with configurable ordering
- TargetRange class for key range representation

Includes unit tests for all encoding functions.

This is part of the experimental location-aware routing for improved
latency.
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.
@rahul2393 rahul2393 requested review from a team as code owners January 8, 2026 05:58
@google-cla
Copy link

google-cla bot commented Jan 8, 2026

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@product-auto-label product-auto-label bot added size: xl Pull request size is extra large. api: spanner Issues related to the googleapis/java-spanner API. labels Jan 8, 2026
@gemini-code-assist
Copy link

Summary of Changes

Hello @rahul2393, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant new capability for the Google Cloud Spanner client library: key-aware channel integration. This feature enables the client to intelligently route read requests directly to the most appropriate Spanner backend server, bypassing the traditional front-end. This is achieved through a new KeyAwareChannel that intercepts specific RPCs, a ChannelFinder that manages routing logic and caches database metadata, and KeyRecipe and SsFormat utilities for encoding keys. The functionality is designed to be opt-in, requiring explicit configuration, and is supported by new comprehensive tests to ensure its reliability and correctness.

Highlights

  • Key-Aware Channel Integration: Introduced a new mechanism to enable key-aware routing for Spanner RPCs, allowing clients to bypass the traditional front-end (SpanFE) and route requests directly to the appropriate Spanner server based on key ranges.
  • Dynamic Routing Logic: Implemented ChannelFinder, KeyRecipeCache, and KeyRangeCache to dynamically determine the optimal server for a given read request by processing database metadata (key recipes, group, and tablet information) received from the server.
  • Extensible Channel Management: Developed a KeyAwareChannel that extends ManagedChannel to intercept key-aware operations (specifically StreamingRead), and a ChannelFinderServerFactory to manage and create gRPC channels to various Spanner backend servers.
  • SSFormat Encoding: Added KeyRecipe and SsFormat utilities to encode Spanner keys into the internal ssformat byte string representation, which is crucial for efficient key-based routing.
  • Conditional Activation: The key-aware routing feature is opt-in, activated by configuring an experimental host in SpannerOptions and setting the GOOGLE_SPANNER_EXPERIMENTAL_LOCATION_API environment variable to 'true'.
  • Comprehensive Testing: Included new golden file tests for KeyRecipe encoding to ensure correctness across various data types and operations, as well as an integration test (BypassPointReadTest) to validate the end-to-end bypass functionality.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@rahul2393 rahul2393 force-pushed the feat/key-aware-channel-integration branch from 41e3a29 to e3056a9 Compare January 8, 2026 06:03
@rahul2393 rahul2393 changed the title Feat/key aware channel integration feat: experimental location aware routing- Part 5 Jan 8, 2026
Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a significant new feature for key-aware channel integration, which enables location-aware routing (SpanFE bypass) for Spanner. The changes include a number of new classes for managing channels, caching routing information, and encoding keys. The feature is gated behind an experimental flag and an environment variable.

My review highlights several important points. There are critical issues such as the unimplemented shutdown() and shutdownNow() methods in KeyAwareChannel, which could lead to resource leaks. There's also a high-severity issue regarding the use of a predictable random number generator in production code. Additionally, I've pointed out the extensive use of System.out.println for debugging, which should be replaced with a proper logging framework. I've also included suggestions for code simplification and improving consistency.

Comment on lines +97 to +107
public ManagedChannel shutdownNow() {
// TODO: Need to manage shutdown of all created channels in serverFactory
// and clear channelFinders map, potentially shutting down individual finders/channels.
return this;
}

@Override
public ManagedChannel shutdown() {
// TODO: Need to manage shutdown of all created channels in serverFactory
return this;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The shutdown() and shutdownNow() methods are not implemented and contain TODO comments. A ManagedChannel must properly implement shutdown logic to release resources, such as the underlying channels created by GrpcChannelFinderServerFactory. Without a proper implementation, this can lead to resource leaks. Please implement these methods to properly shut down all channels managed by serverFactory and clean up any other resources.

}

// For random value encoding - use seed 12345 for deterministic testing
private static final java.util.Random testRandom = new java.util.Random(12345);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The testRandom field is a java.util.Random instance initialized with a fixed seed, which is then used in encodeRandomValuePart to generate "random" values. Using a predictable random number generator in production code is a security risk. Given its name, this seems intended for testing only. If so, it should be moved to test code or injected as a dependency during tests. If random values are needed in production, a secure random source like java.security.SecureRandom should be used instead.

Comment on lines +489 to +491
String locationApiEnvVar = System.getenv("GOOGLE_SPANNER_EXPERIMENTAL_LOCATION_API");
boolean isExperimentalHost = options.isExperimentalHost();
boolean envVarEnabled = "true".equalsIgnoreCase(locationApiEnvVar);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic to check for the GOOGLE_SPANNER_EXPERIMENTAL_LOCATION_API environment variable is implemented directly here using System.getenv(). However, SpannerOptions.java was also modified to add an isEnableLocationApi() method to the SpannerEnvironment interface, which is not being used.

To maintain consistency with how other environment-based flags are handled, you should read this value from SpannerOptions instead. This would involve adding isEnableLocationApi() to SpannerOptions and its builder, and then calling options.isEnableLocationApi() here. This change would centralize environment-dependent configuration within SpannerOptions.

Comment on lines 63 to 64
System.out.println("DEBUG [BYPASS]: Database ID changed from " + databaseId
+ " to " + cacheUpdate.getDatabaseId() + ", clearing caches");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This class, and several other new classes in this PR (KeyAwareChannel, KeyRangeCache, KeyRecipeCache), use System.out.println for logging debug information. In a library, logging should be done through a standard logging framework like java.util.logging (which is already used in GapicSpannerRpc) instead of printing to standard output. This allows consumers of the library to control log levels and output destinations. Please replace these System.out.println calls with appropriate logger calls (e.g., logger.fine(...)).

Comment on lines +52 to +56
synchronized (channelBuilder) {
InstantiatingGrpcChannelProvider.Builder newBuilder =
channelBuilder.setEndpoint(addr);
return new GrpcChannelFinderServer(addr, newBuilder.build());
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The synchronized (channelBuilder) block within the computeIfAbsent lambda appears to be unnecessary and could be confusing. computeIfAbsent is already atomic for a given key. Inside the lambda, channelBuilder.setEndpoint(addr) returns a new builder instance rather than mutating the shared channelBuilder field, so there is no shared mutable state being modified that would require synchronization. Removing the synchronized block would make the code clearer.

            InstantiatingGrpcChannelProvider.Builder newBuilder =
                channelBuilder.setEndpoint(addr);
            return new GrpcChannelFinderServer(addr, newBuilder.build());

Comment on lines +431 to +501
private static long[] parseTimestamp(String ts) {
// Parse RFC3339 timestamp using Java time library
// Remove trailing Z and parse
String withoutZ = ts.substring(0, ts.length() - 1);

// Parse date-time parts
int dotIdx = withoutZ.indexOf('.');
String dateTimePart;
int nanos = 0;
if (dotIdx >= 0) {
dateTimePart = withoutZ.substring(0, dotIdx);
String fracStr = withoutZ.substring(dotIdx + 1);
// Pad to 9 digits
while (fracStr.length() < 9) {
fracStr = fracStr + "0";
}
// Truncate to 9 digits
if (fracStr.length() > 9) {
fracStr = fracStr.substring(0, 9);
}
nanos = Integer.parseInt(fracStr);
} else {
dateTimePart = withoutZ;
}

// Parse date and time components
// Format: YYYY-MM-DDTHH:MM:SS
String[] dateTime = dateTimePart.split("T");
String[] dateParts = dateTime[0].split("-");
String[] timeParts = dateTime[1].split(":");

int year = Integer.parseInt(dateParts[0]);
int month = Integer.parseInt(dateParts[1]);
int day = Integer.parseInt(dateParts[2]);
int hour = Integer.parseInt(timeParts[0]);
int minute = Integer.parseInt(timeParts[1]);
int second = Integer.parseInt(timeParts[2]);

// Compute days since epoch using proleptic Gregorian calendar
long days = civilDayNumber(year, month, day);
long seconds = days * 86400L + hour * 3600L + minute * 60L + second;

return new long[] {seconds, nanos};
}

// Compute the civil day number (days since Unix epoch 1970-01-01)
// This matches absl::CivilDay calculation
private static long civilDayNumber(int year, int month, int day) {
// Algorithm from http://howardhinnant.github.io/date_algorithms.html
// This produces the same results as absl::CivilDay
int y = year;
int m = month;
int d = day;

// Adjust year and month (March = month 1 in this algorithm)
if (m <= 2) {
y -= 1;
m += 12;
}
m -= 3;

// Days from era 0 (year 0 March 1) to given date
int era = (y >= 0 ? y : y - 399) / 400;
int yoe = y - era * 400; // year of era [0, 399]
int doy = (153 * m + 2) / 5 + d - 1; // day of year [0, 365]
int doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; // day of era [0, 146096]
long dayNumber =
(long) era * 146097 + doe - 719468; // shift epoch from 0000-03-01 to 1970-01-01

return dayNumber;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The methods parseTimestamp and civilDayNumber (used by parseDate) implement custom logic for parsing RFC3339 timestamps and dates. The java.time API, available since Java 8, provides robust and standard ways to handle this which would make the code simpler and less error-prone.

For example:

  • parseTimestamp could be simplified using java.time.Instant.parse().
  • parseDate could use java.time.LocalDate.parse(dateStr).toEpochDay() to get the number of days since the epoch.

Consider refactoring to use the standard library unless there's a specific performance or compatibility reason for this custom implementation.

* <li>A database with table T created and populated
* </ul>
*/
public class BypassPointReadTest {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This test is implemented as a class with a main method, which is not standard for tests in a Maven project. It should be converted into a JUnit test (e.g., using @Test annotations). This would allow it to be run as part of the standard build and test cycle. Using System.exit(1) is also not ideal for a test suite, as it can halt the entire test run. A JUnit test would fail gracefully and report the error without exiting.

This commit adds the key recipe system for translating requests to
sortable string format (ssformat) keys for routing.

Key components:
- KeyRecipe: Translates requests to ssformat keys using server-provided recipes
- KeyRecipeCache: Caches recipes by request fingerprint for efficient reuse
- PreparedRead: Helper class for preparing read requests with routing info
- RecipeGoldenTest: Golden tests for recipe encoding validation
- RecipeTestCases: Test case definitions for recipe tests

This is part of the experimental location-aware routing for improved latency.
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.
This commit adds the KeyAwareChannel class and integrates all components
for location-aware routing in the Spanner client.

Key components:
- ChannelFinder: Orchestrates routing decisions using caches
- KeyAwareChannel: Custom ManagedChannel that intercepts key-aware methods
- SpannerOptions integration: Enables feature via setExperimentalHost()
- GapicSpannerRpc modifications: Wire in KeyAwareChannel

Activation requires:
- SpannerOptions.Builder.setExperimentalHost() with location-aware endpoint
- GOOGLE_SPANNER_EXPERIMENTAL_LOCATION_API=true environment variable

This is part of the experimental location-aware routing for improved latency.
@rahul2393 rahul2393 force-pushed the feat/key-aware-channel-integration branch from 4d74d9e to 5b5e6f5 Compare January 8, 2026 06:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api: spanner Issues related to the googleapis/java-spanner API. size: xl Pull request size is extra large.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants