Skip to content

[core][taskEvents out of GCS][9/n] Reroute ray.timeline to task events head - #65218

Open
karticam wants to merge 10 commits into
ray-project:masterfrom
karticam:karticam/reroute-ray-timeline
Open

[core][taskEvents out of GCS][9/n] Reroute ray.timeline to task events head#65218
karticam wants to merge 10 commits into
ray-project:masterfrom
karticam:karticam/reroute-ray-timeline

Conversation

@karticam

@karticam karticam commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

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 tasks to task events head based on the flag RAY_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.

@karticam
karticam requested a review from a team as a code owner August 4, 2026 22:48
@karticam karticam added core Issues that should be addressed in Ray Core go add ONLY when ready to merge, run all tests labels Aug 4, 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 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.

Comment thread python/ray/_private/state.py Outdated
Comment on lines +82 to +85
except (requests.ConnectionError, requests.Timeout):
# The dashboard may have restarted at a new address; re-resolve next call.
self._endpoint = None
raise

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

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.

Suggested change
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

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.

Seems to be a valid comment

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/_private/state.py Outdated
Comment on lines +86 to +90
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}."
)

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

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.

Suggested change
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}."
)

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.

Seems like a good comment for better observability in error handling

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/_private/state.py Outdated
Comment on lines +369 to +375
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

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

Suggested change
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

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.

Seemed to be a valid comment

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

Comment thread python/ray/_private/state.py
@karticam
karticam force-pushed the karticam/reroute-state-head branch from f9bd399 to ca17504 Compare August 5, 2026 10:23
@karticam
karticam force-pushed the karticam/reroute-ray-timeline branch from 7000109 to 16c84bd Compare August 5, 2026 10:31
@karticam
karticam force-pushed the karticam/reroute-state-head branch from ca17504 to f84c2ce Compare August 6, 2026 00:03
@karticam
karticam force-pushed the karticam/reroute-ray-timeline branch from 16c84bd to c1012be Compare August 6, 2026 00:03
@karticam
karticam force-pushed the karticam/reroute-state-head branch from f84c2ce to 0045511 Compare August 6, 2026 22:58
@karticam
karticam force-pushed the karticam/reroute-ray-timeline branch from c1012be to 031279b Compare August 6, 2026 22:59
Comment thread python/ray/_private/state.py Outdated
)

if _READ_TASK_EVENTS_FROM_DASHBOARD_HEAD:
logger.warning(

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

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.

removed it from here now. now we throw only in error cases which is:

  1. when it tries to get the dashboard endpoint, but GCS internal KV returns no endpoint.
  2. 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

Comment thread python/ray/_private/state.py Outdated

return dict(result)

def _get_profiling_task_events(self):

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.

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.

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.

changed

Comment thread python/ray/_private/state.py Outdated
Comment on lines +369 to +375
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

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.

Seemed to be a valid comment

Comment thread python/ray/_private/state.py Outdated
address = address.decode()
if not address.startswith(("http://", "https://")):
address = f"http://{address}"
self._endpoint = f"{address}/api/task_events/query"

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: The endpoint should probably be a constant string so that the same can be referenced using same constant.

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/_private/state.py Outdated
self._endpoint = f"{address}/api/task_events/query"
return self._endpoint

def get_task_events(self, timeout: int):

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: The return type annotation is missing

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/_private/state.py Outdated
# The dashboard may have restarted at a new address; re-resolve next call.
self._endpoint = None
raise
if response.status_code != 200:

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.

Wondering if we should check 2xx instead of 200? Also, wondering if we should use something like response.raise_for_status()?

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.

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.

Comment thread python/ray/_private/state.py Outdated
Comment on lines +86 to +90
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}."
)

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.

Seems like a good comment for better observability in error handling

Comment thread python/ray/_private/state.py

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.

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.

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.

added the tests - both unit tests and integration tests. premerge is still running to see if they work

Comment thread python/ray/_private/state.py Outdated
)
reply = gcs_service_pb2.GetTaskEventsReply()
reply.ParseFromString(response.content)
if reply.status.code != 0:

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: it's better to use a constant like StatusCode.OK instead of the actual code number.

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.

made a constant in this file. HTTP status code constant OK resolves to 200, so just using our own variable for this

@karticam
karticam force-pushed the karticam/reroute-state-head branch 2 times, most recently from 2a899f1 to 1847097 Compare August 7, 2026 20:32
@karticam
karticam force-pushed the karticam/reroute-ray-timeline branch from 031279b to 30afc48 Compare August 7, 2026 21:04
@karticam
karticam force-pushed the karticam/reroute-state-head branch from 1847097 to 1117ea1 Compare August 7, 2026 22:57
@karticam
karticam force-pushed the karticam/reroute-ray-timeline branch from 9387c98 to 20138e3 Compare August 7, 2026 22:59
@karticam
karticam force-pushed the karticam/reroute-state-head branch from 1117ea1 to 19b9fca Compare August 8, 2026 01:08
@karticam
karticam force-pushed the karticam/reroute-ray-timeline branch from 20138e3 to e477ec8 Compare August 8, 2026 01:08
@karticam
karticam force-pushed the karticam/reroute-state-head branch from 5ceb88a to 6d7f58d Compare August 8, 2026 07:08
@karticam
karticam force-pushed the karticam/reroute-ray-timeline branch from 8a6583a to f170291 Compare August 8, 2026 07:09
Comment thread python/ray/tests/test_state_api_2.py Outdated
@karticam
karticam force-pushed the karticam/reroute-ray-timeline branch from f170291 to f521f5b Compare August 8, 2026 08:06

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

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

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

Comment thread python/ray/tests/test_state_api_2.py
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>
@karticam
karticam force-pushed the karticam/reroute-ray-timeline branch from d30bc16 to 97f0d17 Compare August 8, 2026 21:49
@karticam
karticam requested review from a team as code owners August 8, 2026 21:49
@karticam
karticam changed the base branch from karticam/reroute-state-head to master August 8, 2026 21:49
Signed-off-by: Kartica Modi <karticamodi@gmail.com>
@karticam
karticam force-pushed the karticam/reroute-ray-timeline branch from 97f0d17 to f5c37be Compare August 8, 2026 21:50

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

All nits and things that can be addressed in followup PRs.

Comment on lines +29 to +30
# GcsStatus.code value for a successful reply.
_GCS_STATUS_CODE_OK = 0

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.

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"

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.

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."
)

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.

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()

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.

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.

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.

fixed. thanks for pointing out

Signed-off-by: Kartica Modi <karticamodi@gmail.com>
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