Skip to content

[core][DO NOT MERGE] End-to-end testing changes for removing task events out of GCS - 3 - #65253

Open
karticam wants to merge 78 commits into
ray-project:masterfrom
karticam:karticam/test-task-events-end-to-end-10-n-with-opt
Open

[core][DO NOT MERGE] End-to-end testing changes for removing task events out of GCS - 3#65253
karticam wants to merge 78 commits into
ray-project:masterfrom
karticam:karticam/test-task-events-end-to-end-10-n-with-opt

Conversation

@karticam

@karticam karticam commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

#65248 + ray task event recorder optimizations.

test pr. do not merge

karticam added 30 commits July 28, 2026 11:33
- New TaskEventsHead SubprocessModule exposing POST /api/task_events
- Deserialization isolated in _deserialize_request (wire format TBD)
- In-memory buffer stub; storage/GC is a later task

Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
task/actor task definition events

Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
no thread spawning when task event recorder flags are disabled.

Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
is responsible for making test heavy

Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
karticam added 17 commits August 5, 2026 17:03
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
…ashboard head

Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
…g constant

Signed-off-by: Kartica Modi <karticamodi@gmail.com>
…m the migration

Signed-off-by: Kartica Modi <karticamodi@gmail.com>
…s enabled

Signed-off-by: Kartica Modi <karticamodi@gmail.com>
…s them up

Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
… stack

Signed-off-by: Kartica Modi <karticamodi@gmail.com>
- enable_ray_event: true
- enable_ray_task_event_recorder: true
- enable_task_events_to_dashboard_head: true
- enable_core_worker_task_event_to_gcs: false

Signed-off-by: Kartica Modi <karticamodi@gmail.com>
With the migration flags on, GcsTaskManager no longer receives task events,
so its metrics/usage counters stay empty and the aggregator has no GCS
fallback. Skip the tests that assert on those, to be re-enabled/addressed
when GcsTaskManager is removed:
- test_failure_4::test_task_failure_when_driver_local_raylet_dies
- test_task_events::test_status_task_events_metrics
- test_usage_stats: test_get_extra_usage_tags_to_report, test_actor_stats,
  test_task_stats, test_usage_report_e2e, test_usage_stats_tags

Signed-off-by: Kartica Modi <karticamodi@gmail.com>
- ExportEvents extracts under mutex_, then groups/serializes/sends unlocked,
  so it no longer blocks AddEvents on the task's call path.
- Claim the in-flight gRPC slot under mutex_ (grpc_in_progress_.exchange);
  MarkGrpcDone releases it when there is nothing to send.
- Bound each export to task_events_send_batch_size /
  task_events_dropped_task_attempt_batch_size, as TaskEventBufferImpl does.

Signed-off-by: Kartica Modi <karticamodi@gmail.com>
… path

- Definition events hold the task spec; lifecycle events hold their status
  changes. Both build their proto in SerializeData() at export time.
- Move the two populate helpers into observability/task_event_populators so the
  event wrappers and the buffer's ToRpcRayEvents share one implementation.
- Move TaskStateUpdate to observability/; TaskStatusEvent keeps an alias so its
  construction sites are unchanged.
- Lifecycle MergeData appends status changes instead of proto MergeFrom, so a
  later change cannot clear fields an earlier one set (review comment T4).

Signed-off-by: Kartica Modi <karticamodi@gmail.com>
- TaskAttemptId is (TaskID, attempt) instead of (task_id binary, attempt), so
  building one no longer allocates per event.
- The status ring stores the attempt next to the event, so the export path does
  not downcast every event a second time.
- Add RayEventRecorderInterface::AddEvent and replace ToRayEventInterfaces with
  RecordTo, dropping the vector allocated per record call.
- Share the session name as a shared_ptr instead of copying it into every event;
  GetSessionName no longer returns by value.

Signed-off-by: Kartica Modi <karticamodi@gmail.com>
…ination

- recording_enabled_ requires one of the GCS / aggregator / export destinations
  to be live. With the recorder handling the aggregator send and GCS and export
  off, buffered events were dropped at flush, so the ring was filled on the
  task's call path for nothing.
- enabled_ keeps owning the io thread and GCS client lifecycle so Stop() still
  tears them down; Enabled() reports both.
- Gate on the destinations rather than on the recorder, so recorder plus GCS
  export keeps recording.
- task_event_buffer_test's base fixture names a destination so its tests still
  exercise the ring.

Signed-off-by: Kartica Modi <karticamodi@gmail.com>
@karticam karticam added the core Issues that should be addressed in Ray Core label Aug 6, 2026
@karticam
karticam requested a review from edoakes as a code owner August 6, 2026 10:31
@karticam karticam added the go add ONLY when ready to merge, run all tests label Aug 6, 2026
@karticam
karticam requested review from a team and MengjinYan as code owners August 6, 2026 10:31

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request migrates task events out of GCS to the dashboard head by introducing a new TaskEventsHead module, an in-memory TaskEventStorage, and a background TaskEventManager. It also updates the state API and timeline queries to read from the dashboard head when enabled. The review feedback highlights several critical issues, including a missing aiohttp import that would cause a runtime error, potential infinite CPU-spinning loops in background subscription handlers on persistent exceptions, a potential KeyError when accessing task log info, overly restrictive filtering of events lacking task metadata, a potential integer overflow when narrowing 64-bit log offsets to 32-bit integers, and leftover AI-generation tags in comments.

Comment on lines +1 to 4
import asyncio
import json
import logging
from abc import ABC, abstractmethod

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.

high

The aiohttp module is not imported in this file, which will cause a NameError when attempting to catch aiohttp.ClientConnectionError on line 398. Please add import aiohttp to the imports at the top of the file.

Suggested change
import asyncio
import json
import logging
from abc import ABC, abstractmethod
import asyncio
import aiohttp
import json
import logging
from abc import ABC, abstractmethod

Comment on lines +118 to +125
while True:
try:
for _, worker_delta in await subscriber.poll(
batch_size=_SUBSCRIBER_POLL_BATCH_SIZE
):
self._handle_worker_delta(worker_delta)
except Exception:
logger.exception("Failed handling worker-death notifications.")

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.

high

The while True loop in _subscribe_for_worker_deaths does not have any sleep or backoff in the except block. If subscriber.poll persistently raises an exception (e.g., due to a lost connection to GCS), this loop will spin infinitely at 100% CPU and flood the logs. Adding a short sleep (e.g., await asyncio.sleep(1)) in the except block prevents this.

Suggested change
while True:
try:
for _, worker_delta in await subscriber.poll(
batch_size=_SUBSCRIBER_POLL_BATCH_SIZE
):
self._handle_worker_delta(worker_delta)
except Exception:
logger.exception("Failed handling worker-death notifications.")
while True:
try:
for _, worker_delta in await subscriber.poll(
batch_size=_SUBSCRIBER_POLL_BATCH_SIZE
):
self._handle_worker_delta(worker_delta)
except Exception:
logger.exception("Failed handling worker-death notifications.")
await asyncio.sleep(1)

Comment on lines +130 to +137
while True:
try:
for _, job_data in await subscriber.poll(
batch_size=_SUBSCRIBER_POLL_BATCH_SIZE
):
self._handle_job_update(job_data)
except Exception:
logger.exception("Failed handling job-finished notifications.")

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.

high

The while True loop in _subscribe_for_finished_jobs does not have any sleep or backoff in the except block. If subscriber.poll persistently raises an exception, this loop will spin infinitely at 100% CPU and flood the logs. Adding a short sleep (e.g., await asyncio.sleep(1)) in the except block prevents this.

Suggested change
while True:
try:
for _, job_data in await subscriber.poll(
batch_size=_SUBSCRIBER_POLL_BATCH_SIZE
):
self._handle_job_update(job_data)
except Exception:
logger.exception("Failed handling job-finished notifications.")
while True:
try:
for _, job_data in await subscriber.poll(
batch_size=_SUBSCRIBER_POLL_BATCH_SIZE
):
self._handle_job_update(job_data)
except Exception:
logger.exception("Failed handling job-finished notifications.")
await asyncio.sleep(1)

for key in keys:
task_state[key] = src.get(key)

task_log_info = task_state["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.

high

Accessing task_state["task_log_info"] directly can raise a KeyError if "task_log_info" is not present in the dictionary (which happens if the field is unset in the protobuf message and MessageToDict omits it). Using .get("task_log_info") is much safer.

Suggested change
task_log_info = task_state["task_log_info"]
task_log_info = task_state.get("task_log_info")

Comment on lines +47 to +48
if not task_event.HasField("task_info"):
return False

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.

medium

If task_event does not have task_info (which can happen under partial data loss or if the definition event hasn't arrived/was dropped), _passes_filters returns False immediately. This means these events are completely filtered out and invisible even for unfiltered queries or queries that only filter by task_id. We should allow them to pass if the filters don't require task_info.

Suggested change
if not task_event.HasField("task_info"):
return False
if not task_event.HasField("task_info"):
# If task_info is missing (e.g. due to partial data loss), we can still match task_filters.
# However, we must reject if there are filters that require task_info.
if (
filters.exclude_driver
or len(filters.job_filters) > 0
or len(filters.actor_filters) > 0
or len(filters.task_name_filters) > 0
):
return False
for task_filter in filters.task_filters:
if not _apply_predicate(
task_filter.predicate, task_event.task_id, task_filter.task_id
):
return False
return True

Comment on lines +383 to +388
// The export schema is public and its offsets are int32, so the wider offsets are
// narrowed here rather than changing a field type that consumers already parse.
dest->set_stdout_start(static_cast<int32_t>(src.stdout_start()));
dest->set_stdout_end(static_cast<int32_t>(src.stdout_end()));
dest->set_stderr_start(static_cast<int32_t>(src.stderr_start()));
dest->set_stderr_end(static_cast<int32_t>(src.stderr_end()));

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.

medium

Casting src.stdout_start() (which is int64_t) directly to int32_t via static_cast can cause integer overflow and wrap-around to negative values if the log file exceeds 2 GB. Since the export schema requires int32, we should cap the values at INT32_MAX (2147483647LL) to prevent negative offsets.

  // The export schema is public and its offsets are int32, so the wider offsets are
  // narrowed here rather than changing a field type that consumers already parse.
  // We cap the offsets at INT32_MAX to prevent wrap-around to negative values.
  dest->set_stdout_start(static_cast<int32_t>(std::min(src.stdout_start(), 2147483647LL)));
  dest->set_stdout_end(static_cast<int32_t>(std::min(src.stdout_end(), 2147483647LL)));
  dest->set_stderr_start(static_cast<int32_t>(std::min(src.stderr_start(), 2147483647LL)));
  dest->set_stderr_end(static_cast<int32_t>(std::min(src.stderr_end(), 2147483647LL)));

}

const CoreWorkerOptions options_;
// (claude) options_.session_name shared with every task event this worker records,

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.

medium

There are several comments across the newly added/modified files in this PR that contain the (claude) prefix (likely left over from AI-assisted code generation). Please clean these up by removing the (claude) tag to keep the codebase professional and tidy.

Suggested change
// (claude) options_.session_name shared with every task event this worker records,
// options_.session_name shared with every task event this worker records,

- task_event_buffer_test: session_name is now shared_ptr<const string>
  (fix 3); pass std::make_shared instead of a string literal.
- ray_task_definition/lifecycle_event_test: GetTaskAttempt().first is now a
  TaskID (fix 3); compare against task_id, not task_id.Binary().
- Drop the {false, false} destination params: fix 4 disables the buffer when
  no destination is live, so the ring-store assertions no longer apply.

Signed-off-by: Kartica Modi <karticamodi@gmail.com>
… 10-n base

- task_event_buffer_test: TaskEventBufferImpl session_name is std::string, not
  a shared_ptr (an over-broad earlier replace); pass the plain string.
- task_event_buffer_export_event_test / actor_task_execution_queue_test:
  TaskStatusEvent session_name is now shared_ptr<const string>; use make_shared
  / the session_name_ member.

Signed-off-by: Kartica Modi <karticamodi@gmail.com>
test_globally_disable_task_events sets task_events_report_interval_ms=0 to turn
all task events off. The recorder ignored it (only checked the enable flags), so
with the flags on it kept recording; RayTaskEventRecorder::Enabled() now also
requires the interval to be > 0, matching the buffer.

Signed-off-by: Kartica Modi <karticamodi@gmail.com>
@karticam
karticam force-pushed the karticam/test-task-events-end-to-end-10-n-with-opt branch from 4104239 to 57a8b8c Compare August 6, 2026 14:58

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit 57a8b8c. Configure here.

self.gcs_options = None
self._global_state_accessor = None
self._init_lock = Lock()
self._task_events_head_client = None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale task-events client after disconnect

Medium Severity

GlobalState.disconnect clears _global_state_accessor but leaves the new cached _task_events_head_client in place. After ray.shutdown() / reconnect, ray.timeline() can keep using the old accessor and dashboard endpoint from the previous session.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 57a8b8c. Configure here.

import ray
from ray._common.constants import HEAD_NODE_RESOURCE_NAME, NODE_ID_PREFIX
from ray._common.utils import binary_to_hex, decode, hex_to_binary
from ray._private import ray_constants

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unclear PR title or description

Low Severity

⚠️ This PR needs a clearer title and/or description.

To help reviewers, please ensure your PR includes:

  • Title: A concise summary of the change
  • Description:
    • What problem does this solve?
    • How does this PR solve it?
    • Any relevant context for reviewers such as:
      • Why is the problem important to solve?
      • Why was this approach chosen over others?

See this list of PRs as examples for PRs that have gone above and beyond:

This violates the Clear PR Descriptions and Titles rule: the description is effectively “test pr. do not merge” plus a link, and does not explain the problem or how this change fixes it.

Fix in Cursor Fix in Web

Triggered by project rule: Bugbot Rules

Reviewed by Cursor Bugbot for commit 57a8b8c. Configure here.

Signed-off-by: Kartica Modi <karticamodi@gmail.com>
…ion_name

The cherry-picked merge test built RayTaskLifecycleEvent with a string-literal
session_name, but on this base fix3 made it shared_ptr<const string>; pass
make_shared so the test compiles.

Signed-off-by: Kartica Modi <karticamodi@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core Issues that should be addressed in Ray Core go add ONLY when ready to merge, run all tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant