[router][thin-client] Add getAllStoreNames capability via /stores endpoint - #2704
Conversation
endpoint - Add TYPE_STORE_NAMES router resource type and handle it in MetaDataHandler using HelixReadOnlyStoreConfigRepository.getAvailableStoreNames() - Add getAvailableStoreNames() to ReadOnlyStoreConfigRepository interface with implementation in HelixReadOnlyStoreConfigRepository - Move MultiStoreResponse from venice-common to venice-client-common so venice-thin-client can use it without a compile-scope dependency on venice-common - Add StoreMetadataFetcher interface and RouterBasedStoreMetadataFetcher impl that calls the /store_names router endpoint - Add ClientFactory.createStoreMetadataFetcher() factory method mirroring createStoreSchemaFetcher() - Add unit tests for RouterBasedStoreMetadataFetcher and TestMetaDataHandler - Add integration test TestRouterBasedStoreMetadataFetcher using VeniceClusterWrapper Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…outer store propagation The router's HelixReadOnlyStoreConfigRepository updates its store set asynchronously via ZK watches, so getAllStoreNames() may not reflect newly created stores immediately. Wrap the assertion in waitForNonDeterministicAssertion to handle the propagation delay. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…efore querying Add empty pushes so stores have a version before asserting router visibility, rename test to testGetAllStoreNames, and use assertCommand for store creation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TestRouterBasedStoreMetadataFetcher Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TestRouterBasedStoreMetadataFetcher Set up a D2Client from the cluster's ZK address and use D2-based routing for both the generic avro clients and the StoreMetadataFetcher, matching the pattern used by other integration tests (e.g. TestDaVinciRequestBasedMetaRepository). Also bump the test timeout to 120s to account for two empty pushes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…m stores - Rename TYPE_STORE_NAMES -> TYPE_STORES and endpoint /store_names -> /stores - Replace getAvailableStoreNames() with getStores(boolean includeSystemStores) on ReadOnlyStoreConfigRepository, filtering out system stores by default - Update RouterBasedStoreMetadataFetcher, MetaDataHandler, and all tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cover both includeSystemStores=true and includeSystemStores=false branches. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
RouterStoreAclHandlerTest Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a router-backed mechanism for thin clients to fetch all store names across clusters via a new /stores metadata endpoint.
Changes:
- Extend
ReadOnlyStoreConfigRepositorywithgetStores(includeSystemStores)and implement system-store filtering inHelixReadOnlyStoreConfigRepository. - Add router
/storesendpoint (RouterResourceType.TYPE_STORES) returning aMultiStoreResponse. - Introduce thin-client
StoreMetadataFetcher+RouterBasedStoreMetadataFetcher, plus unit/integration test coverage and aClientFactoryconstructor.
Reviewed changes
Copilot reviewed 13 out of 14 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| services/venice-router/src/test/java/com/linkedin/venice/router/TestMetaDataHandler.java | Adds unit tests validating /stores response and empty-store behavior. |
| services/venice-router/src/test/java/com/linkedin/venice/router/acl/RouterStoreAclHandlerTest.java | Updates request-type coverage to include TYPE_STORES. |
| services/venice-router/src/main/java/com/linkedin/venice/router/MetaDataHandler.java | Implements /stores handling and returns MultiStoreResponse. |
| services/venice-router/src/main/java/com/linkedin/venice/router/api/RouterResourceType.java | Adds TYPE_STORES("stores") to router resource types. |
| internal/venice-test-common/src/integrationTest/java/com/linkedin/venice/client/store/TestRouterBasedStoreMetadataFetcher.java | Adds integration test for fetching store names via router over D2. |
| internal/venice-common/src/test/java/com/linkedin/venice/helix/TestHelixReadOnlyStoreConfigRepository.java | Adds tests for including/excluding system stores in getStores. |
| internal/venice-common/src/main/java/com/linkedin/venice/meta/ReadOnlyStoreConfigRepository.java | Adds getStores(boolean includeSystemStores) to the repository interface. |
| internal/venice-common/src/main/java/com/linkedin/venice/helix/HelixReadOnlyStoreViewConfigRepositoryAdapter.java | Delegates the new getStores API to the underlying repository. |
| internal/venice-common/src/main/java/com/linkedin/venice/helix/HelixReadOnlyStoreConfigRepository.java | Implements getStores and filters system stores by prefix. |
| internal/venice-client-common/src/main/java/com/linkedin/venice/controllerapi/MultiStoreResponse.java | Adds response model used by router + thin client for /stores. |
| clients/venice-thin-client/src/test/java/com/linkedin/venice/client/store/RouterBasedStoreMetadataFetcherTest.java | Adds unit tests for router-based store-name fetching and error cases. |
| clients/venice-thin-client/src/main/java/com/linkedin/venice/client/store/StoreMetadataFetcher.java | Introduces public interface for non-store/cluster-scoped metadata fetches. |
| clients/venice-thin-client/src/main/java/com/linkedin/venice/client/store/RouterBasedStoreMetadataFetcher.java | Implements getAllStoreNames() via GET /stores and JSON deserialization. |
| clients/venice-thin-client/src/main/java/com/linkedin/venice/client/store/ClientFactory.java | Adds createStoreMetadataFetcher(ClientConfig) factory method. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…etcher - Replace injected TransportClient with owned D2TransportClient; make TransportClient constructor package-private (VisibleForTesting) so the fetcher is always responsible for its own client lifecycle - Add null check for response body before JSON deserialization - Add unit test for null response body case - Close parentControllerClient in @afterclass of integration test Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…Fetcher Mirror the RouterBasedStoreSchemaFetcher pattern: accept an injected AbstractAvroStoreClient and call getRaw() with retry instead of owning a TransportClient directly. The caller retains ownership of the client lifecycle so close() is a no-op. Update ClientFactory and unit tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 14 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
RouterBasedStoreMetadataFetcher AbstractAvroStoreClient.getRaw() always calls discoverD2Service(), which hits discover_cluster/<storeName> on the router. Since this fetcher is cluster-agnostic and has no store name, that discovery request becomes discover_cluster/null, causing a VeniceNoStoreException after 10 retries. Fix: bypass AbstractAvroStoreClient and use D2TransportClient directly. The fetcher constructs its own D2TransportClient from the passed-in D2Client and d2ServiceName, so it owns the lifecycle and closes it properly. The package-private TransportClient constructor is kept for unit tests. Restore full unit test coverage including null body and close. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 15 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…ore set - Add 30s timeout to transportClient.get() in RouterBasedStoreMetadataFetcher to prevent indefinite hangs on network stalls; include path in exception message - Add timeout unit test - Cache filtered regular-store set in HelixReadOnlyStoreConfigRepository so getStores(false) is O(1) with no per-call allocation; kept in sync via refresh() and handleChildChange() Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 14 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Restore the design pulled in from linkedin#2704 discussion r3066176915: the public RouterBasedStoreMetadataFetcher constructor takes inputs the fetcher uses to build its own TransportClient (now ClientConfig instead of the original (D2Client, String)), so the fetcher owns the transport and close() is always safe. The (TransportClient) ctor becomes package-private VisibleForTesting again, matching the original intent — callers cannot pass an externally-owned transport that the fetcher would then close.
Restore the design pulled in from linkedin#2704 discussion r3066176915: the public RouterBasedStoreMetadataFetcher constructor takes inputs the fetcher uses to build its own TransportClient (now ClientConfig instead of the original (D2Client, String)), so the fetcher owns the transport and close() is always safe. The (TransportClient) ctor becomes package-private VisibleForTesting again, matching the original intent — callers cannot pass an externally-owned transport that the fetcher would then close.
The earlier "to avoid store-level D2 service discovery" wording was a holdover from linkedin#2704 and no longer matches the design — D2 is now just one of three supported transports. The actual reason for the direct TransportClient is that AbstractAvroStoreClient is store-scoped while /stores is cluster-agnostic. Reword and drop the stale D2 framing.
Restore the original public constructor signature from linkedin#2704 as a thin delegate that constructs and owns its own D2TransportClient. This keeps the API binary- and source-compatible for any caller that wired up RouterBasedStoreMetadataFetcher directly with a D2 client, while the new (ClientConfig) overload remains the preferred entry point for new code (it supports HTTP/HTTPS routing too).
Problem Statement
No way to query all store names across clusters from the thin client without
ControllerClient(cluster-specific).HelixReadOnlyStoreConfigRepositoryalready caches this data globally but it wasn't exposed via the router.Solution
getStores(boolean includeSystemStores)toReadOnlyStoreConfigRepositoryand implement inHelixReadOnlyStoreConfigRepositorywith system store filtering (venice_system_store_prefix).TYPE_STORES("stores")toRouterResourceTypeand handle inMetaDataHandler.handleStoresLookup(), returning aMultiStoreResponsewith regular stores only.StoreMetadataFetcherinterface andRouterBasedStoreMetadataFetcherthat callsGET /storesand deserializes the response.ClientFactory.createStoreMetadataFetcher(ClientConfig)factory method.D2 interaction
The fetcher takes a
D2Client+d2ServiceName, constructs its ownD2TransportClient(owns the lifecycle), and callsGET d2://<d2ServiceName>/storesdirectly. D2 resolves the service name via ZooKeeper, load-balances across live router instances, and the router returns aMultiStoreResponse.We use
D2TransportClientdirectly rather thanAbstractAvroStoreClientbecausegetRaw()unconditionally callsdiscoverD2Service(), hittingdiscover_cluster/<storeName>on the router. With no store name this becomesdiscover_cluster/null→ 404 →VeniceNoStoreExceptionafter 10 retries.Code changes
Concurrency-Specific Checks
Both reviewer and PR author to verify
synchronized,RWLock) are used where needed.ConcurrentHashMap,CopyOnWriteArrayList).How was this PR tested?
RouterBasedStoreMetadataFetcherTest(happy path, empty stores, router error, null response, null body, transport failure, close),TestHelixReadOnlyStoreConfigRepository(getStoreswith/without system stores).TestRouterBasedStoreMetadataFetcher— creates 2 stores viaVeniceTwoLayerMultiRegionMultiClusterWrapperover D2, verifiesgetAllStoreNames()returns both.TestMetaDataHandlerupdated for/storesendpoint.Does this PR introduce any user-facing or breaking changes?
StoreMetadataFetcherAPI accessible viaClientFactory.createStoreMetadataFetcher().