Skip to content

feat: add a broadcast channel for job state event notifications#1891

Open
Jeadie wants to merge 5 commits into
apache:mainfrom
spiceai:upstream/job-state-broadcast-channel
Open

feat: add a broadcast channel for job state event notifications#1891
Jeadie wants to merge 5 commits into
apache:mainfrom
spiceai:upstream/job-state-broadcast-channel

Conversation

@Jeadie

@Jeadie Jeadie commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

None Closes #. Suggested by @milenkovicm that we should upstream our changes (see comment).

Rationale for this change

Useful in spiceai/datafusion-ballista for production features.

What changes are included in this PR?

Summary

Adds a tokio::sync::broadcast sender to SchedulerServer, subscribable via subscribe_job_updates(). On job state changes (queued / running / completed / failed / cancelled) the scheduler broadcasts a JobStateEvent carrying the job id and new state. Lets components holding a handle to the scheduler observe job lifecycle transitions without polling.

Relationship to the existing JobStatusSubscriber

Similar to the existing JobStatusSubscriber (mpsc) mechanism wired through submit_job(..., subscriber). This broadcast channel is a separate, fan-out style notification path; worth deciding the best DX between these.

JobStatusSubscriber (existing) JobStateEvent broadcast (this PR)
Channel mpsc - single consumer broadcast - fan-out
Scope 1 subscriber per submitted job, request-scoped Global; fires for all jobs
Consumer submitting gRPC client, via ExecuteQuery / GetJobStatusResult. in-process component(s), via subscribe_job_updates()
Payload Full protobuf JobStatus Lightweight JobStateEvent { job_id: String, state }

Both fire from the same lifecycle points in query_stage_scheduler.rs.

Ported from spiceai#15.

Jeadie added 2 commits June 23, 2026 13:47
Adds a tokio::sync::broadcast Sender to SchedulerServer, subscribable via
subscribe_job_updates(). On job state changes (queued/running/completed/
failed/cancelled) the scheduler broadcasts a JobStateEvent carrying the job
id and new state.

Ported from #15 for upstreaming.

@andygrove andygrove left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for upstreaming this. It's cleanly structured, the doc comments are thorough, and I like that it's purely additive with no change to existing behavior. Ignoring send errors when there are no receivers is the right call for a broadcast channel.

On the design question you raised yourself about this versus the existing JobStatusSubscriber: agreed it's worth settling. It might help to document when a consumer should reach for each one, since they overlap in purpose but differ in scope and payload. If the broadcast path can eventually cover the in-process cases the mpsc path serves, consolidating later would be nice, but that doesn't need to block this.

Would it also be worth a test that subscribes via subscribe_job_updates() and drives a job through its lifecycle, asserting the expected events arrive? The unit tests cover the event types well, and a test on the actual broadcast wiring would guard the lifecycle hookups in query_stage_scheduler.rs against future refactors.

One minor note: JobState mirrors the protobuf job_status::Status. A From conversion between them, or a comment tying them together, could help keep the two from drifting over time.

This review was prepared with the help of an LLM.

///
/// This determines how many job state events can be buffered before
/// slow receivers start lagging behind.
const JOB_STATE_CHANNEL_CAPACITY: usize = 256;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 256 capacity is a sensible default. For a scheduler with high job throughput and a slow or briefly stalled subscriber, the buffer could fill and subscribers would start seeing Lagged. Would it be worth pulling this into SchedulerConfig so operators with heavier job rates can size it, keeping 256 as the default? Different deployments run very different workloads, so a fixed buffer may not fit everyone.

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.

This is now configurable.

Comment thread ballista/scheduler/src/scheduler_server/query_stage_scheduler.rs Outdated
Jeadie and others added 3 commits July 3, 2026 11:42
…nnel

- Add job_state_channel_capacity to SchedulerConfig (default 256) so
  operators with high job throughput can tune the buffer; removes the
  hardcoded const from SchedulerServer
- Move all job state broadcasts to after their state transitions succeed,
  so subscribers never see an event for a commit that failed; aligns
  Queued/Failed/Completed/Cancelled with how Running already behaved
- Add From<job_status::Status> for JobState to keep the two enums in
  sync; document that Cancelled has no protobuf counterpart
- Expand module doc with guidance on when to use JobStateEvent vs
  JobStatusSubscriber, and reference job_state_channel_capacity for tuning
- Add integration tests covering the broadcast wiring: lifecycle order
  (Queued -> Running -> Completed) and cancel path
@phillipleblanc

Copy link
Copy Markdown
Contributor

@andygrove this is ready for another review, thanks!

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.

3 participants