[core][taskEvents out of GCS][9/n] Reroute ray.timeline to task events head - #65218
[core][taskEvents out of GCS][9/n] Reroute ray.timeline to task events head#65218karticam wants to merge 10 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces TaskEventsHeadClient to fetch task events from the dashboard head instead of the GCS when the migration flag RAY_ENABLE_TASK_EVENTS_TO_DASHBOARD_HEAD is enabled, caching the endpoint and reusing the HTTP session. Unit tests have been added to verify both migration-enabled and default behaviors. The review feedback suggests broadening exception handling to requests.RequestException to catch all request-related failures, improving authentication error messages using format_authentication_http_error, and ensuring thread-safety and accessor freshness during the lazy initialization of the client.
| except (requests.ConnectionError, requests.Timeout): | ||
| # The dashboard may have restarted at a new address; re-resolve next call. | ||
| self._endpoint = None | ||
| raise |
There was a problem hiding this comment.
Catching only requests.ConnectionError and requests.Timeout might miss other request-related exceptions (such as DNS resolution failures or other RequestException subclasses) that also indicate the dashboard head is unreachable or restarted. Broadening the exception handling to requests.RequestException ensures the cached endpoint is correctly invalidated in all failure scenarios.
| except (requests.ConnectionError, requests.Timeout): | |
| # The dashboard may have restarted at a new address; re-resolve next call. | |
| self._endpoint = None | |
| raise | |
| except requests.RequestException: | |
| # The dashboard may have restarted at a new address; re-resolve next call. | |
| self._endpoint = None | |
| raise |
There was a problem hiding this comment.
Seems to be a valid comment
| if response.status_code != 200: | ||
| raise RuntimeError( | ||
| f"Failed to read task events from the dashboard head at {endpoint}: " | ||
| f"HTTP {response.status_code} {response.reason}." | ||
| ) |
There was a problem hiding this comment.
If token authentication is enabled, requests to the dashboard head might fail with HTTP 401 (Unauthorized) or 403 (Forbidden). Using format_authentication_http_error allows us to raise a much more user-friendly and informative error message explaining how to resolve the authentication issue.
| if response.status_code != 200: | |
| raise RuntimeError( | |
| f"Failed to read task events from the dashboard head at {endpoint}: " | |
| f"HTTP {response.status_code} {response.reason}." | |
| ) | |
| if response.status_code != 200: | |
| from ray._private.authentication.http_token_authentication import ( | |
| format_authentication_http_error, | |
| ) | |
| auth_err = format_authentication_http_error( | |
| response.status_code, response.text | |
| ) | |
| if auth_err: | |
| raise RuntimeError(auth_err) | |
| raise RuntimeError( | |
| f"Failed to read task events from the dashboard head at {endpoint}: " | |
| f"HTTP {response.status_code} {response.reason}." | |
| ) |
There was a problem hiding this comment.
Seems like a good comment for better observability in error handling
| def _get_task_events_head_client(self) -> "TaskEventsHeadClient": | ||
| """Lazily build and cache the reusable dashboard-head task-events client.""" | ||
| if self._task_events_head_client is None: | ||
| self._task_events_head_client = TaskEventsHeadClient( | ||
| self._connect_and_get_accessor() | ||
| ) | ||
| return self._task_events_head_client |
There was a problem hiding this comment.
The lazy initialization of _task_events_head_client is not thread-safe and can lead to race conditions if multiple threads call _get_task_events_head_client concurrently. Additionally, if GlobalState is disconnected and reconnected, the cached client will hold a reference to the old, stale accessor. Protecting the initialization with self._init_lock and checking if the accessor has changed ensures thread-safety and robustness under reconnects.
| def _get_task_events_head_client(self) -> "TaskEventsHeadClient": | |
| """Lazily build and cache the reusable dashboard-head task-events client.""" | |
| if self._task_events_head_client is None: | |
| self._task_events_head_client = TaskEventsHeadClient( | |
| self._connect_and_get_accessor() | |
| ) | |
| return self._task_events_head_client | |
| def _get_task_events_head_client(self) -> "TaskEventsHeadClient": | |
| """Lazily build and cache the reusable dashboard-head task-events client.""" | |
| accessor = self._connect_and_get_accessor() | |
| with self._init_lock: | |
| if ( | |
| self._task_events_head_client is None | |
| or self._task_events_head_client._accessor is not accessor | |
| ): | |
| self._task_events_head_client = TaskEventsHeadClient(accessor) | |
| return self._task_events_head_client |
There was a problem hiding this comment.
Seemed to be a valid comment
There was a problem hiding this comment.
done. though not done in the suggested way since it has a race. the accessor we get initially might change later on inside the lock since and we might build the client with a stale accessor.
therefore use the accessor that you grab inside the lock/
f9bd399 to
ca17504
Compare
7000109 to
16c84bd
Compare
ca17504 to
f84c2ce
Compare
16c84bd to
c1012be
Compare
f84c2ce to
0045511
Compare
c1012be to
031279b
Compare
| ) | ||
|
|
||
| if _READ_TASK_EVENTS_FROM_DASHBOARD_HEAD: | ||
| logger.warning( |
There was a problem hiding this comment.
Nit: we should probably only output the warning if the dashboard is not started with the cluster. Or we should probably be clear that if there is no information shown or receive certain error, the user should check for whether the dashboard is enabled.
There was a problem hiding this comment.
removed it from here now. now we throw only in error cases which is:
- when it tries to get the dashboard endpoint, but GCS internal KV returns no endpoint.
- if it gets the endpoint, but the request fails. .
so we dont proactively throw the error, but show it only when there is some error
|
|
||
| return dict(result) | ||
|
|
||
| def _get_profiling_task_events(self): |
There was a problem hiding this comment.
The function name seems a bit misleading. We just get all the task events in this function without filtering for profile events so probably should update the function name here.
| def _get_task_events_head_client(self) -> "TaskEventsHeadClient": | ||
| """Lazily build and cache the reusable dashboard-head task-events client.""" | ||
| if self._task_events_head_client is None: | ||
| self._task_events_head_client = TaskEventsHeadClient( | ||
| self._connect_and_get_accessor() | ||
| ) | ||
| return self._task_events_head_client |
There was a problem hiding this comment.
Seemed to be a valid comment
| address = address.decode() | ||
| if not address.startswith(("http://", "https://")): | ||
| address = f"http://{address}" | ||
| self._endpoint = f"{address}/api/task_events/query" |
There was a problem hiding this comment.
Nit: The endpoint should probably be a constant string so that the same can be referenced using same constant.
| self._endpoint = f"{address}/api/task_events/query" | ||
| return self._endpoint | ||
|
|
||
| def get_task_events(self, timeout: int): |
There was a problem hiding this comment.
Nit: The return type annotation is missing
| # The dashboard may have restarted at a new address; re-resolve next call. | ||
| self._endpoint = None | ||
| raise | ||
| if response.status_code != 200: |
There was a problem hiding this comment.
Wondering if we should check 2xx instead of 200? Also, wondering if we should use something like response.raise_for_status()?
There was a problem hiding this comment.
Checked for 2xx manually.
Dint' use response.raise_for_status() since first, it raises on only >= 400 (lets 3xx pass by) and second we want to throw a RuntimeError with our custom message. Doing a raise_for_status() , we would have to wrap it around try catch and then rethrow RuntimeError.
| if response.status_code != 200: | ||
| raise RuntimeError( | ||
| f"Failed to read task events from the dashboard head at {endpoint}: " | ||
| f"HTTP {response.status_code} {response.reason}." | ||
| ) |
There was a problem hiding this comment.
Seems like a good comment for better observability in error handling
There was a problem hiding this comment.
I'm wondering if we should add an e2e test to validate the same information can be returned between the GCS path and the event_head path.
There was a problem hiding this comment.
added the tests - both unit tests and integration tests. premerge is still running to see if they work
| ) | ||
| reply = gcs_service_pb2.GetTaskEventsReply() | ||
| reply.ParseFromString(response.content) | ||
| if reply.status.code != 0: |
There was a problem hiding this comment.
nit: it's better to use a constant like StatusCode.OK instead of the actual code number.
There was a problem hiding this comment.
made a constant in this file. HTTP status code constant OK resolves to 200, so just using our own variable for this
2a899f1 to
1847097
Compare
031279b to
30afc48
Compare
1847097 to
1117ea1
Compare
9387c98 to
20138e3
Compare
1117ea1 to
19b9fca
Compare
20138e3 to
e477ec8
Compare
5ceb88a to
6d7f58d
Compare
8a6583a to
f170291
Compare
f170291 to
f521f5b
Compare
There was a problem hiding this comment.
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).
❌ 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 f521f5b. Configure here.
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>
d30bc16 to
97f0d17
Compare
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
97f0d17 to
f5c37be
Compare
MengjinYan
left a comment
There was a problem hiding this comment.
All nits and things that can be addressed in followup PRs.
| # GcsStatus.code value for a successful reply. | ||
| _GCS_STATUS_CODE_OK = 0 |
There was a problem hiding this comment.
Could resolved in a followup PR. Looks like the GCS status code is the same set of code used in status.h. To avoid duplicate definition, it would be better to use the cython binding here. For now, it's better to add a comment about where the code is defined.
| # Timeout for the synchronous task-events query to the dashboard head. | ||
| _DASHBOARD_HEAD_QUERY_TIMEOUT_S = 30 | ||
| # Dashboard-head route that answers task-events queries. | ||
| _TASK_EVENTS_QUERY_PATH = "/api/task_events/query" |
There was a problem hiding this comment.
Could be resolved in a followup PR. Looks like the constant could be used in other places as well. It's better to move it to a more general place and use the constant in other places.
| raise RuntimeError( | ||
| "Ray has not been started yet. Timeline requires Ray to be initialized first." | ||
| ) | ||
|
|
There was a problem hiding this comment.
I might not be clear in my previous comment. But regarding the warning, let's just add one in all cases saying that the timeline feature will be served from the API server in the near future, and if you don't have the API server enabled, please do so to avoid interruption to the usage.
|
|
||
| def __init__(self, accessor: GlobalStateAccessor): | ||
| self._accessor = accessor | ||
| self._session = requests.Session() |
There was a problem hiding this comment.
It seems that the session is never closed. We should probably add a close function for the TaskEventHeadLClient and close it in the disconnect function in the GlobalState.
There was a problem hiding this comment.
fixed. thanks for pointing out
Signed-off-by: Kartica Modi <karticamodi@gmail.com>

Part of the effort to move task events out of GCS.
This PR builds on top of #65160
Similar to how we route
ray list tasksto task events head based on the flagRAY_ENABLE_TASK_EVENTS_TO_DASHBOARD_HEAD, we also route ray.timeline to task events head based on the flag. If the flag is not set, GCS is queried.