Skip to content

feat: add poll_now_notify to poll_loop and on_work_available callback#1893

Open
Jeadie wants to merge 4 commits into
apache:mainfrom
spiceai:upstream/poll-now-notify-on-work-available
Open

feat: add poll_now_notify to poll_loop and on_work_available callback#1893
Jeadie wants to merge 4 commits into
apache:mainfrom
spiceai:upstream/poll-now-notify-on-work-available

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?

Adds two related hooks for push-style executor scheduling:

  • An optional poll_now_notify: Option<Arc<Notify>> parameter on the executor poll_loop. When provided, the idle wait becomes a tokio::select! over the poll interval and the notify, so the scheduler can wake an idle executor immediately instead of waiting for the next poll tick. None preserves the existing timer-only behavior.
  • An OnWorkAvailableFn callback on SchedulerConfig (on_work_available), invoked by the query stage scheduler when new work becomes available (after JobSubmitted and when new stages become runnable).

Ported from spiceai#12

Jeadie added 2 commits June 23, 2026 13:58
Adds an optional poll_now_notify: Option<Arc<Notify>> parameter to the
executor poll_loop so the scheduler can wake an idle executor immediately
instead of waiting for the next poll interval. The idle wait becomes a
tokio::select! over the poll interval and the notify.

Adds an OnWorkAvailableFn callback to SchedulerConfig, invoked from the
query stage scheduler when new work becomes available (after JobSubmitted
and when new stages become runnable).

Ported from #12 for upstreaming.
@Jeadie Jeadie changed the title Upstream/poll now notify on work available feat: add poll_now_notify to poll_loop and on_work_available callback Jun 23, 2026
Comment thread ballista/scheduler/src/config.rs Outdated
/// - Tasks complete and new stages become runnable
///
/// This allows external systems to notify executors to poll immediately
/// rather than waiting for their next poll interval.

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.

Suggested change
/// rather than waiting for their next poll interval.
/// rather than waiting for their next poll interval.
///
/// # Warning
///
/// This callback is executed synchronously within the scheduler's main event loop.
/// Implementations **must be non-blocking** and should offload any blocking or
/// long-running operations (such as network I/O) to a separate task or thread.

The callback is synchronous and a blocking implementation may affect badly the Scheduler;w work.

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.

Comment added.

Comment thread ballista/executor/src/execution_loop.rs Outdated
match &poll_now_notify {
Some(notify) => {
tokio::select! {
() = tokio::time::sleep(Duration::from_millis(50)) => {}

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 duration here could be longer.
The idea is that notify will notify us as soon as there is some work, right ?
So, there is no need to poll so frequently if an external mechanism will notify us when to do it.

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.

Agreed, implemented to use a 1s fallback if a poll_now_notify is provided, otherwise unchanged at 50ms to preserve existing behavior.

Comment on lines +303 to +309
// Notify external systems when new stages become runnable
if !stage_events.is_empty()
&& let Some(ref callback) = self.config.on_work_available
{
callback("tasks_completed:new_stages_runnable");
}

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.

Shouldn't this be done after line 312 ? I.e. after posting the events.
Currently this will tell executors to poll but it seems to early.

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 stage_events are not necessarily "new stages are runnable". They might be JobRunningFailed or JobFinished, neither of which represents new runnable work.

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 correct, changed to remove the callback from TaskUpdating and moved into the JobUpdated arm, assuming update_job returns new_tasks > 0. So this only notifies when stages genuinely had new scheduleable tasks, and only after they are updated.

cors_allowed_origins: opt.cors_allowed_origins,
#[cfg(feature = "rest-api")]
cors_allowed_methods: opt.cors_allowed_methods,
on_work_available: None,

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.

How is this new setting supposed to be set ?
I'd expect at least a new with_on_work_available setter to be added if there is no new command line option.

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.

Added SchedulerConfig::with_on_work_available


// Notify external systems that new work is available
if let Some(ref callback) = self.config.on_work_available {
callback(&format!("job_submitted:{job_id}"));

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 event_sender.post_event(QueryStageSchedulerEvent::ReviveOffers) is non-blocking and the events might be still in the channel when this callback is executed. I.e. the first poll may see no events.

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.

Fixed. The callback now fires only after graph.revive() has run (in update_job, and submit_plan revives before caching), which is the exact point tasks become visible to poll_work. Now a woken executor's first poll always finds the work.

Comment thread ballista/scheduler/src/config.rs Outdated
///
/// This allows external systems to notify executors to poll immediately
/// rather than waiting for their next poll interval.
pub type OnWorkAvailableFn = Arc<dyn Fn(&str) + Send + Sync>;

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 function receives a plain &str as a command/message. IMO it would be better to use an Enum is all possible variants of commands.

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.

Is there a need to use an Arc ?
It could be a Box.

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.

Replaced the &str with a WorkAvailableReason enum (JobSubmitted { job_id } / NewStagesRunnable { job_id }), so consumers get structured data instead of parsing strings.

It also needs to stay Arc because SchedulerConfig derives Clone and Box<dyn Fn> isn't cloneable. We could drop the Clone requirement if we want to keep it as Box.

- Replace the callback's `&str` argument with a `WorkAvailableReason` enum
- Fire the stage-resolution callback from the JobUpdated arm after
  `update_job` has revived the graph, so it only fires for genuinely new
  runnable work and never before the work is pollable
- Add `SchedulerConfig::with_on_work_available`
- Document that the callback runs synchronously in the scheduler event loop
  and must not block
- Idle-poll less aggressively (1s fallback) when a poll_now notifier is wired
@phillipleblanc

Copy link
Copy Markdown
Contributor

@martin-g Thanks for the thorough review, this should be ready now for another look.

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