Skip to content

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

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

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

Conversation

@karticam

@karticam karticam commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

test PR

contains all changes from PR 2/n to 10/n. All such PRs are prefixed by [taskEvents out of GCS].
Besides that, the other commits are:

  1. 360bb91 => toggles the flag used to trigger the entire feature
    essentially these flags were configured:
    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
  2. 899bd35 => disables some tests which are known followups for the feature.
  • Unlike GcsTaskManager, we have not yet added support for metrics on task events head for events stored, events recorder etc.
  • There is one edge case in test_task_failure_4.py which is yet to be covered.

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 11 commits August 5, 2026 17:02
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>
…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>
@karticam
karticam requested review from a team, MengjinYan and edoakes as code owners August 6, 2026 05:40
@karticam karticam added core Issues that should be addressed in Ray Core go add ONLY when ready to merge, run all tests labels Aug 6, 2026

@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 implements the migration of task events out of GCS to the dashboard head. It introduces a new TaskEventsHead subprocess module, an in-memory TaskEventStorage on the dashboard head, and a background TaskEventManager to reconcile task states against GCS worker and job lifecycle events. Additionally, it integrates a new RayTaskEventRecorder in the core worker to export task events to the aggregator and updates the state API to query task events from the dashboard head. The review feedback highlights critical robustness and correctness issues, including a runtime TypeError when slice-deleting protobuf repeated fields, potential background task crashes due to unhandled gRPC and subscription exceptions, silent omission of task events lacking static metadata in query filters, and a redundant variable assignment.

Comment on lines +47 to +61
if not task_event.HasField("task_info"):
return False
task_info = task_event.task_info
if filters.exclude_driver and task_info.type == TaskType.DRIVER_TASK:
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
for job_filter in filters.job_filters:
if not _apply_predicate(
job_filter.predicate, task_info.job_id, job_filter.job_id
):
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.

high

The current implementation of _passes_filters immediately discards any task event that does not have task_info populated. In a distributed system, lifecycle events (like RUNNING or FINISHED) can arrive out of order or definition events can be dropped due to memory limits. Discarding these events means tasks with only state updates will be silently hidden from the user entirely.

We can make this check robust by removing the strict task_info check and instead matching against the top-level job_id (which is always present on TaskEvents) and using default values for missing task_info fields.

    task_info = task_event.task_info
    if filters.exclude_driver and task_info.type == TaskType.DRIVER_TASK:
        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
    for job_filter in filters.job_filters:
        if not _apply_predicate(
            job_filter.predicate, task_event.job_id, job_filter.job_id
        ):
            return False

num_profile = len(existing.profile_events.events)
if num_profile > MAX_NUM_PROFILE_EVENTS_PER_TASK:
to_drop = num_profile - MAX_NUM_PROFILE_EVENTS_PER_TASK
del existing.profile_events.events[:to_drop]

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

In Python, protobuf repeated composite fields (represented as RepeatedCompositeContainer) do not support slice deletion (e.g., del container[:to_drop]). Attempting to do so will raise a TypeError at runtime, crashing the dashboard head process when a task accumulates more than MAX_NUM_PROFILE_EVENTS_PER_TASK profile events.

Instead, we should copy the kept events, clear the field, and extend it.

Suggested change
del existing.profile_events.events[:to_drop]
kept_events = list(existing.profile_events.events)[to_drop:]
existing.profile_events.ClearField("events")
existing.profile_events.events.extend(kept_events)

Comment on lines +115 to +137
async def _subscribe_for_worker_deaths(self) -> None:
subscriber = GcsAioWorkerDeltaSubscriber(address=self._gcs_address)
await subscriber.subscribe()
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.")

async def _subscribe_for_finished_jobs(self) -> None:
subscriber = GcsAioJobSubscriber(address=self._gcs_address)
await subscriber.subscribe()
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.

medium

If GCS is down or slow to start up, subscriber.subscribe() will raise an exception, causing the background subscription tasks to die permanently at startup. Additionally, if subscriber.poll() fails due to a transient network error, the loops will enter a tight busy-loop, spamming logs and consuming 100% CPU.

We should wrap the subscription step in a retry loop and add a sleep delay in the exception handlers to prevent CPU exhaustion.

    async def _subscribe_for_worker_deaths(self) -> None:
        subscriber = GcsAioWorkerDeltaSubscriber(address=self._gcs_address)
        while True:
            try:
                await subscriber.subscribe()
                break
            except Exception as e:
                logger.warning(f"Failed to subscribe to worker deaths, retrying in 1s: {e}")
                await asyncio.sleep(1)
        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)

    async def _subscribe_for_finished_jobs(self) -> None:
        subscriber = GcsAioJobSubscriber(address=self._gcs_address)
        while True:
            try:
                await subscriber.subscribe()
                break
            except Exception as e:
                logger.warning(f"Failed to subscribe to finished jobs, retrying in 1s: {e}")
                await asyncio.sleep(1)
        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)

Comment on lines +101 to +113
async def _get_worker_info(
self, worker_id: bytes
) -> Optional[gcs_pb2.WorkerTableData]:
if self._worker_info_stub is None:
self._worker_info_stub = gcs_service_pb2_grpc.WorkerInfoGcsServiceStub(
self._gcs_aio_channel
)
reply = await self._worker_info_stub.GetWorkerInfo(
gcs_service_pb2.GetWorkerInfoRequest(worker_id=worker_id)
)
if not reply.HasField("worker_table_data"):
return None
return reply.worker_table_data

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 GCS is temporarily unavailable or restarts, GetWorkerInfo can raise a gRPC error. This will propagate out of asyncio.gather and crash the background task. We should wrap the gRPC call in a try-except block to handle errors gracefully.

Suggested change
async def _get_worker_info(
self, worker_id: bytes
) -> Optional[gcs_pb2.WorkerTableData]:
if self._worker_info_stub is None:
self._worker_info_stub = gcs_service_pb2_grpc.WorkerInfoGcsServiceStub(
self._gcs_aio_channel
)
reply = await self._worker_info_stub.GetWorkerInfo(
gcs_service_pb2.GetWorkerInfoRequest(worker_id=worker_id)
)
if not reply.HasField("worker_table_data"):
return None
return reply.worker_table_data
async def _get_worker_info(
self, worker_id: bytes
) -> Optional[gcs_pb2.WorkerTableData]:
if self._worker_info_stub is None:
self._worker_info_stub = gcs_service_pb2_grpc.WorkerInfoGcsServiceStub(
self._gcs_aio_channel
)
try:
reply = await self._worker_info_stub.GetWorkerInfo(
gcs_service_pb2.GetWorkerInfoRequest(worker_id=worker_id)
)
except Exception as e:
logger.error(f"Failed to get worker info for {worker_id.hex()}: {e}")
return None
if not reply.HasField("worker_table_data"):
return None
return reply.worker_table_data

Comment on lines +125 to +127
self._num_task_attempts_dropped_tracked = len(self._dropped_task_attempts)
if num_to_evict == 0:
return

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

This assignment is redundant because self._num_task_attempts_dropped_tracked is reassigned again on line 131 after the set is modified. We can safely remove this line.

Suggested change
self._num_task_attempts_dropped_tracked = len(self._dropped_task_attempts)
if num_to_evict == 0:
return
if num_to_evict == 0:
return

@pytest.mark.skip(
reason="task-events-out-of-GCS migration: the terminal failure event is lost "
"when the node-local aggregator dies with its raylet; no GCS fallback yet."
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Lost failures on raylet death

High Severity

With the new defaults, task events no longer go to GCS and instead flow through the node-local aggregator. When that raylet/aggregator dies, the terminal failure event can be lost because there is no GCS fallback, so tasks may never be marked failed in the dashboard-head store.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1e39141. Configure here.

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>
@karticam
karticam changed the base branch from master to karticam/reroute-ray-timeline August 6, 2026 09:48
@karticam
karticam changed the base branch from karticam/reroute-ray-timeline to master August 6, 2026 09:48
@karticam
karticam force-pushed the karticam/test-task-events-end-to-end-10-n branch from 1e39141 to 899bd35 Compare August 6, 2026 09:53

@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 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

Reviewed by Cursor Bugbot for commit 899bd35. 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 _task_events_head_client cached. After reconnect, _get_task_events_head_client() keeps returning that client, which still holds the old accessor, so ray.timeline() / profile_events() can query with a stale GCS handle when the dashboard-head read path is enabled.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 899bd35. Configure here.

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 unstale A PR that has been marked unstale. It will not get marked stale again if this label is on it.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant