Skip to content

[core][taskEvents out of GCS][8/n] Reroute state head to task events head for state APIs. - #65160

Open
karticam wants to merge 7 commits into
ray-project:karticam/gcs-task-mgr-to-db-head-2from
karticam:karticam/reroute-state-head
Open

[core][taskEvents out of GCS][8/n] Reroute state head to task events head for state APIs.#65160
karticam wants to merge 7 commits into
ray-project:karticam/gcs-task-mgr-to-db-head-2from
karticam:karticam/reroute-state-head

Conversation

@karticam

@karticam karticam commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Part of the effort to move task events out of GCS.
This PR builds on top of #65158

#65158 adds logic on task events head to serve queries for task events. This PR adds the logic to route the task related state APIs (ray list tasks) to task events head rather than GCS.

Changes done:

  1. Adds a flag RAY_task_events_read_from_dashboard_head which determines whether state head sends the request to GCS or dashboard head (specifically task events head).
  2. If the request is send to dashboard head, it makes an HTTP client to task events head. Since both of them are subprocesses on the same node, they just communicate through unix sockets without the need for any auth.
  3. The HTTP client is made lazily on the first request.

@karticam
karticam requested a review from a team as a code owner August 1, 2026 09:19
@karticam karticam added core Issues that should be addressed in Ray Core go add ONLY when ready to merge, run all tests labels Aug 1, 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 introduces an in-memory task event storage and query system on the dashboard head, allowing task queries to be served directly from the dashboard instead of GCS. It implements pubsub subscribers for worker and job updates, a publisher client, and a dedicated TaskEventsHead module with garbage-collection policies. The review feedback is highly constructive and identifies several critical areas for improvement: a bug in the query logic that incorrectly reverses priority tiers during truncation, fragile pubsub subscriptions that lack startup retry logic, missing exception handling for gRPC calls and event conversion, and silent background task failures due to unhandled exceptions in done callbacks.

Comment on lines +137 to +138
# Iterate newest-first so a limit keeps the most recent events.
for task_event in reversed(candidates):

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

Reversing the concatenated candidates list reverses the priority tiers, placing Tier 2 (running/failed tasks) at the end of the list. This causes running tasks to be truncated first when a limit is applied. We should instead iterate over candidates directly after updating get_all_task_events to return newest-first within each tier.

Suggested change
# Iterate newest-first so a limit keeps the most recent events.
for task_event in reversed(candidates):
# Iterate newest-first so a limit keeps the most recent events.
for task_event in candidates:

Comment on lines +293 to +298
def get_all_task_events(self) -> List[gcs_pb2.TaskEvents]:
"""All stored task events, higher-priority tiers first, oldest-first within a tier."""
result: List[gcs_pb2.TaskEvents] = []
for tier in range(self._gc_policy.MAX_PRIORITY - 1, -1, -1):
result.extend(self._tiers[tier].values())
return result

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

Update get_all_task_events to return newest-first within each tier. This ensures that when candidates are processed, the most recent events within each priority tier are kept, while preserving the priority tier order (Tier 2 first, then Tier 1, then Tier 0).

Suggested change
def get_all_task_events(self) -> List[gcs_pb2.TaskEvents]:
"""All stored task events, higher-priority tiers first, oldest-first within a tier."""
result: List[gcs_pb2.TaskEvents] = []
for tier in range(self._gc_policy.MAX_PRIORITY - 1, -1, -1):
result.extend(self._tiers[tier].values())
return result
def get_all_task_events(self) -> List[gcs_pb2.TaskEvents]:
"""All stored task events, higher-priority tiers first, newest-first within a tier."""
result: List[gcs_pb2.TaskEvents] = []
for tier in range(self._gc_policy.MAX_PRIORITY - 1, -1, -1):
result.extend(reversed(self._tiers[tier].values()))
return result

Comment on lines +170 to +192
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.

high

Calling subscriber.subscribe() outside the while True loop makes the subscription fragile. If GCS is temporarily unavailable during startup, the subscription will fail, the background task will terminate, and the dashboard head will never receive notifications for the lifetime of the process. Wrapping the subscription and the loop in a retry block with a delay ensures robustness.

    async def _subscribe_for_worker_deaths(self) -> None:
        while True:
            try:
                subscriber = GcsAioWorkerDeltaSubscriber(address=self.gcs_address)
                await subscriber.subscribe()
                while True:
                    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. Retrying in 5 seconds...")
                await asyncio.sleep(5)

    async def _subscribe_for_finished_jobs(self) -> None:
        while True:
            try:
                subscriber = GcsAioJobSubscriber(address=self.gcs_address)
                await subscriber.subscribe()
                while True:
                    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. Retrying in 5 seconds...")
                await asyncio.sleep(5)

Comment on lines +156 to +168
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.aiogrpc_gcs_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

The gRPC call self._worker_info_stub.GetWorkerInfo is not wrapped in a try-except block. If GCS is down or there is a network issue, it will raise a grpc.RpcError, causing the background task _on_worker_dead to crash ungracefully. Wrapping it in a try-except block ensures the background task handles the error 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.aiogrpc_gcs_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.aiogrpc_gcs_channel
)
try:
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
except Exception as e:
logger.error(f"Failed to get worker info for {worker_id.hex()}: {e}")
return None

Comment on lines +66 to +92
@routes.post("/api/task_events")
async def add_task_events(
self, request: aiohttp.web.Request
) -> aiohttp.web.Response:
body = await request.read()
try:
add_events_request = self._deserialize_request(body)
except Exception as e:
logger.warning(f"Failed to deserialize task events request: {e}")
return dashboard_optional_utils.rest_response(
status_code=dashboard_utils.HTTPStatusCode.INTERNAL_ERROR,
message=f"Failed to deserialize task events request: {e}",
)

task_events, dropped_task_attempts = convert_to_task_events(add_events_request)
self._store.record_data_loss_from_worker(dropped_task_attempts)
for task_event in task_events:
self._store.add_or_replace_task_event(task_event)
logger.debug(
"Received %d task events (%d attempts stored)",
len(task_events),
self._store.num_task_events_stored,
)
return dashboard_optional_utils.rest_response(
status_code=dashboard_utils.HTTPStatusCode.OK,
message="",
)

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

convert_to_task_events and the subsequent store updates are called outside the try-except block. If any conversion or processing error occurs, it will result in an unhandled 500 error. Wrapping the entire processing logic in the try-except block ensures robust error handling.

    @routes.post("/api/task_events")
    async def add_task_events(
        self, request: aiohttp.web.Request
    ) -> aiohttp.web.Response:
        body = await request.read()
        try:
            add_events_request = self._deserialize_request(body)
            task_events, dropped_task_attempts = convert_to_task_events(add_events_request)
            self._store.record_data_loss_from_worker(dropped_task_attempts)
            for task_event in task_events:
                self._store.add_or_replace_task_event(task_event)
            logger.debug(
                "Received %d task events (%d attempts stored)",
                len(task_events),
                self._store.num_task_events_stored,
            )
            return dashboard_optional_utils.rest_response(
                status_code=dashboard_utils.HTTPStatusCode.OK,
                message="",
            )
        except Exception as e:
            logger.warning(f"Failed to process task events request: {e}")
            return dashboard_optional_utils.rest_response(
                status_code=dashboard_utils.HTTPStatusCode.INTERNAL_ERROR,
                message=f"Failed to process task events request: {e}",
            )

Comment on lines +202 to +205
def _spawn(self, coro) -> None:
task = asyncio.create_task(coro)
self._background_tasks.add(task)
task.add_done_callback(self._background_tasks.discard)

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

Background tasks are created without any exception handling in their done callbacks. If a background task raises an exception, it will fail silently with a "Task exception was never retrieved" warning. Adding a done callback that logs exceptions ensures background task failures are visible.

Suggested change
def _spawn(self, coro) -> None:
task = asyncio.create_task(coro)
self._background_tasks.add(task)
task.add_done_callback(self._background_tasks.discard)
def _spawn(self, coro) -> None:
task = asyncio.create_task(coro)
self._background_tasks.add(task)
def done_callback(t):
self._background_tasks.discard(t)
try:
t.result()
except asyncio.CancelledError:
pass
except Exception:
logger.exception("Background task failed")
task.add_done_callback(done_callback)

Comment thread python/ray/dashboard/modules/state/state_head.py
Comment thread python/ray/util/state/state_manager.py Outdated
Comment thread python/ray/util/state/state_manager.py
@karticam
karticam force-pushed the karticam/reroute-state-head branch 2 times, most recently from a74d19a to 506e9bf Compare August 3, 2026 09:47

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

Fix All in Cursor

Reviewed by Cursor Bugbot for commit 506e9bf. Configure here.

Comment thread python/ray/dashboard/modules/task_events/task_event_storage.py Outdated
@karticam
karticam force-pushed the karticam/reroute-state-head branch from 506e9bf to f9bd399 Compare August 3, 2026 17:50
@karticam
karticam changed the base branch from master to karticam/task-events-query-on-db-head August 4, 2026 17:26
@karticam
karticam force-pushed the karticam/task-events-query-on-db-head branch from 714fb66 to 4ccf6bc Compare August 5, 2026 10:09
@karticam
karticam force-pushed the karticam/reroute-state-head branch 2 times, most recently from ca17504 to f84c2ce Compare August 6, 2026 00:03
@karticam
karticam force-pushed the karticam/task-events-query-on-db-head branch from 509eb72 to 303e70b Compare August 6, 2026 22:57
@karticam
karticam force-pushed the karticam/reroute-state-head branch from f84c2ce to 0045511 Compare August 6, 2026 22:58
"""End-to-end reroute of ``ray list tasks`` from GCS to the dashboard head.

With ``RAY_enable_task_events_to_dashboard_head`` on, ``StateHead`` queries the
``TaskEventsHead`` subprocess over its unix socket instead of GCS. This drives the

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: I feel this comment could be simplified and written more briefly also too much context

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.

done

Comment thread python/ray/util/state/state_manager.py Outdated
async def _get_task_events_from_dashboard_head(
self, request: GetTaskEventsRequest, timeout: int = None
) -> Optional[GetTaskEventsReply]:
if self._dashboard_socket_dir is None or self._dashboard_session_name is None:

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: we can move this check to init (only checked if _READ_TASK_EVENTS_FROM_DASHBOARD_HEAD is true)

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.

done

@sampan-s-nayak sampan-s-nayak 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.

LGTM, as discussed we can use a common task_event_module across all code paths calling this module (aggregator agent, state manager and other one-off cases)

@karticam
karticam force-pushed the karticam/task-events-query-on-db-head branch from 303e70b to 02f7126 Compare August 7, 2026 15:58
@karticam
karticam force-pushed the karticam/reroute-state-head branch from 0045511 to 5de0612 Compare August 7, 2026 16:44
@karticam
karticam force-pushed the karticam/task-events-query-on-db-head branch from 473484c to d51377e Compare August 7, 2026 20:32
@karticam
karticam force-pushed the karticam/reroute-state-head branch from 2a899f1 to 1847097 Compare August 7, 2026 20:32
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>
@karticam
karticam force-pushed the karticam/reroute-state-head branch from 1847097 to 1117ea1 Compare August 7, 2026 22:57
@karticam
karticam requested review from a team as code owners August 7, 2026 22:57
@karticam
karticam changed the base branch from karticam/task-events-query-on-db-head to karticam/gcs-task-mgr-to-db-head-2 August 7, 2026 22:58
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.

2 participants