Skip to content

minor: Add logs and metrics observability to the segment upgrade path that fires while using concurrent locking#19651

Open
capistrant wants to merge 9 commits into
apache:masterfrom
capistrant:segment-upgrade-metrics-realtime
Open

minor: Add logs and metrics observability to the segment upgrade path that fires while using concurrent locking#19651
capistrant wants to merge 9 commits into
apache:masterfrom
capistrant:segment-upgrade-metrics-realtime

Conversation

@capistrant

Copy link
Copy Markdown
Contributor

Description

Investigating potential issues with concurrent append and replace actions not being properly accepted and reflected on realtime ingestion tasks who should be upgrading their segments to reflect the concurrent append that happened on an interval they are ingesting to. This PR attempts to add some logs and metrics to the related code to help give observability into the process and smoke out if there are indeed issues going on. Biggest risk of these new mets and logs is probably chattiness, since they scale with the volume of upgrade segments that may be existing under a clusters normal operaions.

New Metrics

Metric Description Dimensions Normal value
ingest/segmentUpgrade/count Number of pending segments that a concurrent replace (for example, compaction) upgraded to a new version and asked the supervisor to have running tasks announce under the new version. Emitted by the replace task only when streaming ingestion is running concurrently with replace on the same interval. dataSource, taskId, taskType, groupId, tags 0 unless concurrent append and replace is in use.
ingest/segmentUpgrade/notified Number of upgraded pending segments the supervisor successfully routed to at least one running task. Compare with ingest/segmentUpgrade/count: count should equal notified + unmatched over the same period and dataSource. supervisorId, dataSource, stream, tags Equal to count in a healthy cluster.
ingest/segmentUpgrade/unmatched Number of upgraded pending segments the supervisor could not route to any running task. These are not announced under the new version until handoff, so the corresponding data may be briefly missing from queries. supervisorId, dataSource, stream, tags 0. A non-zero value indicates a lost upgrade announcement.
ingest/segmentUpgrade/sendFailed Number of upgrade requests that matched a running task but failed to reach it over the wire after retries. supervisorId, dataSource, stream, taskId, tags 0
ingest/segmentUpgrade/announced Number of upgraded segments a task announced under the new version. Emitted once per task, so it scales with the replica count. Do not subtract it directly from ingest/segmentUpgrade/count, which is per-segment rather than per-replica. dataSource, taskId, taskType, groupId, tags Greater than 0 while concurrent replace occurs.
ingest/segmentUpgrade/skipped Number of upgrade requests a task received but did not announce. The reason dimension is one of unknownBase (the request reached the wrong task), noSink (the base sink is no longer present), or dropping (the base sink is handing off, which is benign and covered by the durable publish path). dataSource, taskId, taskType, groupId, tags, reason 0, excluding reason=dropping.

Release note

TBD


Key changed/added classes in this PR

TBD


This PR has:

  • been self-reviewed.
  • added documentation for new or modified features or behaviors.
  • a release note entry in the PR description.
  • added Javadocs for most classes and all non-trivial methods. Linked related entities via Javadoc links.
  • added or updated version, license, or notice information in licenses.yaml
  • added comments explaining the "why" and the intent of the code wherever would not be obvious for an unfamiliar reader.
  • added unit tests or modified existing tests to cover new code paths, ensuring the threshold for code coverage is met.
  • added integration tests.
  • been tested in a test Druid cluster.

…ncurrent append and replace ingestion

mets checkpoint

doc and test

tidy up

self review

@FrankChen021 FrankChen021 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I have reviewed the code for correctness, edge cases, concurrency/lifecycle, security, data loss, API compatibility, and missing-test risks; no high-confidence issues found.

Reviewed 11 of 11 changed files.


This is an automated review by Codex GPT-5.5

@kfaraz kfaraz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for adding the metrics/logs, @capistrant ! These would be instrumental in debugging failures in concurrent compaction.

Overall looks good, but I have left some nitpicks.

public class SegmentUpgradeMetrics
{
/** Number of upgraded pending segments a REPLACE commit created and handed to the supervisor. Task-action dims. */
public static final String COUNT = "ingest/segmentUpgrade/count";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please include the prefix realtime in all the metrics to distinguish upgrade of pending segments from upgrade of appended segments.

Suggested change
public static final String COUNT = "ingest/segmentUpgrade/count";
public static final String REALTIME_PERSISTED = "ingest/realtime/segmentUpgrade/persisted";

// Values for the DruidMetrics.REASON dimension on SKIPPED.

/** The task holds no pending segment matching upgradedFromSegmentId (request targeted the wrong task). */
public static final String REASON_UNKNOWN_BASE = "unknownBase";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: The reasons need not be camel cased, e.g. unknown base sink instead of unknownBase.

public static final String COUNT = "ingest/segmentUpgrade/count";

/** A record was delivered to at least one running task. Supervisor dims. */
public static final String NOTIFIED = "ingest/segmentUpgrade/notified";

@kfaraz kfaraz Jul 9, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
public static final String NOTIFIED = "ingest/segmentUpgrade/notified";
public static final String REALTIME_TASK_NOTIFIED = "ingest/realtime/segmentUpgrade/notified";

return;
}

log.info("Registering [%d] upgraded pending segments created by task[%s] on supervisor[%s]",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Maybe move this log to the end of this method and include a summary map from pending segment ID to number of tasks notified.


supervisor.addTaskGroupToActivelyReadingTaskGroup(
supervisor.getTaskGroupIdForPartition("0"),
ImmutableMap.of("0", "5"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: Use Map.of and Set.of for brevity.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ack on Set. the method signature seems to require explicit ImmutableMap tho

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Oh, I see. Thanks for the clarification!

if (basePendingSegment == null || droppingSinks.contains(basePendingSegment) || !sinks.containsKey(basePendingSegment)) {
if (basePendingSegment == null) {
// This task never allocated a segment matching upgradedFromSegmentId, i.e. the request targeted the wrong task.
log.info(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should be a warn, I think.

Comment on lines +1203 to +1220
log.info(
"Not announcing upgraded pending segment[%s] because this task has no base sink matching"
+ " upgradedFromSegmentId[%s]; the upgrade request likely targeted the wrong task[%s].",
upgradedPendingSegment, pendingSegmentRecord.getUpgradedFromSegmentId(), myId
);
return UpgradeAnnouncementOutcome.SKIPPED_UNKNOWN_BASE;
} else if (droppingSinks.contains(basePendingSegment)) {
// Expected during handoff: the base sink is being dropped
log.debug(
"Not announcing upgraded pending segment[%s] for base segment[%s] on task[%s] because the base sink is being dropped.",
upgradedPendingSegment, basePendingSegment, myId
);
return UpgradeAnnouncementOutcome.SKIPPED_DROPPING;
} else {
// Unexpected: the base sink is gone even though this task once held it.
log.info(
"Not announcing upgraded pending segment[%s] for base segment[%s] on task[%s] because the base sink is no longer present.",
upgradedPendingSegment, basePendingSegment, myId

@kfaraz kfaraz Jul 9, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I feel like these logs can be merged into one and can be logged by the caller itself using the result of this method.

capistrant and others added 2 commits July 9, 2026 20:32
applying the pure text changes. will commit responses to review in another commit

Co-authored-by: Kashif Faraz <kashif.faraz@gmail.com>
Comment thread docs/operations/metrics.md Outdated
|`ingest/notices/time`|Milliseconds taken to process a notice by the supervisor.|`supervisorId`, `dataSource`, `tags`| < 1s |
|`ingest/pause/time`|Milliseconds spent by a task in a paused state without ingesting.|`dataSource`, `taskId`, `tags`| < 10 seconds|
|`ingest/handoff/time`|Total number of milliseconds taken to handoff a set of segments.|`dataSource`, `taskId`, `taskType`, `groupId`, `tags`|Depends on the coordinator cycle time.|
|`ingest/realtime/segmentUpgrade/count`|Number of pending segments that a concurrent replace (for example, compaction) upgraded to a new version and asked the supervisor to have running tasks announce under the new version. Emitted by the replace task only when streaming ingestion is running concurrently with replace on the same interval.|`dataSource`, `taskId`, `taskType`, `groupId`, `interval`, `version`, `tags`|0 unless [concurrent append and replace](../ingestion/concurrent-append-replace.md) is in use.|

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We should probably rename this metric from count to persisted to signify that this is the count that was actually persisted to the metadata store.
Also, rephrased the text a bit.

Suggested change
|`ingest/realtime/segmentUpgrade/count`|Number of pending segments that a concurrent replace (for example, compaction) upgraded to a new version and asked the supervisor to have running tasks announce under the new version. Emitted by the replace task only when streaming ingestion is running concurrently with replace on the same interval.|`dataSource`, `taskId`, `taskType`, `groupId`, `interval`, `version`, `tags`|0 unless [concurrent append and replace](../ingestion/concurrent-append-replace.md) is in use.|
|`ingest/realtime/segmentUpgrade/persisted`|Number of pending segments that were upgraded in the metadata store while publishing segments to an interval. Emitted by the Overlord only when a REPLACE task (e.g., compaction task with `useConcurrentLocks` set to `true`) commits segments to an interval already containing pending segments allocated to an APPEND task (e.g. streaming task with `useConcurrentLocks` set to `true`). |`dataSource`, `taskId`, `taskType`, `groupId`, `interval`, `version`, `tags`|0 unless [concurrent append and replace](../ingestion/concurrent-append-replace.md) is in use.|

* segment-upgrade metric can be sliced by the specific segment being re-announced. Mirrors
* {@code IndexTaskUtils.setSegmentDimensions}, which serves the same purpose for {@code DataSegment}s.
*/
public static ServiceMetricEvent.Builder setSegmentDimensions(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: Maybe move this method to IndexTaskUtils itself, as it feels like a more generic place (rather than upgrade specific) allowing for reuse for this method.

Comment on lines +191 to +196
log.info("No active streaming supervisor for datasource[%s]; the [%d] upgraded pending segment(s) from task[%s]"
+ " will become queryable when their tasks hand off.",
task.getDataSource(),
upgradedPendingSegments.size(),
task.getId()
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

minor rephrase

Suggested change
log.info("No active streaming supervisor for datasource[%s]; the [%d] upgraded pending segment(s) from task[%s]"
+ " will become queryable when their tasks hand off.",
task.getDataSource(),
upgradedPendingSegments.size(),
task.getId()
);
log.info(
"Could not find any active concurrent streaming supervisor for datasource[%s]."
+ " Ignoring registry of [%d] pending segment(s) upgraded by task[%s]. These will become queryable only after handoff."
task.getDataSource(),
upgradedPendingSegments.size(),
task.getId()
);

/**
* Notifies every running task in the matching task group(s) of an upgraded pending segment.
*
* @return the number of tasks notified, which the caller aggregates into a per-batch summary

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
* @return the number of tasks notified, which the caller aggregates into a per-batch summary
* @return the number of tasks notified

}

// Only the exceptional no-match case is logged per segment; the notified count is returned to the task action,
// which summarizes the whole batch in one log line, and each notification is captured by the per-task metric.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We need not call this out. This method should be agnostic of the behaviour of its caller.
We choose not to log the successful case here to reduce verbosity, irrespective of the behaviour of the caller.

Comment on lines +1457 to +1459
"Could not find any task matching taskAllocatorId[%s] in supervisor[%s] for upgraded pending segment[%s]"
+ " (upgradedFrom[%s]); it will not be re-announced until handoff. Currently tracking [%d] activelyReading"
+ " and [%d] pendingCompletion task group(s).",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do you think logging the number of actively reading / pending completion groups would be useful for debugging? I feel we may omit these for now.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

hmm I guess probably not useful. was kinda just packing in bits of information wherever I could find them in this PR. but you're right I'm not sure what I do with this number in this log so I will remove

{
ANNOUNCED(null),
/** The task holds no pending segment matching upgradedFromSegmentId (request targeted the wrong task). */
SKIPPED_UNKNOWN_BASE("unknown base"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
SKIPPED_UNKNOWN_BASE("unknown base"),
SKIPPED_UNKNOWN_BASE("unknown base sink"),

* Logs and emits a metric for the outcome of a pending-segment upgrade request, keyed off the {@code outcome}
* returned by {@link StreamAppenderator#registerUpgradedPendingSegment}.
*/
private void recordUpgradeOutcome(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
private void recordUpgradeOutcome(
private void recordUpgradeResult(

*/
private void recordUpgradeOutcome(
PendingSegmentRecord upgradedPendingSegment,
StreamAppenderator.PendingSegmentUpgradeResult outcome

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
StreamAppenderator.PendingSegmentUpgradeResult outcome
StreamAppenderator.PendingSegmentUpgradeResult result

@FrankChen021 FrankChen021 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Severity Findings
P0 0
P1 1
P2 1
P3 0
Total 2
Severity Findings
P0 0
P1 1
P2 1
P3 0
Total 2

Reviewed 16 of 16 changed files.


This is an automated review by Codex GPT-5.6-Sol

"ingest/notices/time" : { "dimensions" : ["dataSource"], "type" : "timer", "conversionFactor": 1000.0, "help": "Seconds taken to process a notice by the supervisor." },
"ingest/pause/time" : { "dimensions" : ["dataSource"], "type" : "timer", "conversionFactor": 1000.0, "help": "Seconds spent by a task in a paused state without ingesting." },
"ingest/handoff/time" : { "dimensions" : ["dataSource"], "type" : "timer", "conversionFactor": 1000.0, "help": "Total number of seconds taken to handoff a set of segments." },
"ingest/realtime/segmentUpgrade/count" : { "dimensions" : ["dataSource", "interval", "version"], "type" : "count", "help": "Number of pending segments upgraded by a concurrent replace." },

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P1] Avoid unbounded per-version Prometheus series

The default exporter has no TTL unless flushPeriod is explicitly configured, and its collector retains every label tuple. version, interval, and taskId continually change across replacements and tasks, so recurring concurrent compaction makes these six default metrics accumulate series indefinitely on long-lived Overlord/supervisor processes, potentially exhausting heap. Keep high-cardinality identifiers out of the default mapping or enforce bounded expiry.

notifiedTasksBySegment.size(),
task.getId(),
activeSupervisorIdWithAppendLock.get(),
notifiedTasksBySegment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P2] Bound the per-segment INFO summary

The upgraded-segment list is not capped, yet this builds and formats every segment ID into one INFO message. Broad compaction can produce thousands of entries and megabyte-scale log lines on the task-action path. Log aggregate counts with a bounded sample, reserving full detail for debug output.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

fair point

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants