Skip to content

autosharding: implementation of EndpointMap, autosharding client and LB Policy - #13039

Open
shivaspeaks wants to merge 58 commits into
grpc:masterfrom
shivaspeaks:autosharding-part3-lazy-endpoints
Open

shivaspeaks wants to merge 58 commits into
grpc:masterfrom
shivaspeaks:autosharding-part3-lazy-endpoints

Conversation

@shivaspeaks

@shivaspeaks shivaspeaks commented Sep 8, 2026 •

Copy link
Copy Markdown
Member

@shivaspeaks
shivaspeaks marked this pull request as ready for review September 8, 2026 09:49
@shivaspeaks
shivaspeaks requested a review from sauravzg September 8, 2026 09:49
@shivaspeaks

Copy link
Copy Markdown
Member Author

I've added the LB policy that consumes it (AutoShardingLoadBalancer) and the AutoshardingClient it depends on, so every expectation on EndpointMap is now visible at a call site.

Note for updates in EndpointMap:

  1. Index is now list position, not a stored field.
  2. Index assigned after de-duplication.
  3. Rebuild in place instead of a fresh map: every grpc-java LB retains children by key. This is what makes the rebuilding flag necessary: LazyLoadBalancer publishes IDLE synchronously from acceptResolvedAddresses, so without it the LB would build a picker from a half-rebuilt map.
  4. The picker snapshot is built here, not in the picker.
  5. requestConnection returns early unless IDLE, where the gRFC calls ExitIdle() unconditionally.
  6. AutoShardingAttributes.ATTR_ENDPOINT_HOSTNAME and XdsInternalAttributes.ATTR_ADDRESS_NAME (gRFC A81) carry the same value but are two different keys. We need to bridge them when we do xds integration or we can refactor it out in util, we simply cannot let autosharding have a dependency on xds because then it will be cycle when we do xds integration.

@shivaspeaks

Copy link
Copy Markdown
Member Author

I will re-request for your review after I do my final review. Please wait for the re-request @sauravzg before taking a look.

@shivaspeaks shivaspeaks changed the title autosharding: implementation of EndpointMap autosharding: implementation of EndpointMap, autosharding client and LB Policy Sep 22, 2026

@sauravzg sauravzg left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed all of source. Some comments may be outdated since this was a large PR and I have been incrementally reviewing it for quite some time.

*/
@Internal
public final class AutoShardingLoadBalancerProvider extends LoadBalancerProvider {
private static final String POLICY_NAME = "autosharding_experimental";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: go/java-practices/constants#when-to-use-a-constant-versus-an-inlined-literal

/**
* Translates {@code AutoshardingLbConfig} from its JSON form, as specified by gRFC A119.
*
* <p>The configuration is a proto3 message, so an absent string field and an empty one are the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why is proto3 is relevant here if our input is a map? IIRC, proto3 when converted to json has a consistent behavior that the key is ommitted.

For us , I don't think the distinction even matters. Empty and missing are both garbage for us.

* Translates {@code AutoshardingLbConfig} from its JSON form, as specified by gRFC A119.
*
* <p>The configuration is a proto3 message, so an absent string field and an empty one are the
* same value and no field is required. What gets rejected here is therefore a judgment call

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The grfc should actively call out valid or invalid configurations.

We should fail as early as possible. An empty address will never connect and the config should be rejected , there's no point passing it along to the channel. I assume the same for channelFactoryKey.

Go seems to treat all of them as mandatory: https://github.com/grpc/grpc-go/blob/5ff04b66ea52b067fab16c735f330f6c2528c53a/balancer/autosharding/config.go#L46-L55

* useful message instead of an exception. The constructor here re-checks the same constraints as
* a backstop that cannot be bypassed.
*/
final class AutoShardingLoadBalancerConfig {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Would this benefit from autovalue ?

Comment on lines +25 to +31
* <p>These keys are set on the resolver result by whoever is driving the policy: the
* {@code cds_experimental} LB policy in xDS deployments, or the application in non-xDS ones.
* They are internal to gRPC and carry no compatibility guarantee; the supported public API for
* configuring this policy is added separately.
*
* <p>The locality substituted for {@code %s} in {@code autosharding_target} is not injected here:
* it is read from {@link io.grpc.EquivalentAddressGroup#ATTR_LOCALITY_NAME}, and used only when

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The docs are leaky. Docs should talk about the contract of an entity i.e. how the entity is supposed to be used , not leak how the implmentation actually uses it.

encode(startKey), encode(endKey)));
continue;
}
if (keyOrder == 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Merge it with >0 , with a single error message.

* @param dropped collects a description of each slice that was dropped
*/
private static List<Assignment.Slice> dropOverlaps(
List<Assignment.Slice> sorted, List<String> dropped) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

similar to above, sorted , dropped , kept are very generic variable names.

int end = index + 1;
while (end < sorted.size() && reaches(runEndKey, sorted.get(end).getStartKey())) {
byte[] endKey = sorted.get(end).getEndKey();
if (endKey == null || UNSIGNED_BYTES_COMPARATOR.compare(endKey, runEndKey) > 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this'll throw if runEndKey is null, which'll happen if the first entry is [x, inf) and the second entry is finite. Remember to add tests for the fix.

}

/** A single {@code WatchShardingAssignment} stream. */
private final class AutoshardingStream

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: I understand that this follows precendent from other implementations like LrsStream etc , where we have stream handle all of the lifecycle and handle call as well as observing the responses.

I'd like you to explore composition here to break down responsibilities a bit where we separate the call and callobserver side of things. I haven't thought about it but this is what I'd come up with on an initial glance.

private final class AutoshardingStream {
  private final List<AssignmentChunk> bufferedChunks = new ArrayList<>();
  private ClientCallStreamObserver<WatchShardingAssignmentRequest> requestStream;
  private boolean receivedGoodAssignment;
  private boolean closed;
  void start() {
     // start the call using ResponseObserver
  }
  void close(Status status) { ... requestStream.cancel(...); }
  private final class ResponseObserver
      implements ClientResponseObserver<WatchShardingAssignmentRequest,
          WatchShardingAssignmentResponse> {
    @Override public void beforeStart(ClientCallStreamObserver<WatchShardingAssignmentRequest> rs) {
      requestStream = rs;
    }
    @Override public void onNext(WatchShardingAssignmentResponse r) {
      syncContext.execute(() -> handleResponse(r));
    }
    @Override public void onError(Throwable t) {
      syncContext.execute(() -> handleStreamClosed(Status.fromThrowable(t)));
    }
    @Override public void onCompleted() { ... }
  }
}

void start() {
// wait_for_ready keeps the stream pending through transient connectivity failures instead
// of failing it, which recovers faster than applying backoff around stream creation.
ClientCalls.asyncBidiStreamingCall(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why are we using the generic API instead of intantiating a stub and calling the method on the stub?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants