[core][taskEvents out of GCS][10/n] Fix bugs found after migration - #65247
[core][taskEvents out of GCS][10/n] Fix bugs found after migration #65247karticam wants to merge 5 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request migrates task events from GCS to the dashboard head, introducing a new TaskEventsHead subprocess module to store and query task events in memory, and a TaskEventManager to handle background reconciliation against GCS worker and job events. The state API and timeline queries are updated to read from this new dashboard-head store when enabled. Feedback on the changes suggests several robustness improvements: handling missing worker info and wrapping gRPC calls in try-except blocks in TaskEventManager, implementing retry loops with delays for pubsub subscriptions to avoid CPU-burning loops, using an insertion-ordered dict for FIFO eviction of dropped task attempts in TaskEventStorage, and wrapping the entire request handler in TaskEventsHead to prevent unhandled 500 errors.
| 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 |
There was a problem hiding this comment.
Wrap the gRPC call to GetWorkerInfo in a try-except block to handle transient network or gRPC errors gracefully. Otherwise, any gRPC exception will propagate and crash the _on_worker_dead task, preventing task failure marking.
| 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) | |
| ) | |
| if not reply.HasField("worker_table_data"): | |
| return None | |
| return reply.worker_table_data | |
| except Exception as e: | |
| logger.warning(f"Failed to get worker info for {worker_id.hex()}: {e}") | |
| return None |
| def __init__(self): | ||
| self._num_profile_events_dropped = 0 | ||
| self._num_task_attempts_dropped_tracked = 0 | ||
| self._num_dropped_task_attempts_evicted = 0 | ||
| self._dropped_task_attempts: Set[TaskAttempt] = set() | ||
|
|
||
| def record_task_attempt_dropped(self, task_attempt: TaskAttempt) -> None: | ||
| self._dropped_task_attempts.add(task_attempt) | ||
| self._num_task_attempts_dropped_tracked = len(self._dropped_task_attempts) |
There was a problem hiding this comment.
Using a set for self._dropped_task_attempts results in arbitrary eviction of tracked dropped attempts when the cap is reached. If a recently dropped attempt is arbitrarily evicted, late events for it could be partially re-stored. Using a dict as an ordered set (since Python 3.7+ dicts preserve insertion order) allows us to perform FIFO eviction, ensuring we keep the most recently dropped attempts tracked.
| def __init__(self): | |
| self._num_profile_events_dropped = 0 | |
| self._num_task_attempts_dropped_tracked = 0 | |
| self._num_dropped_task_attempts_evicted = 0 | |
| self._dropped_task_attempts: Set[TaskAttempt] = set() | |
| def record_task_attempt_dropped(self, task_attempt: TaskAttempt) -> None: | |
| self._dropped_task_attempts.add(task_attempt) | |
| self._num_task_attempts_dropped_tracked = len(self._dropped_task_attempts) | |
| def __init__(self): | |
| self._num_profile_events_dropped = 0 | |
| self._num_task_attempts_dropped_tracked = 0 | |
| self._num_dropped_task_attempts_evicted = 0 | |
| self._dropped_task_attempts: Dict[TaskAttempt, None] = {} | |
| def record_task_attempt_dropped(self, task_attempt: TaskAttempt) -> None: | |
| self._dropped_task_attempts[task_attempt] = None | |
| self._num_task_attempts_dropped_tracked = len(self._dropped_task_attempts) |
| num_to_evict = min( | ||
| len(self._dropped_task_attempts), num_to_evict + int(0.1 * num_to_evict) | ||
| ) | ||
| self._num_task_attempts_dropped_tracked = len(self._dropped_task_attempts) | ||
| if num_to_evict == 0: | ||
| return | ||
| self._num_dropped_task_attempts_evicted += num_to_evict | ||
| to_evict = list(self._dropped_task_attempts)[:num_to_evict] | ||
| self._dropped_task_attempts.difference_update(to_evict) | ||
| self._num_task_attempts_dropped_tracked = len(self._dropped_task_attempts) |
There was a problem hiding this comment.
Evict the oldest dropped task attempts first using FIFO order by leveraging the insertion-ordered dict keys.
| num_to_evict = min( | |
| len(self._dropped_task_attempts), num_to_evict + int(0.1 * num_to_evict) | |
| ) | |
| self._num_task_attempts_dropped_tracked = len(self._dropped_task_attempts) | |
| if num_to_evict == 0: | |
| return | |
| self._num_dropped_task_attempts_evicted += num_to_evict | |
| to_evict = list(self._dropped_task_attempts)[:num_to_evict] | |
| self._dropped_task_attempts.difference_update(to_evict) | |
| self._num_task_attempts_dropped_tracked = len(self._dropped_task_attempts) | |
| num_to_evict = min( | |
| len(self._dropped_task_attempts), num_to_evict + int(0.1 * num_to_evict) | |
| ) | |
| self._num_task_attempts_dropped_tracked = len(self._dropped_task_attempts) | |
| if num_to_evict == 0: | |
| return | |
| self._num_dropped_task_attempts_evicted += num_to_evict | |
| to_evict = list(self._dropped_task_attempts.keys())[:num_to_evict] | |
| for k in to_evict: | |
| del self._dropped_task_attempts[k] | |
| self._num_task_attempts_dropped_tracked = len(self._dropped_task_attempts) |
There was a problem hiding this comment.
in same parity as GcsTaskManager
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
Reviewed by Cursor Bugbot for commit 73a8272. Configure here.
c1012be to
031279b
Compare
73a8272 to
dc8e355
Compare
There was a problem hiding this comment.
Wondering should we add tests to verify the added log task log info conversion?
| ) | ||
|
|
||
|
|
||
| def _convert_task_log_info(src, dst) -> None: |
There was a problem hiding this comment.
nit: the src and dst can be confusing. Better to add some descriptions to talk about the conversion is from where to where.
There was a problem hiding this comment.
Wondering is the task_log_info field covered in any of the test state api tests?
| # These delays are read by the dashboard-head store from ray._config; set them via | ||
| # env vars so the head subprocess picks them up even if _system_config does not | ||
| # propagate to the head's ray._config. | ||
| monkeypatch.setenv("RAY_gcs_mark_task_failed_on_job_done_delay_ms", "1000") |
There was a problem hiding this comment.
Nit: if we've already set the env vars through monkeypatch, should we remove the settings to the _system_config?
| # These make list_nodes, list_workers, list_actors never return in 20secs | ||
| # Pin the read path to GCS so list_tasks stays a delayable GCS | ||
| # GetTaskEvents query. | ||
| m.setenv("RAY_enable_task_events_to_dashboard_head", "0") |
There was a problem hiding this comment.
Could be address in a separate PR but wondering is the behavior worth testing with the new setup?
031279b to
30afc48
Compare
dc8e355 to
b93cf23
Compare
9387c98 to
20138e3
Compare
b93cf23 to
5551877
Compare
20138e3 to
e477ec8
Compare
5551877 to
94e50b4
Compare
8a6583a to
f170291
Compare
94e50b4 to
76a7522
Compare
97f0d17 to
f5c37be
Compare
76a7522 to
3c1efde
Compare
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>
3c1efde to
7522e44
Compare

Part of the effort to move task events out of GCS.
This PR builds on top of #65218
To test the entire feature, I took changes from PR 2/n to 9/n, changed the flags so that
task_event_bufferto GCS is stopped,ray_task_event_recorderis used, task events are sent to task events head via aggregator agent, and state APIs and ray timeline consume task events from task event head.essentially these flags were configured:
enable_ray_event: trueenable_ray_task_event_recorder: trueenable_task_events_to_dashboard_head: trueenable_core_worker_task_event_to_gcs: falseSome issues in tests were found. This PR resolves those issues, so that premerge tests pass even even after we switch the flags ON default