feat: event log and history server (Spark History Server equivalent) - #2259
Closed
andygrove wants to merge 5 commits into
Closed
feat: event log and history server (Spark History Server equivalent)#2259andygrove wants to merge 5 commits into
andygrove wants to merge 5 commits into
Conversation
…or out dto_build Move the scheduler's REST response types into a new leaf crate, `ballista-history`, and pull the graph-to-DTO construction out of the axum handlers into a pure `api::dto_build` module. Behavior preserving: the same DTOs are produced from the same state, so live REST responses are byte-identical. The existing handler tests cover this, and the helper unit tests move alongside the functions they test. This is the first step toward a history server that replays completed jobs and serves the same `/api/*` responses without a live scheduler. Splitting the DTOs into a serde-only crate lets that server build the identical wire types without depending on the scheduler's live execution graph, and moving construction out of the handlers means it can run against state that did not come from a handler request. `JobResponse::job_id` becomes a `String` rather than `ballista_core::JobId` so the new crate stays serde-only. `JobId` is `#[serde(transparent)]` over `String`, so the JSON is unchanged.
Follow-up cleanups on the extraction: - Collapse the three near-identical ExecutionStage arms in graph_to_query_stages into one destructuring match, dropping the mutable placeholder-zero summary. - Take PlanFormat by value instead of &JobQueryParams, and move PlanFormat into ballista-history. It is part of the wire contract, and the pure builder no longer imports an axum query-param type back out of the handler module. - Inject `now` into graph_to_query_stages rather than reading the clock, so replaying a stored log renders stable elapsed times. - Share percent_complete and min_start_time; use displayable() instead of the longhand DisplayableExecutionPlan::new(). - Drop the dead JobConfig alias and the unused serde_json dev-dependency, make task_status_to_dto private, and remove a duplicated test. - Enable #![warn(missing_docs)] on ballista-history and document the types, matching the other Ballista crates. - Register ballista-history with the release tooling: version bump script, publish order, and crate dependency graph.
The previous wording left it ambiguous whether the history server re-derives responses from stored execution state or replays stored DTOs. It replays them: the scheduler builds each response once against the live graph and writes it to the event log, so byte-identical output is a structural property rather than two implementations agreeing. Also records the consequence, that anything not captured at write time cannot be recovered at replay time.
The crate holds the /api/* wire types, and it has three parties, not one: the scheduler serves them, the web TUI deserializes them, and a future history server will serve replayed copies. Naming it after the history server made it awkward for the TUI, which parses live scheduler responses and today keeps its own duplicate declarations. Renaming it after the contract it defines removes that friction. The event-log schema, writer, and reader can then land as a separate ballista-history crate that depends on this one.
Adds a Spark-History-Server equivalent: the scheduler can record a durable per-job event log during execution, and a standalone history server replays those logs and serves the same /api/* responses the live scheduler does, so the existing TUI browses completed jobs with no scheduler running. New ballista-history crate: - A versioned JSONL event schema. JobStart / StageStart / StageEnd / TaskEnd form an incremental timeline; the terminal JobEnd embeds the finished REST responses. - An async buffered EventLogWriter. Timeline events are dropped rather than allowed to block if the queue backs up, so logging cannot stall scheduling. JobEnd is the exception and waits for capacity, since a job missing it is invisible to the history server. - A reader that folds a completed log back into the served payload. Scheduler: - New --event-log-dir flag, off by default. When unset there is no channel, no task, no file, and no per-event work beyond one Option check. - An event-log tee at the top of QueryStageScheduler::on_receive maps JobSubmitted, TaskUpdating, JobFinished, JobRunningFailed and JobCancel onto history events. Cancellation is recorded there because the handler below it drops the graph. - JobEnd renders its stage snapshot as of completed_at rather than the wall clock, so replay is deterministic. History server: - ballista-history-server --event-log-dir <dir> loads completed logs and serves /api/* from the stored responses. Corrupt or partial logs are skipped rather than failing startup. Because JobEnd stores responses the scheduler already built, replay never re-derives them and there is no second implementation to drift. A test asserts the history server serves byte-identical JSON to the live scheduler for the same graph. get_job_config now returns the sorted JobConfig map so live and replayed output agree on key order. Scope for this first cut: local filesystem storage, completed jobs only. The TaskEnd timeline is captured but not yet surfaced in a UI.
Member
Author
|
Closing this: it bundles slices 2-4 of the split and so just duplicates #1925, which already has the whole feature. Reopening the series as the intended small pieces instead, starting with the event-log crate on its own. The forward-porting done here (the |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Which issue does this PR close?
Closes #1923.
This supersedes #1925, which was the same feature as a single unreviewable change against a base that is now 189 commits stale.
Rationale for this change
Ballista's TUI shows jobs, stages, tasks and metrics by reading the scheduler's REST API, but that state is ephemeral. Completed jobs are cleaned up after
finished_job_state_clean_up_interval_seconds, and everything is gone when the scheduler restarts. There is no way to look at a job after the fact, which is exactly when you usually want to.user-personas.mdlists "a history server / UI" among the things Persona 2 depends on, so this is filling in a guarantee already written down rather than adding a new one.What changes are included in this PR?
New
ballista-historycrate. Depends onballista-api-typesplus serde, tokio and log.JobStart,StageStart,StageEndandTaskEndform an incremental timeline; the terminalJobEndembeds the finished REST responses.EventLogWriter. All file I/O happens on a background task, so the scheduler's event loop never waits on disk. Timeline events are dropped rather than allowed to block if the queue backs up, on the grounds that losing a progress record beats stalling scheduling.JobEndis the exception and waits for capacity, because a job missing it is invisible to the history server.Scheduler. A new
--event-log-dirflag, off by default. When it is unset there is no channel, no task, no file, and no per-event work beyond oneOptioncheck.When it is set, a tee at the top of
QueryStageScheduler::on_receivemapsJobSubmitted,TaskUpdating,JobFinished,JobRunningFailedandJobCancelonto history events.JobCancelis handled there specifically because the handler below it drops the graph, so that is the last point at which a cancelled job can be recorded.JobPlanningFailedis deliberately absent: it is posted instead ofJobSubmitted, so the job has neither a graph nor an open log.New
ballista-history-serverbinary.ballista-history-server --event-log-dir <dir>loads completed logs at startup and serves/api/*from the stored responses. Corrupt or partial logs are skipped rather than failing startup, so one bad file cannot hide every other job.Docs. A new History Server user-guide page covering both flags, the endpoints, and the operational caveats below.
Why replayed output can be trusted
The history server never re-derives a response.
JobEndstores theJobResponseandQueryStagesResponsethe scheduler already built from its live execution graph, and replay deserializes and re-serializes them unchanged. Byte-identity is a structural property, not two implementations agreeing.history_store_serves_byte_identical_json_to_live_schedulerpins this: it builds the live DTOs, emits a realJobEndthrough the real async writer, loads it back through the history server's ownHistoryStore::load, and asserts the serialized JSON matches.Two things fell out of getting that test deterministic, and both are improvements in their own right:
JobEndrenders its stage snapshot as ofcompleted_atrather than the wall clock, so the stored record is a snapshot of the job at the moment it ended.get_job_confignow returns the sortedJobConfigmap. It previously servedSessionConfig::to_props()directly, which is aHashMap, so live output had non-deterministic key order and could never have matched a replay.Are there any user-facing changes?
Yes, all additive and opt-in:
--event-log-dir <dir>, default disabled.ballista-history-serverbinary.--host/--portto browse completed jobs with no live scheduler.GET /api/job/{job_id}/confignow returns keys in sorted order. Same content, deterministic ordering.No breaking API changes.
Verified locally:
cargo test -p ballista-scheduler --libpasses (338 tests, 13 of them new),cargo test -p ballista-historypasses, clippy is clean for all three crates with--all-features -D warnings, fmt/taplo/prettier are clean, and the--no-default-featurescheck still passes.I also smoke-tested the binary against a hand-written event log: it loads the job and serves
/api/jobs,/api/job/{id},/api/job/{id}/stagesand/api/job/{id}/configcorrectly, and 404s on an unknown job.Known limitations
Called out in the docs rather than left to be discovered:
?plan_format=has no effect against a history server.TaskEndtimeline is captured so a future UI can show a job progressing, but nothing reads it yet.Note on size
This is ~1700 lines, which is large for one review. It was originally scoped as three PRs (event crate, scheduler wiring, history server) and can still be split that way if reviewers would prefer. The three parts are cleanly separable: the crate has no scheduler dependencies, and the wiring and the server touch disjoint files.