[core][DO NOT MERGE] End-to-end testing changes for removing task events out of GCS - 3 - #65253
Conversation
- 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>
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>
There was a problem hiding this comment.
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.
| import asyncio | ||
| import json | ||
| import logging | ||
| from abc import ABC, abstractmethod |
There was a problem hiding this comment.
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.
| import asyncio | |
| import json | |
| import logging | |
| from abc import ABC, abstractmethod | |
| import asyncio | |
| import aiohttp | |
| import json | |
| import logging | |
| from abc import ABC, abstractmethod |
| 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.") |
There was a problem hiding this comment.
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.
| 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) |
| 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.") |
There was a problem hiding this comment.
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.
| 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"] |
There was a problem hiding this comment.
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.
| task_log_info = task_state["task_log_info"] | |
| task_log_info = task_state.get("task_log_info") |
| if not task_event.HasField("task_info"): | ||
| return False |
There was a problem hiding this comment.
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.
| 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 |
| // 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())); |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
| // (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>
4104239 to
57a8b8c
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
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 |
There was a problem hiding this comment.
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)
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 |
There was a problem hiding this comment.
Unclear PR title or 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 is effectively “test pr. do not merge” plus a link, and does not explain the problem or how this change fixes it.
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>


#65248 + ray task event recorder optimizations.
test pr. do not merge