Controller Cross-Region Fan-Out and Retry Behavior#
This page documents how the Venice controller talks to other regions/fabrics, and the (currently non-uniform) retry behavior of each cross-region call site. It is a reference for anyone touching the parent/child controllers or the cross-region coordination layer.
Line numbers below are indicative and drift as the files change; the stable anchor is the method name. Search for the method in
VeniceParentHelixAdmin(parent) orVeniceHelixAdmin(child).
Where the controller clients come from#
Both controllers reach other regions through ControllerClient instances keyed by region/fabric. Ownership of those client maps is centralized in FabricControllerClientProvider:
getControllerClientMap(clusterName)— the standard per-cluster/per-fabric map (built from the child data-center URL and D2 allowlists).getFabricBuildoutControllerClient(clusterName, fabric)— a single client for a fabric that may be outside the standard allowlist (build-out / data-recovery destinations), cached separately.
VeniceHelixAdmin (the child) constructs one provider and exposes it via getFabricControllerClientProvider(); VeniceParentHelixAdmin (the parent) shares the same instance. The child's getControllerClientMap(...) delegates to the provider, and ~25 production call sites plus the parent's fan-out methods draw clients from it.
The fan-out helper#
Most "ask every region the same thing" loops follow one best-effort shape, captured by FabricControllerClientProvider.queryAllRegions(...):
<R extends ControllerResponse, V> Map<String, V> queryAllRegions(
Map<String, ControllerClient> controllerClients,
String clusterName,
int maxAttempts, // 1 = no retry; >1 wraps in ControllerClient.retryableRequest
Function<ControllerClient, R> request, // the RPC to issue per region
Function<R, V> onSuccess, // map a successful response to a per-region value
V errorSentinel) // value stored for a region whose query errored
For each region it runs request (retried when maxAttempts > 1), and on a per-region error it logs and stores errorSentinel rather than aborting. The parent's multi-fabric version queries (getCurrentVersionForMultiRegions, getFutureVersionsForMultiColos, getBackupVersionsForMultiColos) use it. The remaining fan-out sites still hand-roll their loops because their error policy is bespoke (fail-fast, throw, region-filtered, error-collecting, first-success-wins, fold/max, etc.).
The two retry engines#
There is no single retry policy. Cross-region calls that retry use one of two mechanisms, with different semantics:
1. ControllerClient.retryableRequest(client, totalAttempts, request)#
- Fixed delay of
Utils.sleep(2000)(2 s) between attempts — not exponential. Five attempts therefore cost up to ~8 s of sleeping on the unhappy path. - Stops early on success, on a "value schema not found" response, or when an optional
abortRetryConditionreturns true (the overload used byqueryAllRegionspassesr -> false). - After exhausting attempts: throws
VeniceExceptionif the request threw an exception, but returns the last error response if it merely returnedisError().
2. RetryUtils.executeWithMaxAttemptAndExponentialBackoff(op, attempts, initialDelay, maxDelay, maxTotalDelay, retryFailureTypes)#
- True exponential backoff, bounded by
maxDelayandmaxTotalDelay. - Retries only on the listed exception types; other exceptions propagate immediately.
- Used by sites whose body throws a sentinel exception to signal "not done yet, retry the whole region sweep."
Retry catalog#
Fan-out sites that retry (all use 5 attempts)#
| Method | Class:line | RPC | Engine | Backoff / cost | Error policy |
|---|---|---|---|---|---|
getFutureVersionsForMultiColos | Parent:2264 | getFutureVersions | queryAllRegions(5) → retryableRequest | fixed 2 s between (~8 s) | sentinel |
isActiveActiveReplicationEnabledInAllRegion | Parent:1702 | getStore | retryableRequest(5) | fixed 2 s between (~8 s) | log+continue; throw on A/A mismatch |
rollForwardToFutureVersion | Parent:2446 | rollForwardToFutureVersion | RetryUtils exp-backoff (5) | 100 ms → 500 ms cap, 10 s max | throw; track failedRegions |
pollChildRegionsForRollbackStatus | Parent:2603 | getStore | RetryUtils exp-backoff (5) | 1 s → 10 s cap, 30 s max | fold + full re-poll |
isRTTopicDeletionPermittedByAllControllers | Child:4267 | getStore | RetryUtils exp-backoff (5) | 10 ms → 500 ms cap, 5 s max | skip-missing; fail-fast → false |
Fan-out sites with no retry (single attempt)#
| Method | Class:line | RPC | Error policy |
|---|---|---|---|
getCurrentVersionForMultiRegions | Parent:2301 | getStore | sentinel |
getBackupVersionsForMultiColos | Parent:2276 | getBackupVersions | sentinel |
getInUseValueSchemaIds | Parent:764 | getInUseSchemaIds | fail-fast → empty set |
getOffLineJobStatus | Parent:3451 | getLeaderControllerUrl / queryJobStatus | log → UNKNOWN, aggregate |
getClusterStaleStores | Parent:4835 | getClusterStores | throw |
getLargestUsedVersionFromStoreGraveyard | Parent:4906 | getStoreLargestUsedVersion | fold/max |
getLargestUsedVersion | Parent:4919 | getStoreLargestUsedVersion | fold/max |
listStorePushInfo | Parent:4955 | getRegionPushDetails | skip null/continue |
checkResourceCleanupBeforeStoreCreation | Parent:4982 | checkResourceCleanupForStoreCreation | fail-fast throw |
removeStoreFromGraveyard | Parent:5519 | removeStoreFromGraveyard | fail-fast throw |
validateStoreDeleted | Parent:5585 | validateStoreDeleted | collect errors |
sendPushJobDetails | Child:1623 | sendPushJobDetails | first-success-wins |
getStoreInfoInChildColos | Child:1963 | getStore | fail-fast throw |
deleteRTTopicFromAllFabrics | Child:4335 | deleteKafkaTopic | log + continue |
Single-region calls (pick one region's client) — all no-retry#
getRepushInfo (Parent), getCurrentVersionInRegion (Parent), getOffLinePushStatus single-region path (Parent), getStoreInChildRegion (Parent), initiateDataRecovery / prepareDataRecovery / isStoreVersionReadyForDataRecovery (Parent), wipeCluster (Parent), compareStore (Parent), copyOverStoreSchemasAndConfigs (Parent), getStoreInfo (Child), and the store-migration source lookup (Child). Each issues a single attempt; the error policy is throw, except isStoreVersionReadyForDataRecovery (collect) and getCurrentVersionInRegion (returns -1).
Known inconsistencies and future work#
- The three sibling version queries disagree on retry:
getFutureVersionsForMultiColosretries (5× / fixed 2 s), whilegetCurrentVersionForMultiRegionsandgetBackupVersionsForMultiColosdo not. The future-version retry was added deliberately (it gates a "don't start a push when a future version exists" decision), so unifying is a semantic choice, not a pure cleanup. Because all three now go throughqueryAllRegions, changing a policy is a one-number (maxAttempts) edit per site. - Two engines, four profiles: any "unify retry" effort must pick both an engine (fixed-delay
retryableRequestvs exponentialRetryUtils) and a delay profile; the five retrying sites currently use four different profiles. - Bespoke loops remain hand-rolled: only the three uniform best-effort version queries were migrated to
queryAllRegions; the rest keep their own loops because their error policy differs. Consolidating them further would require a more configurable helper and careful behavior preservation.