[vpj][controller][protocol][build] Report push job write timings - #2972
[vpj][controller][protocol][build] Report push job write timings#2972ymuppala wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR activates PushJobDetails v6 to report per-sink data-writer write timings (external storage vs Venice), emits controller metrics (Tehuti + OTel histogram) for those timings with deduplication, and adds/extends VPJ + controller plumbing (including a targeted per-version StorageMode mutation API used by fail-open regional external write).
Changes:
- Activate
PushJobDetailsv6 and verify schema compatibility/rollout safety via tests. - Track and aggregate per-task write durations in VPJ (MR + Spark) and surface them through push job status reporting.
- Emit controller metrics for per-sink write timings (terminal-only,
-1skipped, deduped by push) and add an admin route to downgrade only a version’sStorageModein selected regions.
Reviewed changes
Copilot reviewed 47 out of 47 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| services/venice-controller/src/test/java/com/linkedin/venice/controller/TestVeniceParentHelixAdmin.java | Adds parent-admin tests for targeted per-region version StorageMode updates and error propagation. |
| services/venice-controller/src/test/java/com/linkedin/venice/controller/TestVeniceHelixAdmin.java | Adds helix-admin tests for version-only StorageMode mutation and region-filter skip behavior. |
| services/venice-controller/src/test/java/com/linkedin/venice/controller/TestPushJobStatusStats.java | Extends tests for terminal-only timing emission, -1 skipping, incremental dimensioning, and dedup. |
| services/venice-controller/src/test/java/com/linkedin/venice/controller/stats/PushJobTehutiMetricNameEnumTest.java | Verifies new Tehuti metric names for per-sink write times. |
| services/venice-controller/src/test/java/com/linkedin/venice/controller/stats/PushJobStatusStatsOtelTest.java | Adds OTel histogram tests for per-sink write time metrics and dimensioning. |
| services/venice-controller/src/test/java/com/linkedin/venice/controller/stats/PushJobOtelMetricEntityTest.java | Verifies new OTel metric entity definition and dimensions. |
| services/venice-controller/src/test/java/com/linkedin/venice/controller/server/StoresRoutesTest.java | Adds route tests for updating version StorageMode and access control behavior. |
| services/venice-controller/src/main/java/com/linkedin/venice/controller/VeniceParentHelixAdmin.java | Implements parent fan-out call to child controllers for version StorageMode update with region filtering. |
| services/venice-controller/src/main/java/com/linkedin/venice/controller/VeniceHelixAdmin.java | Adds write-time dedup cache, emits per-sink timing metrics on terminal status, and implements version StorageMode update with region filter. |
| services/venice-controller/src/main/java/com/linkedin/venice/controller/stats/PushJobStatusStats.java | Adds per-sink write-time metric recording (Tehuti Avg/Max + OTel histogram w/ sink dimension). |
| services/venice-controller/src/main/java/com/linkedin/venice/controller/server/StoresRoutes.java | Adds /update_store_version_storage_mode route handler. |
| services/venice-controller/src/main/java/com/linkedin/venice/controller/server/AdminSparkServer.java | Registers the new update-version-storage-mode route. |
| services/venice-controller/src/main/java/com/linkedin/venice/controller/Admin.java | Adds Admin API for per-version StorageMode update with region filter. |
| internal/venice-test-common/src/integrationTest/java/com/linkedin/venice/endToEnd/TestVPJDualWriteExternalStorageMultiRegion.java | Adds integration coverage for fail-open when one region exhausts external retries. |
| internal/venice-test-common/src/integrationTest/java/com/linkedin/venice/endToEnd/InMemoryExternalStorageWriter.java | Adds failure injection + attempt counting for integration tests. |
| internal/venice-common/src/test/java/com/linkedin/venice/status/protocol/TestPushJobDetailsSchemaCompatibility.java | Updates schema tests to validate v6 activation, appended fields only, and v5→v6 default resolution. |
| internal/venice-common/src/test/java/com/linkedin/venice/meta/ReadOnlyStoreTest.java | Adds tests for setVersionStorageMode semantics and read-only behavior. |
| internal/venice-common/src/test/java/com/linkedin/venice/controllerapi/ControllerRouteDimensionTest.java | Registers the new controller route for dimension tests. |
| internal/venice-common/src/main/resources/avro/PushJobDetails/v6/PushJobDetails.avsc | Introduces v6 schema with appended timing fields and -1 defaults. |
| internal/venice-common/src/main/java/com/linkedin/venice/serialization/avro/AvroProtocolDefinition.java | Bumps PUSH_JOB_DETAILS active protocol version to 6 with deployment-ordering documentation. |
| internal/venice-common/src/main/java/com/linkedin/venice/meta/Store.java | Adds setVersionStorageMode to Store interface (default no-op). |
| internal/venice-common/src/main/java/com/linkedin/venice/meta/ReadOnlyStore.java | Overrides setVersionStorageMode to throw UnsupportedOperationException. |
| internal/venice-common/src/main/java/com/linkedin/venice/meta/AbstractStore.java | Implements setVersionStorageMode mutation on backing store versions. |
| internal/venice-common/src/main/java/com/linkedin/venice/controllerapi/ControllerRoute.java | Adds UPDATE_STORE_VERSION_STORAGE_MODE route definition. |
| internal/venice-common/src/main/java/com/linkedin/venice/controllerapi/ControllerClient.java | Adds client methods to call update-version-storage-mode API. |
| internal/venice-client-common/src/test/java/com/linkedin/venice/stats/dimensions/VenicePushJobDataWriterSinkTest.java | Adds dimension enum tests for new sink dimension values. |
| internal/venice-client-common/src/test/java/com/linkedin/venice/stats/dimensions/VeniceMetricsDimensionsTest.java | Verifies naming formats for new VENICE_PUSH_JOB_DATA_WRITER_SINK dimension. |
| internal/venice-client-common/src/main/java/com/linkedin/venice/stats/dimensions/VenicePushJobDataWriterSink.java | Adds sink dimension enum (venice / external_storage). |
| internal/venice-client-common/src/main/java/com/linkedin/venice/stats/dimensions/VeniceMetricsDimensions.java | Adds VENICE_PUSH_JOB_DATA_WRITER_SINK dimension constant. |
| clients/venice-push-job/src/test/java/com/linkedin/venice/spark/datawriter/task/SparkDataWriterTaskTrackerTest.java | Adds Spark tracker tests for failed regions + write-time accumulation semantics. |
| clients/venice-push-job/src/test/java/com/linkedin/venice/spark/datawriter/jobs/AbstractDataWriterSparkJobTest.java | Adds Spark job tests for task-output aggregation of failed regions and timings. |
| clients/venice-push-job/src/test/java/com/linkedin/venice/hadoop/VenicePushJobLifecycleTest.java | Adds VPJ lifecycle tests for regional downgrade-before-EOP and timing fields in details. |
| clients/venice-push-job/src/test/java/com/linkedin/venice/hadoop/task/datawriter/DualWriteVeniceWriterTest.java | Adds unit tests for fail-open behavior and per-leg timing accounting. |
| clients/venice-push-job/src/test/java/com/linkedin/venice/hadoop/mapreduce/datawriter/task/MapReduceDataWriterTaskTrackerTest.java | Adds MR tracker tests for counter-based region failures and timing counters. |
| clients/venice-push-job/src/main/java/com/linkedin/venice/vpj/VenicePushJobConstants.java | Adds config key for fail-open-on-region-failure behavior. |
| clients/venice-push-job/src/main/java/com/linkedin/venice/spark/SparkConstants.java | Extends Spark task-output schema to carry failed regions + per-task timing totals. |
| clients/venice-push-job/src/main/java/com/linkedin/venice/spark/datawriter/writer/SparkPartitionWriterFactory.java | Emits expanded Spark task-output row including failed regions and timing totals. |
| clients/venice-push-job/src/main/java/com/linkedin/venice/spark/datawriter/writer/SparkPartitionWriter.java | Surfaces failed regions + timing totals from the task tracker. |
| clients/venice-push-job/src/main/java/com/linkedin/venice/spark/datawriter/task/SparkDataWriterTaskTracker.java | Adds per-task failed-region tracking and non-accumulator timing aggregation for speculative safety. |
| clients/venice-push-job/src/main/java/com/linkedin/venice/spark/datawriter/jobs/AbstractDataWriterSparkJob.java | Aggregates failed regions + timings from collected task-output rows into driver task tracker. |
| clients/venice-push-job/src/main/java/com/linkedin/venice/hadoop/VenicePushJob.java | Populates v6 timing fields, downgrades failed regions’ version StorageMode before EOP, and logs timing summary. |
| clients/venice-push-job/src/main/java/com/linkedin/venice/hadoop/task/datawriter/DualWriteVeniceWriter.java | Implements per-leg timing measurement, fail-open disabling per region, and tracker reporting. |
| clients/venice-push-job/src/main/java/com/linkedin/venice/hadoop/task/datawriter/DataWriterTaskTracker.java | Adds tracker APIs for per-leg timing and failed external regions reporting. |
| clients/venice-push-job/src/main/java/com/linkedin/venice/hadoop/task/datawriter/AbstractPartitionWriter.java | Wires fail-open config and region-aware DualWrite writer construction with tracker. |
| clients/venice-push-job/src/main/java/com/linkedin/venice/hadoop/mapreduce/datawriter/task/ReporterBackedMapReduceDataWriterTaskTracker.java | Reports failed regions and per-leg timings to MR counters and exposes snapshot accessors. |
| clients/venice-push-job/src/main/java/com/linkedin/venice/hadoop/mapreduce/datawriter/task/CounterBackedMapReduceDataWriterTaskTracker.java | Reads failed regions + timing totals from MR counters on the driver side. |
| clients/venice-push-job/src/main/java/com/linkedin/venice/hadoop/mapreduce/counter/MRJobCounterHelper.java | Adds MR counter group/counters to transport failed regions and per-leg write-time totals. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 47 out of 47 changed files in this pull request and generated no new comments.
Suppressed comments (2)
services/venice-controller/src/main/java/com/linkedin/venice/controller/VeniceHelixAdmin.java:1651
- The dedup check for data-writer sink write-time metrics is not atomic:
getIfPresent+putcan race under concurrent processing of duplicate terminal PushJobDetails and record the histogram twice. Use an atomicputIfAbsentviaCache#asMap()(orcache.get(key, ...)) to guarantee at-most-once emission per dedup key.
internal/venice-common/src/main/java/com/linkedin/venice/controllerapi/ControllerRoute.java:292 UPDATE_STORE_VERSION_STORAGE_MODEdoes not declareCLUSTERas a required param, but the server handler readsrequest.queryParams(CLUSTER)and passes it toAdmin#updateStoreVersionStorageMode. This means requests that omitclusterwill passvalidateParamsbut fail later (or behave unexpectedly). AddCLUSTERto the route’s required params so validation matches actual handler requirements.
UPDATE_STORE_VERSION_STORAGE_MODE(
"/update_store_version_storage_mode", HttpMethod.POST, Arrays.asList(NAME, VERSION, STORAGE_MODE), REGIONS_FILTER
),
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 29 out of 29 changed files in this pull request and generated no new comments.
Suppressed comments (1)
services/venice-controller/src/main/java/com/linkedin/venice/controller/VeniceHelixAdmin.java:1651
- Dedup is implemented as a non-atomic
getIfPresent+put. If two threads process the same terminal PushJobDetails concurrently, both can pass the check and record duplicate histogram observations. Use an atomicputIfAbsentviacache.asMap()(or Caffeine'scache.get(key, mappingFunction)) to make the dedup thread-safe.
if (dataWriterSinkWriteTimeEmittedPushIds != null) {
String dedupKey = storeName + "_v" + pushJobDetailsKey.getVersionNumber() + "_"
+ (pushJobDetailsValue.getPushId() == null ? "" : pushJobDetailsValue.getPushId().toString());
if (dataWriterSinkWriteTimeEmittedPushIds.getIfPresent(dedupKey) != null) {
return;
}
dataWriterSinkWriteTimeEmittedPushIds.put(dedupKey, Boolean.TRUE);
}
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 41 out of 41 changed files in this pull request and generated no new comments.
Suppressed comments (2)
services/venice-controller/src/main/java/com/linkedin/venice/controller/VeniceHelixAdmin.java:1653
- The data-writer sink write-time dedup key does not include the cluster name. Since a single controller process can host multiple clusters, pushes in different clusters with the same store/version/pushId can collide and incorrectly suppress histogram observations in the later cluster.
services/venice-controller/src/main/java/com/linkedin/venice/controller/VeniceHelixAdmin.java:5968 - The fail-open counter is gated only on transitioning to INTERNAL, but not on the previous storage mode. As written, an EXTERNAL -> INTERNAL transition with reason=EXTERNAL_WRITE_FAILURE would also be counted, even though it does not represent "losing the external-storage copy" from a dual-write version. If this metric is intended specifically for dual-write fail-open, it should only fire when downgrading from DUAL_WRITE to INTERNAL.
1168b6a to
68dc21f
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 41 out of 42 changed files in this pull request and generated no new comments.
Suppressed comments (2)
services/venice-controller/src/main/java/com/linkedin/venice/controller/VeniceHelixAdmin.java:1662
- The dedup cache key omits the cluster name, so two clusters with the same store/version/pushId can incorrectly suppress metrics for one another. Also,
getIfPresentfollowed byputis not atomic, so concurrent duplicate terminal reports can still double-record. Include the cluster in the key and use an atomicputIfAbsentoncache.asMap().
internal/venice-common/src/main/java/com/linkedin/venice/serialization/avro/AvroProtocolDefinition.java:70 - The deployment-ordering comment is out of date: PushJobDetails v6 appends the nullable
additionalPushMetricsmap (defaultnull), not “two long fields with -1 defaults”. This matters for rollout semantics (v6 readers resolve v5 tonull, and v5 readers ignore the extra field). Please update the comment to match the actual schema and tests.
* <p><b>Deployment ordering.</b> This is a system-store value schema, so controllers must have registered
* v6 (which they do on startup, from the resources of the venice-common they were built with) <em>before</em>
* any push job serializes a v6 payload. Rolling out a VPJ built from this commit against a controller fleet
* still on v5 would make the controller reject the write. The safe order is: deploy controllers first, then
* the push job. v6 only appends two long fields with defaults, so a v5 reader can still read a v6 record and
* a v6 reader resolves a v5 record's missing fields to the -1 defaults.
…external write failures per region Activates PushJobDetails v6 and makes the push job report, per push, how much time its data writer tasks spent in the external storage write path versus the Venice write path, so a slow push can be attributed to the right system without re-running it. Also alerts when a push exhausts its external-storage write retries and fails open in a region. Protocol activation: removes the PushJobDetails v5 versionOverrides pin added when v6 was staged, and bumps AvroProtocolDefinition.PUSH_JOB_DETAILS to 6, so the push job now serializes v6. This requires the schema registration PR to be deployed to the controllers first, otherwise the controller rejects the v6 payload. VPJ side: DualWriteVeniceWriter measures the external write path (throttling wait, batchPut including retries and retry backoff, external flush and external close) and the Venice write path (the Venice/Kafka put calls plus the Venice writer flush and close), and reports both through DataWriterTaskTracker. Both transports carry the two durations: MapReduce through new counters in MRJobCounterHelper, Spark through new accumulators in SparkConstants wired via SparkPartitionWriter(Factory) and AbstractDataWriterSparkJob. VenicePushJob sums the per-task values across successful task outputs and populates the two PushJobDetails fields, or leaves them at -1 when nothing reported. Controller side: VeniceHelixAdmin records the two durations from PushJobDetails, only for succeeded terminal pushes, skipping the -1 sentinel so an unreported push is not counted as a zero, and de-duplicating repeated reports for the same push (atomic putIfAbsent after a successful record, not before) so a retried report or a missing stats registration never double counts or permanently drops an observation. PushJobStatusStats emits them per sink, with a new VENICE_PUSH_JOB_DATA_WRITER_SINK dimension. The values are a sum of per-task wall-clock durations, not a push level wall-clock duration, so with N parallel tasks the sum can be up to N times the push duration. Tehuti therefore only gets Avg and Max: a percentile over a summed task duration has no bounded range to configure and would report infinity. The distribution is available through the OTel histogram instead. External-write-failure alerting: a push that exhausts its external-storage write retries fails open, VPJ downgrades the affected regions' version storage mode back to INTERNAL and the push still succeeds. Nothing in the resulting telemetry said a region lost its external-storage copy, so the condition was invisible until someone read that version. Reuses the existing update_store_version_storage_mode API rather than adding an endpoint or encoding region names into PushJobDetails. The API already carries a regions filter, and the parent resolves that filter into one call per child controller, so the controller that applies the downgrade is by construction the affected region. - Adds VersionStorageModeUpdateReason (UNSPECIFIED, EXTERNAL_WRITE_FAILURE) as an optional, typed request parameter. Absent, blank and unrecognized values resolve to UNSPECIFIED (logging a warning for an unrecognized, non-blank value), so existing callers and older or newer clients are unaffected. - VenicePushJob sends EXTERNAL_WRITE_FAILURE alongside the regions filter; VeniceParentHelixAdmin forwards the reason to each targeted child. - VeniceHelixAdmin emits push_job.external_storage_write_failure.count with cluster, store and region dimensions, only for EXTERNAL_WRITE_FAILURE and only when this call is the one that actually moves the version to INTERNAL. A retried, already-applied downgrade changes nothing and so is not counted twice; a manual storage-mode change is never counted. - Registers PushJobStatusStats for every cluster instead of only clusters with error-leader-replica fail over enabled, so the counter is reported fleet wide. Push id and version number are deliberately not dimensions (unbounded), and push type is not a dimension because the controller applying the downgrade does not know it. Testing: extends the existing linkedin#2967 fail-open case in TestVPJDualWriteExternalStorageMultiRegion, which already builds two real regions, runs a VPJ push whose external writes always fail in one of them, and is already parameterized over both data writer engines. After the existing assertions that the push succeeded and that only the failed region's version moved to INTERNAL, it now reads the child controllers' own InMemoryMetricReader and asserts the failure counter is emitted exactly once, only by the failed region's controller, with the correct region dimension, and that replaying the same downgrade through the parent does not move the counter again. Also fixes review feedback: gates the data-writer sink write-time metrics on succeeded terminal status (not merely terminal), corrects the AvroProtocolDefinition.PUSH_JOB_DETAILS javadoc to describe the actual v6 additionalPushMetrics map schema, removes leftover LI-internal storage-system name mentions from OSS javadoc, documents the asymmetric cost caveat between the external-storage and Venice write-path timings, documents the breaking schema change to AbstractDataWriterSparkJob#createPartitionWriterFactory and drops the resulting dead isNullAt checks, replaces an unreachable IllegalArgumentException on the PushJobStatusStats metrics path with a warn-and-return, and adds VersionStorageModeUpdateReasonTest coverage. Co-authored-by: Copilot <[email protected]>
68dc21f to
cf63b9d
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 42 out of 43 changed files in this pull request and generated no new comments.
Suppressed comments (2)
services/venice-controller/src/main/java/com/linkedin/venice/controller/VeniceHelixAdmin.java:1650
- The Javadoc says the dedup entry is written only after the metric is recorded, but the implementation inserts into the cache before calling recordDataWriterSinkWriteTime(...). This mismatch is confusing when reasoning about potential recording failures and retry behavior; either adjust the comment or change the code to match the documented semantics.
internal/venice-client-common/src/main/java/com/linkedin/venice/meta/VersionStorageModeUpdateReason.java:48 - parseOrDefault() logs WARN on any unrecognized non-blank value. Since this query parameter is explicitly designed for forward/backward compatibility, a newer client sending a new enum value to an older server would generate WARN spam on a hot path. Consider lowering this to DEBUG (or rate-limiting) so unknown-but-valid future values don’t look like operational issues.
for (VersionStorageModeUpdateReason reason: values()) {
if (reason.name().equalsIgnoreCase(trimmedValue)) {
return reason;
}
}
LOGGER.warn("Unrecognized VersionStorageModeUpdateReason value '{}', defaulting to {}", trimmedValue, UNSPECIFIED);
return UNSPECIFIED;
| return store; | ||
| }); | ||
|
|
||
| if (reason == VersionStorageModeUpdateReason.EXTERNAL_WRITE_FAILURE && downgradedFromExternalWrite.get()) { |
There was a problem hiding this comment.
Because this counter is the only paging signal for the silent fail-open case, should it have at-least-once semantics rather than being coupled exclusively to the successful transition? With the current ordering, the metadata update can persist and the controller can then crash before the metric is emitted. A similar gap exists during rolling deployment: an older child can apply the downgrade while ignoring the new reason, and a later retry against upgraded code sees INTERNAL and emits nothing. In both cases the alert is permanently missed. For a page-on-any-nonzero signal, retry duplicates seem safer than misses. Could we emit a failure_reported counter when the child receives EXTERNAL_WRITE_FAILURE (documenting that retries may duplicate it), and retain a separate transition-based counter only if exact applied-transition counts are needed?
There was a problem hiding this comment.
the call flow is VPJ -> Parent controller -> child controller and the protocol for communication is http. There is retry built into VPJ to retry controller API calls in case of error. So, in the case when child controller updates ZK and fails to emit a metric, the API call will fail and the VPJ will retry. If the retries are exhausted, the push job will fail which would be the behavior we want.
There was a problem hiding this comment.
Synced offline, this now makes sense to me.
Problem Statement
Two blind spots in the same push path:
into the external storage write path or the Venice write path, short of re-running the push with
debug logging.
jobDurationInMsonly gives the overall wall clock.affected regions' version storage mode back to
INTERNALand the push still succeeds. Nothingin the resulting telemetry says a region lost its external-storage copy, so the condition stays
invisible until somebody reads that version.
Solution
Activate
PushJobDetailsv6 and populate the two timing fields it added, emit controller metrics fromthem, and add a separate alertable per-region counter for the fail-open case.
Protocol activation
PushJobDetailsv5versionOverridespin added by [protocol][build] Stage PushJobDetails v6 #2971.AvroProtocolDefinition.PUSH_JOB_DETAILSfrom 5 to 6.v6 as staged by #2971 carries a single nullable
additionalPushMetricsmap (map<string, long>,defaulting to
null) rather than two fixed fields. Timings are reported under theexternalStorageWriteTimeMsandveniceWriteTimeMskeys; a missing key means that leg was notreported, and a
nullmap means the push reported no additional metrics at all. The region-levelfailure signal below is deliberately not carried in this map — region names are unbounded and the
map has no per-region shape, so it is a controller metric instead.
After this change push jobs serialize v6. A controller fleet that has not registered v6 rejects the
write, which is why #2971 has to be deployed to the controllers first.
VPJ side
DualWriteVeniceWritermeasures the external storage write path: throttling wait,batchPutcallsincluding retries and retry backoff, external flush, and external close. It separately measures the
Venice write path: the Venice/Kafka put calls plus the Venice writer flush and close.
DataWriterTaskTrackerand carried by both transports:MapReduce through new counters in
MRJobCounterHelper; Spark through new task-output columnsdeclared in
SparkConstantsand written bySparkPartitionWriter/SparkPartitionWriterFactory,which
AbstractDataWriterSparkJobsums from the completed task rows. Spark accumulators aredeliberately not used: with speculative execution a partition can be attempted twice and both
attempts' accumulator updates reach the driver, inflating the total.
VenicePushJobsums the per-task values across successful data writer task outputs and publishesthem under the two
additionalPushMetricskeys, omitting a key when that leg was not reported.Controller side (timings)
VeniceHelixAdminrecords the two durations from the reportedPushJobDetails, skipping absentkeys so an unreported push is not counted as a zero, and de-duplicating repeated reports for the
same push so a retried report does not double count.
PushJobStatusStatsemits them per sink, with the newVENICE_PUSH_JOB_DATA_WRITER_SINKdimension(
external_storage/venice).Alertable per-region external write failure counter
The fail-open is silent by design, so it needs its own explicit signal. Rather than a new endpoint, or
encoding region names into
PushJobDetails, this reuses the existingupdate_store_version_storage_modeAPI from #2967: it already carries a regions filter, the parentresolves that filter into one call per child controller, and each child knows its own region. The
controller that applies the downgrade is therefore by construction the affected region.
New optional request parameter.
VersionStorageModeUpdateReason(UNSPECIFIED,EXTERNAL_WRITE_FAILURE) is a typed enum rather than a free-form string, sent as the optionalversion_storage_mode_update_reasonquery parameter. It is telemetry intent only and never changesthe resulting storage mode. Absent, blank and unrecognized values resolve to
UNSPECIFIED, soexisting callers keep compiling and working, and a newer client talking to an older controller — or
the reverse — never fails a request over it. All pre-existing
ControllerClientandAdminoverloadsare retained and delegate with
UNSPECIFIED.VenicePushJobsendsEXTERNAL_WRITE_FAILUREalongside the existing regions filter fromdowngradeFailedExternalStorageRegionsToInternalBeforeEndOfPush, andVeniceParentHelixAdminforwards the reason to each targeted child controller.
Metric
push_job.external_storage_write_failure.countCOUNTER/NUMBER(not a duration histogram)venice.cluster.name,venice.store.name,venice.region.namepush_job_external_storage_write_failure(Count,CountSinceLastMeasurement)venice.region.namealready existed and is reused; no new dimension enum was added.Emission and dedup semantics
VeniceHelixAdminin the affected region, only when the reason isEXTERNAL_WRITE_FAILUREand this call is the one that actually transitions the version toINTERNAL.same downgrade can arrive several times; a repeat finds the version already
INTERNAL, changesnothing, and is not counted again. The counter therefore reads as "regions that lost their
external-storage copy", not "requests received".
UNSPECIFIEDand is never counted.Re-enabling
DUAL_WRITEis not a failure and is never counted.PushJobStatusStatsis now registered for every cluster rather than only clusters witherror-leader-replica fail over enabled, so the counter is reported fleet wide. This also removes a
latent NPE at the existing
pushJobStatusStatsMap.get(cluster)call site.Intended alert: page on any non-zero rate of
push_job.external_storage_write_failure.count,grouped by region and store. A firing alert means that fabric's copy of that version exists only in
Venice, so reads routed to external storage for it will miss until the store is re-pushed.
Dimensions are deliberately bounded: push id and version number are never dimensions, and push type is
not one either because the controller applying the downgrade does not know it.
Code changes
reports them, and the failure counter whenever a fail-open downgrade is applied.
fail-open downgrade is applied.
Concurrency-Specific Checks
writer task, aggregated once in the driver from completed task outputs. The failure counter is
decided inside the store write lock already held by
storeMetadataUpdate.uses a concurrent map keyed by push.
How was this PR tested?
Unit coverage for the failure counter: VPJ supplies
EXTERNAL_WRITE_FAILURE; callers on theoverloads without a reason still compile and resolve to
UNSPECIFIED; a region excluded by the filterneither updates nor counts; the matching region counts once with its own region as the dimension; an
unrelated reason does not count; an upgrade to
DUAL_WRITEdoes not count; three identical fail-openrequests count once; the parent forwards the reason to only the targeted child; two regions failing
for the same store stay two separate time series.
End-to-end coverage.
TestVPJDualWriteExternalStorageMultiRegion.dualWritePushSucceedsWhenOneRegionExhaustsExternalWriteRetries— the existing #2967 fail-open case, extended rather than duplicated — stands up two real regions and
runs a real VPJ push whose external writes always fail in one of them. On top of its existing
assertions that the push succeeds and that only the failed region's version moves to
INTERNAL, itnow reads the child controllers' own
InMemoryMetricReaderand asserts that:push_job.external_storage_write_failure.countexactlyonce, carrying that region as
venice.region.name;INTERNALand does not movethe counter — which also exercises parent-to-child reason propagation on its own, outside the push.
That test is already parameterized over both data writer engines, so Spark and MapReduce are both
covered. Results:
The count assertion was verified non-vacuous by temporarily expecting 2, which failed with
expected [2] but found [1]on both engines before being reverted.Backward compatibility: a v6 reader resolves a v5 writer's record to a
nulladditionalPushMetricsmap, so pushes from older VPJ versions keep reporting successfully during the rollout and simply do
not contribute timings. The storage-mode reason parameter is optional in both directions and needs no
deployment ordering.
Metric shape (review follow-up)
The reported timing values are a sum of per-task wall-clock durations, not a push-level wall
clock, so with N parallel data writer tasks the sum can be up to N times the push duration. Tehuti
therefore gets
AvgandMaxonly: a percentile needs a bounded, configured range, and a summedtask duration has no meaningful upper bound, so Tehuti percentiles would report infinity. The
distribution is available through the OTel histogram instead. The failure signal is a plain
counter for the same reason it is alertable: it counts events, not durations.
Does this PR introduce any user-facing or breaking changes?
PushJobDetailsv6, so [protocol][build] Stage PushJobDetails v6 #2971 must be deployed to the controllers before this change ships,otherwise the controller rejects the push job details write. No API, config or data-format
change is visible to store owners;
additionalPushMetricsdefaults tonull("not reported")and the new storage-mode reason parameter is optional.