Skip to content

[CDAP-21235] : Introduce Spark Task attempt based record count metrics, buffer and dedup while run and Emit only on task success - #16182

Open
sahusanket wants to merge 1 commit into
developfrom
CDAP-21235_record_count_fix
Open

[CDAP-21235] : Introduce Spark Task attempt based record count metrics, buffer and dedup while run and Emit only on task success#16182
sahusanket wants to merge 1 commit into
developfrom
CDAP-21235_record_count_fix

Conversation

@sahusanket

@sahusanket sahusanket commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Deduplicate Spark Retry Metrics by Introducing Executor-Side Buffering

1. Context

During CDAP Spark pipeline executions, stages report processing metrics (e.g. records.in and records.out) to the CDAP Master. To facilitate diagnostic monitoring, these are enriched with partition and attempt tags under a .raw suffix (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 SUM aggregates 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:

  1. Immediate Progress: Legacy flat progress metrics (e.g. records.out) bypass the buffer and are sent immediately to preserve real-time UI tracking.
  2. Deduplicated raw metrics: Suffix-matched counts (inputs, outputs, errors, alerts) are intercepted on worker task threads and accumulated in memory inside isolated ThreadLocal maps.
  3. Task Hooks: We register Spark TaskFailureListener and TaskCompletionListener hooks on task startup:
    • On Success: The listener flushes the buffered metrics exactly once with the successful attempt's tags.
    • On Failure/Interruption: The listener catches task exceptions/kills and discards the buffered metrics from memory. No database entries are written for failed attempts.
  4. Resource Safety:
    • Checks for TaskContext.get() == null first to prevent thread-local memory leaks on driver and helper threads.
    • Uses a simple stack-allocated loop instead of stream mappings to avoid GC allocations on hot paths.
    • Caches the Metrics reference (getTaskMetrics()) outside the task flush loop to avoid redundant tag maps and child wrapper instantiations.

3. Major Changes

cdap-spark-core-base

  • SparkRuntimeContext.java:
    • Declared ThreadLocal Map and Boolean registers to isolate memory buffers across concurrent worker threads.
    • Updated metric counting hooks to intercept and buffer target suffix-matched counts.
    • Registered Spark completion and failure listeners to flush .raw metrics 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 .raw metric 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 }]
      }
    ]
  }
}

@sahusanket sahusanket self-assigned this Jul 2, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

… buffer and dedup while run and Emit only on task success
@sahusanket
sahusanket force-pushed the CDAP-21235_record_count_fix branch from c378dee to 3135494 Compare July 2, 2026 12:00
@sahusanket sahusanket changed the title Cdap 21235 record count fix [CDAP-21235] : Introduce Spark Task attempt based record count metrics, buffer and dedup while run and Emit only on task success Jul 2, 2026
@sahusanket sahusanket added the build Triggers github actions build label Jul 2, 2026
@sonarqubecloud

sonarqubecloud Bot commented Jul 2, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

build Triggers github actions build

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant