[da-vinci] Lazily discover server blob fallback peers - #2938
Conversation
There was a problem hiding this comment.
Pull request overview
This PR reduces storage-node load during Da Vinci cold starts by changing blob peer discovery from an eager “Da Vinci + server” fetch to a lazy, tiered approach: try Da Vinci peers first, and only discover Venice server peers when needed (and skip fallback if the transfer is cancelled).
Changes:
- Introduces a fallback-capable
BlobFindercontract (supportsFallback+discoverFallbackBlobPeers) and updatesServerAndDaVinciBlobFinderto only discover server peers on-demand. - Updates
NettyP2PBlobTransferManagerto process primary peers first, then trigger fallback discovery only after primary peers are exhausted or unavailable, with cancellation-aware short-circuiting. - Expands unit tests to cover fallback-after-failure, no-fallback-after-success, fallback-when-no-primary-peers, and cancellation behavior.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| internal/venice-common/src/test/java/com/linkedin/venice/blobtransfer/ServerAndDaVinciBlobFinderTest.java | Updates tests for the new “primary-only + explicit fallback” discovery API. |
| internal/venice-common/src/main/java/com/linkedin/venice/blobtransfer/ServerAndDaVinciBlobFinder.java | Makes server peer discovery lazy via discoverFallbackBlobPeers. |
| internal/venice-common/src/main/java/com/linkedin/venice/blobtransfer/BlobFinder.java | Adds fallback discovery methods to the BlobFinder contract. |
| clients/da-vinci-client/src/test/java/com/linkedin/davinci/blobtransfer/TestNettyP2PBlobTransferManager.java | Adds test coverage for tiered discovery and cancellation interactions. |
| clients/da-vinci-client/src/main/java/com/linkedin/davinci/blobtransfer/NettyP2PBlobTransferManager.java | Implements tiered primary/fallback discovery and cancellation-aware fallback suppression. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Lazy fallback discovery runs on the replica blob fetch executor, so failures that the eager implementation could not hit are now reachable and would leave the partition transfer future uncompleted, stalling the replica instead of falling back to Kafka bootstrapping. - Guard the fallback dispatch and discovery body so a rejected execution or an unexpected runtime failure still resolves the future. - Observe the peer chain with exceptionally(): a chain that completes exceptionally (for example the executor rejecting a queued host after close()) never reaches thenRun, which previously hung the transfer on both the fallback and the pre-existing single-tier path. - Report a requested cancellation from those handlers rather than claiming every peer was tried, matching the normal peer-exhaustion path. - Log the fallback discovery error message instead of dropping it. - Restore per-pass transfer timing so blob transfer time and throughput metrics keep their previous meaning.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
clients/da-vinci-client/src/main/java/com/linkedin/davinci/blobtransfer/NettyP2PBlobTransferManager.java:136
processFallbackPeers(...)can run inline on the caller thread when primary discovery is unavailable (lines 129-135). That method immediately callspeerFinder.discoverFallbackBlobPeersIfEnabled(...), which may perform synchronous network/metadata fetch work (e.g.,MetadataBasedServerBlobFinder.discoverBlobPeersblocks on metadata fetch). This reintroduces the same risk this class documents for Netty event-loop threads (see the thenComposeAsync rationale) and also bypasses the rejection-guard logic used when fallback is triggered after primary peers are exhausted.
Dispatch fallback discovery onto replicaBlobFetchExecutor in this branch as well, and handle RejectedExecutionException/other RuntimeException the same way as the peers-exhausted path so the partition future is always resolved.
if (isDiscoveryUnavailable(response)) {
if (peerFinder.supportsFallback()) {
processFallbackPeers(storeName, version, partition, tableFormat, perPartitionTransferFuture, false);
} else {
completeWithNoPeersFound(replicaId, perPartitionTransferFuture);
Problem Statement
ServerAndDaVinciBlobFindereagerly fetched Venice server metadata for every partition, even when a Da Vinci peer was available and could serve the blob. The server metadata response contains routing and schema information for the entire store, so eager requests during a partitioned cold start can add unnecessary storage-node load and affect Fast Client read latency.Solution
Introduce lazy, tiered peer discovery:
The behavior remains gated by the existing
davinci.blob.transfer.server.fallback.enabledconfig, which defaults tofalse.Always resolving the partition transfer future
Fallback discovery is dispatched onto
replicaBlobFetchExecutorrather than running inline inget(), which makes failure paths reachable that the eager implementation could not hit. A partition transfer future that is never completed stalls the replica forever, becauseStoreIngestionTaskwaits on it to decide whether to fall back to Kafka bootstrapping. Each path is now resolved explicitly:close()shutting the executor down) or an unexpected runtime failure still completes the future.exceptionally(). A chain that completes exceptionally never reachesthenRun, so the transfer previously hung. This also covers the pre-existing single-tier path, where the same window existed but was only reachable during shutdown.Blob transfer time and throughput are still measured per discovery pass, so
recordBlobTransferTimeInSecandrecordBlobTransferFileReceiveThroughputkeep their existing meaning on the default (fallback-disabled) path.Code changes
davinci.blob.transfer.server.fallback.enabled(default:false).Concurrency-Specific Checks
Both reviewer and PR author to verify
synchronized,RWLock) are used where needed. State is confined to the per-partitionCompletableFuture, whose completion is idempotent, so no additional locking is introduced.replicaBlobFetchExecutorinstead of a Netty event-loop thread, consistent with the existingthenComposeAsyncrationale.ConcurrentHashMap,CopyOnWriteArrayList).Erroris deliberately left to propagate.How was this PR tested?
New
TestNettyP2PBlobTransferManagercases:testDoesNotDiscoverFallbackAfterPrimaryTransferSucceeds- server discovery is skipped when a Da Vinci peer serves the blob.testDiscoversFallbackWhenNoPrimaryPeersExistandtestFallsBackFromDaVinciPeerToServerAfterTransferFailure- both fallback triggers, asserting discovery and transfer ordering.testCancellationDoesNotDiscoverFallback- a cancelled transfer stops before fallback discovery.testFallbackDiscoveryErrorAfterPrimaryPeersFail- fallback discovery returning an error surfacesVenicePeersAllFailedException.testFallbackDispatchRejectionFailsTransferandtestPeerChainRejectionMidFlightFailsTransfer- both executor-rejection windows resolve the future instead of hanging.testCancellationWinsOverChainRejection- a rejection racing a cancellation reportsVeniceBlobTransferCancelledException.Each new failure-path test was confirmed to fail against the unfixed code before the fix was applied.
Commands:
./gradlew :internal:venice-common:test --tests 'com.linkedin.venice.blobtransfer.ServerAndDaVinciBlobFinderTest'./gradlew :clients:da-vinci-client:test --tests 'com.linkedin.davinci.blobtransfer.TestNettyP2PBlobTransferManager'./gradlew spotlessCheckDoes this PR introduce any user-facing or breaking changes?