Skip to content

Migrate the dashboard to spaday and serve it outside the cluster - #176

Merged
timkpaine merged 5 commits into
mainfrom
tkp/spaday-transports
Aug 21, 2026
Merged

Migrate the dashboard to spaday and serve it outside the cluster#176
timkpaine merged 5 commits into
mainfrom
tkp/spaday-transports

Conversation

@timkpaine

Copy link
Copy Markdown
Member

The dashboard was a Ray Serve FastAPI ingress holding the perspective tables, paired with a hand-rolled esbuild/pnpm frontend. Because the browser had to reach that ingress, it required an inbound port on the cluster, which is not always possible.

Replace the frontend with spaday, spaday-perspective and spaday-webawesome, and move the table host out of the cluster by default. The tracker actor now buffers table operations that the dashboard pulls over Ray's existing connection, so the browser only talks to localhost and the cluster accepts no new traffic. UI state syncs over transports; perspective keeps its own websocket for bulk data.

Two dashboard modes are available:

RayTaskTracker(dashboard="local") served from the caller's process
RayTaskTracker(dashboard="cluster") served from Ray Serve, as before

Pin spaday's asset layout to "installed". Its source/installed detection keys off a js/ directory beside the package, and unrelated wheels create one in site-packages, which 404s the runtime.

Build the Ray Serve app in serve_build_asgi_app rather than passing it to ingress. Ray pickles an ingress app to ship it to the replica, which fails on the perspective server and the transports store.

Drop js/, the hatch-js build hook and the NodeJS toolchain; raydar is now pure Python. Requires Python 3.11 for spaday-perspective, and moves perspective-python from 3.4 to 4.5.

Remove PerspectiveRayServer, PerspectiveProxyRayServer and setup_proxy_server. Table creation and updates go through RayTaskTracker.create_table and update_table as before.

The dashboard was a Ray Serve FastAPI ingress holding the perspective
tables, paired with a hand-rolled esbuild/pnpm frontend. Because the
browser had to reach that ingress, it required an inbound port on the
cluster, which is not always possible.

Replace the frontend with spaday, spaday-perspective and
spaday-webawesome, and move the table host out of the cluster by
default. The tracker actor now buffers table operations that the
dashboard pulls over Ray's existing connection, so the browser only
talks to localhost and the cluster accepts no new traffic. UI state
syncs over transports; perspective keeps its own websocket for bulk
data.

Two dashboard modes are available:

  RayTaskTracker(dashboard="local")    served from the caller's process
  RayTaskTracker(dashboard="cluster")  served from Ray Serve, as before

Pin spaday's asset layout to "installed". Its source/installed
detection keys off a js/ directory beside the package, and unrelated
wheels create one in site-packages, which 404s the runtime.

Build the Ray Serve app in __serve_build_asgi_app__ rather than passing
it to ingress. Ray pickles an ingress app to ship it to the replica,
which fails on the perspective server and the transports store.

Drop js/, the hatch-js build hook and the NodeJS toolchain; raydar is
now pure Python. Requires Python 3.11 for spaday-perspective, and moves
perspective-python from 3.4 to 4.5.

Remove PerspectiveRayServer, PerspectiveProxyRayServer and
setup_proxy_server. Table creation and updates go through
RayTaskTracker.create_table and update_table as before.

Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com>
CI failed importing raydar: ray.serve's haproxy module does
`from jinja2 import Environment` at import time, but ray[serve] does not
declare jinja2. raydar imports ray.serve at module level, so it has to
declare jinja2 itself. Dropping jinja2 along with the Jinja2 templating
went unnoticed locally because the old dependency was still installed.

pandas has the same shape and was already wrong before this branch: it
is imported at the top of task_tracker.py but was listed only under the
develop extra, so `pip install raydar; import raydar` failed.

Verified by installing the wheel into a fresh environment with only the
declared dependencies.

Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com>
@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Test Results

36 tests  +34   36 ✅ +34   29s ⏱️ -19s
 1 suites ± 0    0 💤 ± 0 
 1 files   ± 0    0 ❌ ± 0 

Results for commit 2b06f9e. ± Comparison against base commit 24417eb.

This pull request removes 1 and adds 35 tests. Note that renamed tests count towards both.
raydar.tests.test_task_tracker.TestRayTaskTracker ‑ test_get_proxy_server
raydar.tests.test_dashboard.TestDashboard ‑ test_an_empty_batch_does_not_touch_the_synced_state
raydar.tests.test_dashboard.TestDashboard ‑ test_apply_creates_tables_and_counts_rows
raydar.tests.test_dashboard.TestDashboard ‑ test_clear_empties_the_table
raydar.tests.test_dashboard.TestDashboard ‑ test_datetime_values_are_accepted
raydar.tests.test_dashboard.TestDashboard ‑ test_layout_follows_the_tables
raydar.tests.test_dashboard.TestDashboard ‑ test_layout_override_wins
raydar.tests.test_dashboard.TestDashboard ‑ test_limit_caps_retained_rows
raydar.tests.test_dashboard.TestDashboard ‑ test_repeated_schemas_do_not_recreate_tables
raydar.tests.test_dashboard.TestDashboard ‑ test_state_starts_empty
raydar.tests.test_dashboard.TestDashboard ‑ test_update_of_an_unknown_table_raises
…

♻️ This comment has been updated with latest results.

A poll-loop failure was the worst of these: `apply` was called outside
the try block, so one bad row killed the task and froze the dashboard
with no error surfaced and no way to recover short of a restart. Move
it inside the guard and cover it with a regression test.

Stop churning the synced model. `drain` replays schemas on every call
and returns a truthy dict even when nothing happened, so every poll
rewrote the timestamp and broadcast a patch to every browser twice a
second. Apply now tracks whether anything actually changed.

Report rows from Perspective rather than a lifetime counter. The
counter ignored both `limit` and `clear`, so a capped table showed 5
when it held 2, and a cleared one still showed 5 when it held 0.

Scope teardown to raydar's own Serve application. `exit()` called the
global `ray.serve.shutdown()` even for local and disabled dashboards,
which would take unrelated deployments on the cluster down with it.

Make the local dashboard's lifecycle safe: `stop` is idempotent and
releases the socket, an unstarted dashboard no longer leaks its bound
port, and a thread that outlives its join is no longer forgotten, which
previously let a second server bind the same socket.

Warn instead of failing silently in two places: a non-loopback bind now
says it serves task metadata unauthenticated, and an existing actor
whose dashboard mode differs from the requested one now says so rather
than presenting a permanently empty dashboard.

Also dedupe pending clears, which accumulated unboundedly between
drains, and assert real table contents in the tests that previously
only checked table names.

Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com>
Perspective 4 encodes rows as JSON, which has no datetime, so passing a
`datetime` raised `TypeError: Object of type datetime is not JSON
serializable`. Perspective 3 accepted them, so this broke the pattern
the README documents and any user code carrying it over. Coerce
`datetime` and `date` to epoch millis for columns declared `datetime` or
`date`, keeping the documented API working.

Epoch *seconds* were the more dangerous case: Perspective accepts a
float and reads it as millis, so `time.time()` silently rendered every
row as 1970. The demo and the metrics example in the wiki both did
this; they now pass datetimes. Coercion deliberately does not guess a
bare number's unit.

The existing tests only used integer and string columns, which is why
none of this showed up. Cover the datetime path.

Separately, the chrome was tinted: #0f172a against Perspective's dark
viewer, which is a neutral #242526, read as purple. Match Perspective's
own greys in both themes so the header and the grid look like one
application.

Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com>
test_construction_and_dataframe slept a flat 30 seconds and then asserted
on row 0, so it failed on CI with an empty frame when the GCS metadata
had not landed yet. The tracker code was untouched between the passing
and failing runs, so this was the sleep, not a regression.

Poll for the condition with a generous ceiling instead. The test now
finishes in about 3 seconds rather than always taking 30, and tolerates
a slow runner instead of failing on one. The suite drops from roughly 67
to 36 seconds.

Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com>
@timkpaine
timkpaine marked this pull request as ready for review August 21, 2026 21:07
@timkpaine
timkpaine merged commit 8803497 into main Aug 21, 2026
4 checks passed
@timkpaine
timkpaine deleted the tkp/spaday-transports branch August 21, 2026 21:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant