[cherry-pick][2.58.0][core][taskEvents out of GCS][8/n] Reroute state head to task events head for state APIs. (#65160) - #65304
Conversation
… agent to dashboard head (#65028) Part of the effort to move task-event observability data out of GCS and onto the dashboard head. This PR adds support for moving task events from aggregator agent to dashboard head. Changes are as follows: 1. Adds a new module `task_events_head` that hosts a POST `/api/task_events` endpoint to receive task events from the aggregator agent. For now it just buffers received events in memory. More stuff to be done in upcoming PRs. 2. Adds a new publisher client `AsyncDashboardHeadPublisherClient` and a corresponding flag to `PUBLISH_EVENTS_TO_DASHBOARD_HEAD`. 3. The existing publisher `AsyncGCSTaskEventsPublisherClient` is still kept since some tests depend on it. This will be removed later after the entire migration completes. 4. The new publisher client is similar to `AsyncGCSTaskEventsPublisherClient` in that it has the same event selection to export and has the same proto building, but sends the request over HTTP POST instead of a gRPC call. 5. Each of the two publishers gets its own dropped-task-attempts metadata buffer, since`TaskEventsMetadataBuffer.get()` is destructive and a shared buffer would split the metadata across publishers. 6. The publisher resolves the dashboard head's address lazily from InternalKV (DASHBOARD_ADDRESS) via `gcs_client`, then makes the POST request. If the address isn't registered yet the publish is marked unsuccessful and the publisher retries on its next cycle; once resolved, the endpoint is cached. --------- Signed-off-by: Kartica Modi <karticamodi@gmail.com> (cherry picked from commit 3ca1915) Signed-off-by: elliot-barn <elliot.barnwell@anyscale.com>
…death and job finished (#65057) This PR builds on top of: #65028 and therefore has some changes from that PR as well. GcsTaskManager does task state management based on worker death and job finished notifications. Since as part of the project to move task events out of GCS, we will be putting all this logic on dashboard head (specifically, task_events_head), we need these notifications on task_events_head as well. This PR adds `GcsAioWorkerDeltaSubscriber` and `GcsAioJobSubscriber` and makes `task_events_head` subscribe to GCS for worker delta notifications and job notifications from GCS. For workers delta notifications, we get notifications only on worker deaths. For jobs, any action is taken only when the job is finished. Right now, we just append them to a list. More logic (similar to what is being done in `GcsTaskManager`) will be added in subsequent PRs. --------- Signed-off-by: Kartica Modi <karticamodi@gmail.com> Co-authored-by: Elliot Barnwell <elliot.barnwell@anyscale.com> (cherry picked from commit dc13372) Signed-off-by: elliot-barn <elliot.barnwell@anyscale.com>
…ntation on task events head (#65123) Part of the effort to move task events out of GCS. This PR builds on top of #65057 This is the first part of implementing `GcsTaskManager` inside `task_events_head` Changes done: 1. `ray_event_converter.py`: the incoming requests are `AddEventsRequest` but the storage stores `TaskEvents`, and `dropped_task_attempts`. This layer converts the incoming requests to storage friendly format. 2. `gc_policy.py`: `FinishedTaskActorTaskGcPolicy`: 3 priority tiers (finished tasks, actor, others) that decide eviction order of task events when needed. Mimics the existing policy in `GcsTaskManager`. 3. `task_event_storage.py`: Based on `TaskEventStorage` in `GcsTaskManager`. - The entire storage is organized in three tiered lists (actually implemented using insertion ordered-dicts keyed by taskAttempt in python). For every task attempt, we hold one task event entry, which goes to one of the tiers based on whether the task attempt is finished, is actor task or none of this. - When more task events for an attempt arrive, they are merged with an existing entry and moved across tiers if required. - Task attempts for which events are dropped (either at the source level i.e. core_worker level, or due to eviction here) are maintained in `dropped_task_attempts` per job. - Priority-tiered eviction once over MAX_NUM_TASK_EVENTS (100k), evicting the oldest in the lowest-priority tier first. - `dropped_task_attempts` maintained per job which garbage when the job finishes. If per job list grows past a threshold, its pruned every 5 seconds. - Tracks the counters (stored / reported / attempts-dropped / profile-dropped / per-type). These will be emitted in a followup PR. Followup PRs: 1. Changing task states based on worker death and job finished notification. Receiving those notifications is already wired up. 2. Metric emissions. 3. Serving state API from task events head. --------- Signed-off-by: Kartica Modi <karticamodi@gmail.com> (cherry picked from commit 471d3cc) Signed-off-by: elliot-barn <elliot.barnwell@anyscale.com>
…ath and job notifications (#65141) Part of the effort to move task events out of GCS. This PR builds on top of #65123 which adds the in-memory store on the task events head. Changes done: 1. Adds functions to mark task failed if needed and calls it when a worker is dead or job is finished. 2. When a job update is received, checks if the update is for job finished. If yes, triggers `_on_job_finished` which calls the function to mark tasks failed when job ends, and a function to GC dropped attempts for a finished job. This is done after a 15 second timeout (the same as existing timeout on GcsTaskManager). 3. When a worker update is received, its guaranteed to a worker death. However, we don't have the entire worker table information which is used for observability purposes. To get that, we query GCS one more time, and then mark apt tasks as failed. Also we mark the tasks failed after 1 second. Therefore, this wait is overlapped wth the GCS trip cost. A slight caveat is that if we don't get the worker table info due to some error, we still wait for 1 second before bailing out. NOTE: This PR also contains changes of #65158 squash merged into it. --------- Signed-off-by: Kartica Modi <karticamodi@gmail.com> (cherry picked from commit 328e6b7) Signed-off-by: elliot-barn <elliot.barnwell@anyscale.com>
…head for state APIs. (#65160) 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. --------- Signed-off-by: Kartica Modi <karticamodi@gmail.com> (cherry picked from commit 12436ea) Signed-off-by: elliot-barn <elliot.barnwell@anyscale.com>
There was a problem hiding this comment.
Code Review
This pull request implements the migration of task events from GCS to the dashboard head. It introduces a new TaskEventsHead subprocess module, an in-memory TaskEventStorage with priority-based garbage collection, and a TaskEventManager to reconcile task states against GCS worker and job lifecycle events. Additionally, it adds an HTTP publisher client to send events from the aggregator agent to the dashboard head and updates the State API to query from this new source when enabled. The review feedback highlights a few critical issues: a missing aiohttp import in the publisher client that would cause a NameError on connection failures, potential CPU-exhausting infinite loops in the GCS subscription polling error paths, and a double-counting bug in the dropped task attempts metric.
| @@ -1,3 +1,4 @@ | |||
| import asyncio | |||
There was a problem hiding this comment.
| except Exception: | ||
| logger.exception("Failed handling worker-death notifications.") |
There was a problem hiding this comment.
If subscriber.poll raises an exception repeatedly (e.g., due to a persistent GCS connection issue), this loop will spin infinitely without any delay, leading to 100% CPU exhaustion and log flooding. Adding a short sleep (e.g., await asyncio.sleep(1)) in the except block will prevent this.
| except Exception: | |
| logger.exception("Failed handling worker-death notifications.") | |
| except Exception: | |
| logger.exception("Failed handling worker-death notifications.") | |
| await asyncio.sleep(1) |
| except Exception: | ||
| logger.exception("Failed handling job-finished notifications.") |
There was a problem hiding this comment.
If subscriber.poll raises an exception repeatedly (e.g., due to a persistent GCS connection issue), this loop will spin infinitely without any delay, leading to 100% CPU exhaustion and log flooding. Adding a short sleep (e.g., await asyncio.sleep(1)) in the except block will prevent this.
| except Exception: | |
| logger.exception("Failed handling job-finished notifications.") | |
| except Exception: | |
| logger.exception("Failed handling job-finished notifications.") | |
| await asyncio.sleep(1) |
| self._stats[STAT_TOTAL_ATTEMPTS_DROPPED] += 1 | ||
| if attempt in self._primary_index: | ||
| self._remove_task_attempt(attempt) |
There was a problem hiding this comment.
The metric STAT_TOTAL_ATTEMPTS_DROPPED is incremented here, but if attempt in self._primary_index is true, _remove_task_attempt(attempt) is called, which increments the same metric again. This leads to double-counting of dropped task attempts. We should only increment it here if the attempt is not already in the index.
| self._stats[STAT_TOTAL_ATTEMPTS_DROPPED] += 1 | |
| if attempt in self._primary_index: | |
| self._remove_task_attempt(attempt) | |
| if attempt in self._primary_index: | |
| self._remove_task_attempt(attempt) | |
| else: | |
| self._stats[STAT_TOTAL_ATTEMPTS_DROPPED] += 1 |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 5a90b44. Configure here.
| @@ -1,3 +1,4 @@ | |||
| import asyncio | |||
There was a problem hiding this comment.
Unclear PR description
Low Severity
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:
- [Core] Introduce local port service discovery #59613
- [Core] Improve Large-Scale Resource View Synchronization Through Sync Message Batching #57641
- Remove node observability information from hot path of core components #56474
- [core][rdt] Support out-of-order actors by extracting metadata when creating #59610
- [core] fix open leak for plasma store memory (shm/fallback) by workers #52622
This violates the Clear PR Descriptions and Titles rule: the description covers cherry-pick stacking logistics but does not explain the problem being solved or how the change fixes it.
Triggered by project rule: Bugbot Rules
Reviewed by Cursor Bugbot for commit 5a90b44. Configure here.


Cherry-pick of #65160 (merge commit 12436ea) into
releases/2.58.0.This PR builds on #65141, so this branch is stacked on the #65141 cherry-pick (#65303) and must merge after it — until then the diff shown here includes both commits. The pick applied cleanly on top of that branch, and the patch-id matches the original merge commit exactly.
The intermediate 7/n (#65158) that this PR builds on needs no separate cherry-pick — it was merged into the #65141 PR branch rather than into master, so its content already arrives via #65303.
Not a duplicate: no existing open PR against
releases/2.58.0contains this change. Cherry-picked with AI assistance (Claude Code); pre-commit hooks ran on the changed files and passed.🤖 Generated with Claude Code