[CDAP-21235] : Introduce Spark Task attempt based record count metrics, buffer and dedup while run and Emit only on task success - #16182
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces buffering for pipeline stage metrics in Spark tasks, flushing raw metrics upon successful task completion and adding Spark partition and attempt tags to metric aggregations. The reviewer identified a potential memory leak in SparkRuntimeContext when TaskContext.get() is null (as the thread-local map is populated but never cleared), as well as performance overheads from stream allocations in bufferMetric and redundant getTaskMetrics() calls inside the loop in flushBufferedMetrics.
c378dee to
3135494
Compare
|
| Constants.Metrics.RECORDS_ALERT_SUFFIX | ||
| ); | ||
|
|
||
| private final ThreadLocal<Map<String, Long>> bufferedCounts = ThreadLocal.withInitial(HashMap::new); |
There was a problem hiding this comment.
Why is ThreadLocal needed here?
What is the scope of SparkRuntimeContext object?
There was a problem hiding this comment.
-
On a Spark Executor, SparkRuntimeContext is a process-level singleton shared across the entire JVM.
-
A executor can have 4 threads and each of them will handle 1 partition each . And we are buffering records per stage (example :
user.<StageName>.records.in= 5 ) , so there will be collisions accross threads if we don't handle at Thread ( task ) level.
| TaskContext tc = TaskContext.get(); | ||
| if (tc != null) { | ||
| Map<String, String> taskTags = new HashMap<>(); | ||
| taskTags.put(Constants.Metrics.Tag.SPARK_PARTITION, String.valueOf(tc.partitionId())); | ||
| taskTags.put(Constants.Metrics.Tag.SPARK_ATTEMPT, String.valueOf(tc.attemptNumber())); | ||
| return getMetrics().child(taskTags); | ||
| } | ||
| return getMetrics(); |
There was a problem hiding this comment.
Return early if tc == null
TaskContext tc = TaskContext.get();
if (tc == null) {
return getMetrics();
}
...
...| } | ||
|
|
||
| private void registerCompletionListenerIfNeeded(TaskContext tc) { | ||
| if (!listenerRegistered.get()) { |
There was a problem hiding this comment.
return early to avoid long nested blocks.
if (listenerRegistered.get()) {
return;
}
...
...
vsethi09
left a comment
There was a problem hiding this comment.
Please add unit tests wherever applicable.
3135494 to
bac9fd9
Compare
… buffer and dedup while run and Emit only on task success
bac9fd9 to
0e33278
Compare
|



Deduplicate Spark Retry Metrics by Introducing Executor-Side Buffering
1. Context
During CDAP Spark pipeline executions, stages report processing metrics (e.g.
records.inandrecords.out) to the CDAP Master. To facilitate diagnostic monitoring, these are enriched with partition and attempt tags under a.rawsuffix (e.g.user.stage_name.records.out.raw).Currently, when Spark tasks fail and retry, both the failed and successful attempts write their counts immediately to the metrics database. Consequently, database queries using standard
SUMaggregates double-count records processed during failed attempts, causing audit mismatches.2. Approach
We introduce executor-side metrics buffering to separate real-time execution feedback from the final database records:
records.out) bypass the buffer and are sent immediately to preserve real-time UI tracking.ThreadLocalmaps.TaskFailureListenerandTaskCompletionListenerhooks on task startup:TaskContext.get() == nullfirst to prevent thread-local memory leaks on driver and helper threads.Metricsreference (getTaskMetrics()) outside the task flush loop to avoid redundant tag maps and child wrapper instantiations.3. Major Changes
cdap-spark-core-baseThreadLocalMap and Boolean registers to isolate memory buffers across concurrent worker threads..rawmetrics conditionally and clean up ThreadLocal allocations at thread reuse.4. Verification Example
For a partition processing 1,000 records that fails once (processing 5 records) and succeeds on the retry:
Metrics Query payload (
POST /v3/metrics/query){ "qid": { "tags": { "namespace": "default", "app": "fail_retry_records", "run": "d9faaa02-75e5-11f1-b343-d65e96f69bd1" }, "metrics": [ "user.JavaScript.records.out", "user.JavaScript.records.out.raw" ], "aggregate": true, "timeRange": {"startTime": 0, "endTime": "now"} } }Metrics Query Response
The legacy metric double-counts the failure, while the new
.rawmetric is successfully deduplicated:{ "qid": { "series": [ { "metricName": "user.JavaScript.records.out", "data": [{ "time": 0, "value": 1005 }] }, { "metricName": "user.JavaScript.records.out.raw", "data": [{ "time": 0, "value": 1000 }] } ] } }Diagnostic Breakdown payload (by partition & attempt)
{ "qid": { "tags": { "namespace": "default", "app": "fail_retry_records", "run": "d9faaa02-75e5-11f1-b343-d65e96f69bd1" }, "metrics": [ "user.JavaScript.records.out.raw" ], "groupBy": [ "spark_part", "spark_att" ], "timeRange": {"startTime": 0, "endTime": "now"} } }Diagnostic Response
Only successful attempt metrics exist in the series list:
{ "qid": { "series": [ { "metricName": "user.JavaScript.records.out.raw", "grouping": { "spark_part": "0", "spark_att": "1" }, "data": [{ "time": 0, "value": 1000 }] } ] } }