Skip to content

Conversation

@huangminchn
Copy link
Contributor

Problem Statement

The metadata endpoint was only available over HTTP at /metadata/{storeName}.

Solution

This adds an equivalent getMetadata RPC to the gRPC VeniceReadService so
clients using gRPC can fetch store metadata (schemas, versions, routing
info, helix group info) without falling back to HTTP.

The implementation delegates to the same ReadMetadataRetriever used by
the HTTP path, returning the Avro-serialized MetadataResponseRecord
bytes alongside the schema version ID. Error handling mirrors the HTTP
behavior: UnsupportedOperationException when read quota is not enabled
returns BAD_REQUEST, and other failures return INTERNAL_ERROR.

  • Add VeniceMetadataRequest/Response proto messages and getMetadata RPC
  • Pass ReadMetadataRetriever to VeniceReadServiceImpl via ListenerService
  • Add unit tests for success, error response, quota-disabled, and
    unexpected exception cases
  • Add integration tests replicating the HTTP testMetadataFetchRequest
    (3 servers, topology-aware routing, zone isolation verification) and
    a quota-disabled error case; besides, check that gRPC endpoint response
    matches the HTTP endpoint response

Code changes

  • Added new code behind a config. If so list the config names and their default values in the PR description.
  • Introduced new log lines.
  • Confirmed if logs need to be rate limited to avoid excessive logging.

Concurrency-Specific Checks

Both reviewer and PR author to verify

  • Code has no race conditions or thread safety issues.
  • Proper synchronization mechanisms (e.g., synchronized, RWLock) are used where needed.
  • No blocking calls inside critical sections that could lead to deadlocks or performance degradation.
  • Verified thread-safe collections are used (e.g., ConcurrentHashMap, CopyOnWriteArrayList).
  • Validated proper exception handling in multi-threaded code to avoid silent thread termination.

How was this PR tested?

  • New unit tests added.
  • New integration tests added.
  • Modified or extended existing tests.
  • Verified backward compatibility (if applicable).

Does this PR introduce any user-facing or breaking changes?

  • No. You can skip the rest of this section.
  • Yes. Clearly explain the behavior change and its impact - a new client facing endpoint is added; no changes to existing endpoint.

The metadata endpoint was only available over HTTP at /metadata/{storeName}.
This adds an equivalent getMetadata RPC to the gRPC VeniceReadService so
clients using gRPC can fetch store metadata (schemas, versions, routing
info, helix group info) without falling back to HTTP.

The implementation delegates to the same ReadMetadataRetriever used by
the HTTP path, returning the Avro-serialized MetadataResponseRecord
bytes alongside the schema version ID. Error handling mirrors the HTTP
behavior: UnsupportedOperationException when read quota is not enabled
returns BAD_REQUEST, and other failures return INTERNAL_ERROR.

- Add VeniceMetadataRequest/Response proto messages and getMetadata RPC
- Pass ReadMetadataRetriever to VeniceReadServiceImpl via ListenerService
- Add unit tests for success, error response, quota-disabled, and
  unexpected exception cases
- Add integration tests replicating the HTTP testMetadataFetchRequest
  (6 servers, topology-aware routing, zone isolation verification) and
  a quota-disabled error case
…se body

from HTTP endpoint and grpc endpoint
Copilot AI review requested due to automatic review settings February 10, 2026 00:43
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new getMetadata RPC to the server-side VeniceReadService so gRPC clients can fetch the same store metadata currently available via the HTTP /metadata/{storeName} endpoint.

Changes:

  • Extends VeniceReadService.proto with VeniceMetadataRequest/Response and a getMetadata RPC.
  • Implements getMetadata in VeniceReadServiceImpl by delegating to ReadMetadataRetriever and returning Avro-serialized MetadataResponseRecord bytes + schema id.
  • Adds unit + integration tests covering success and error scenarios, including parity checks vs HTTP.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
internal/venice-common/src/main/proto/VeniceReadService.proto Adds new metadata RPC and proto messages.
services/venice-server/src/main/java/com/linkedin/venice/listener/grpc/VeniceReadServiceImpl.java Implements getMetadata and maps errors to VeniceReadResponseStatus.
services/venice-server/src/main/java/com/linkedin/venice/listener/ListenerService.java Injects ReadMetadataRetriever into the gRPC service implementation.
services/venice-server/src/test/java/com/linkedin/venice/grpc/VeniceReadServiceImplTest.java Unit tests for gRPC metadata success and error handling.
internal/venice-test-common/src/integrationTest/java/com/linkedin/venice/server/VeniceServerTest.java Integration tests for gRPC metadata parity with HTTP and quota-disabled behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +46 to +52
@Override
public void getMetadata(VeniceMetadataRequest request, StreamObserver<VeniceMetadataResponse> responseObserver) {
VeniceMetadataResponse.Builder responseBuilder = VeniceMetadataResponse.newBuilder();
try {
String storeName = request.getStoreName();
MetadataResponse metadataResponse = readMetadataRetriever.getMetadata(storeName);

Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

gRPC server interceptors currently assume every request message is a VeniceClientRequest (e.g., ServerAclHandler and ServerStoreAclHandler cast ReqT to VeniceClientRequest in onMessage). Since this RPC uses VeniceMetadataRequest, enabling SSL/ACL interceptors will likely trigger a ClassCastException before this method executes. Please update the interceptors to handle/skip VeniceMetadataRequest (e.g., instanceof guard + allowlist), or register method-specific interceptors so non-storage RPCs aren't intercepted by VeniceClientRequest-only logic.

Copilot uses AI. Check for mistakes.
} catch (UnsupportedOperationException e) {
// This happens when storageNodeReadQuotaEnabled is false for the store. The metadata endpoint is
// designed for the fast client, which requires read quota enforcement on storage nodes.
responseBuilder.setErrorCode(VeniceReadResponseStatus.BAD_REQUEST).setErrorMessage(e.getMessage());
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

The quota-disabled path maps UnsupportedOperationException to VeniceReadResponseStatus.BAD_REQUEST (400), but the HTTP /metadata/{storeName} endpoint currently returns 403 FORBIDDEN for the same condition (see StorageReadRequestHandler catching UnsupportedOperationException and writing a FORBIDDEN response). If the goal is parity with HTTP semantics, consider returning 403 here as well (and ideally adding a shared constant), or update the parity claim/tests to reflect the intentional difference.

Suggested change
responseBuilder.setErrorCode(VeniceReadResponseStatus.BAD_REQUEST).setErrorMessage(e.getMessage());
responseBuilder.setErrorCode(VeniceReadResponseStatus.FORBIDDEN).setErrorMessage(e.getMessage());

Copilot uses AI. Check for mistakes.
Comment on lines +62 to +66
// Verify that getResponseBody() returns a heap-backed ByteBuf so that nioBuffer() is zero-copy
assertTrue(
expectedResponse.getResponseBody().hasArray(),
"ByteBuf should be heap-backed for zero-copy nioBuffer()");

Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

This test enforces that MetadataResponse.getResponseBody() must be heap-backed (hasArray()), but the gRPC implementation does not actually require that (and ByteString.copyFrom(ByteBuffer) still copies regardless). This assertion makes the test brittle against future MetadataResponse/Netty buffer changes without validating functional behavior; consider removing it or replacing it with an assertion on the produced metadata bytes/content.

Copilot uses AI. Check for mistakes.
Comment on lines +495 to +502
// --- Verify HTTP and gRPC return identical Avro bytes ---
Assert.assertEquals(grpcBody, httpBody, "HTTP and gRPC metadata responses differ for server " + i);

// Deserialize and verify content
RecordDeserializer<MetadataResponseRecord> deserializer =
SerializerDeserializerFactory.getAvroGenericDeserializer(MetadataResponseRecord.SCHEMA$);
GenericRecord metadataResponse = deserializer.deserialize(grpcBody);

Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

The assertion compares raw Avro-serialized bytes between two separate metadata requests (HTTP vs gRPC). The response includes multiple map fields that are populated with HashMap in ServerReadMetadataRepository; Avro map entry ordering is iteration-order dependent and not guaranteed to be stable across calls. This can make the test flaky even when the decoded records are equivalent; consider deserializing both bodies and comparing the resulting MetadataResponseRecord/GenericRecord contents (or comparing a normalized form) instead of raw bytes.

Suggested change
// --- Verify HTTP and gRPC return identical Avro bytes ---
Assert.assertEquals(grpcBody, httpBody, "HTTP and gRPC metadata responses differ for server " + i);
// Deserialize and verify content
RecordDeserializer<MetadataResponseRecord> deserializer =
SerializerDeserializerFactory.getAvroGenericDeserializer(MetadataResponseRecord.SCHEMA$);
GenericRecord metadataResponse = deserializer.deserialize(grpcBody);
// --- Verify HTTP and gRPC return identical Avro content (ignoring map entry order) ---
RecordDeserializer<MetadataResponseRecord> deserializer =
SerializerDeserializerFactory.getAvroGenericDeserializer(MetadataResponseRecord.SCHEMA$);
GenericRecord grpcMetadataResponse = deserializer.deserialize(grpcBody);
GenericRecord httpMetadataResponse = deserializer.deserialize(httpBody);
Assert.assertEquals(
grpcMetadataResponse,
httpMetadataResponse,
"HTTP and gRPC metadata responses differ for server " + i);
// Deserialize and verify content using the gRPC response
GenericRecord metadataResponse = grpcMetadataResponse;

Copilot uses AI. Check for mistakes.
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.

1 participant