Skip to content

Dynamic scheduled tasks#553

Open
cupatea wants to merge 27 commits intorails:mainfrom
cupatea:feature/dynamic_recurring_tasks
Open

Dynamic scheduled tasks#553
cupatea wants to merge 27 commits intorails:mainfrom
cupatea:feature/dynamic_recurring_tasks

Conversation

@cupatea
Copy link

@cupatea cupatea commented Apr 23, 2025

Fixes #186

Add resque-scheduler style dynamic schedules feature, allowing you to add or remove recurring tasks at runtime without touching your static config file.

What’s new:

  • Dynamic scope (static: false) on SolidQueue::RecurringTask model.
  • Renamed methods/vars in SolidQueue::Scheduler::RecurringSchedule to distinguish static vs. dynamic schedules.
  • On boot, @configured_tasks now includes static and dynamic tasks.
  • Added SolidQueue::Scheduler::RecurringSchedule.update_scheduled_tasks:
    • Schedules any new dynamic tasks added since the last run.
    • Unschedule any dynamic tasks removed from the database.
    • Refreshes @configured_tasks to match the current DB state.
  • SolidQueue::Configuration no longer requires a non-blank static config file - pure dynamic scheduling is now supported.
  • SolidQueue::Scheduler watches for changes after launch and updates its metadata so the running process always reflects the true set of recurring tasks.

Tests verify that adding or dropping dynamic tasks at runtime correctly updates what’s scheduled.

@cupatea cupatea force-pushed the feature/dynamic_recurring_tasks branch 3 times, most recently from ebb8716 to ebd9629 Compare April 24, 2025 08:54
@cupatea
Copy link
Author

cupatea commented Jun 12, 2025

Hi @rosa 👋, could you please take a look at this PR when you have a moment? Thanks so much!

@rosa
Copy link
Member

rosa commented Jun 17, 2025

Hey @cupatea, thanks for this! It's a good start, but it needs a few changes. The main ones are:

  • We need to make the sleep interval for the scheduler configurable, as a sort of polling interval, to reload dynamic tasks.
  • The way to use this needs to be a bit friendlier, without users having to go and figure out the details to create a SolidQueue::RecurringTask record. Something like SolidQueue.schedule_recurring_task could work, and it needs to be documented.
  • We'd need the ability to delete dynamically scheduled tasks as well, by task ID. If the task is static, this could simply raise an error.

And then some other more specific changes that I'll note in the code.


def invalid_tasks
recurring_tasks.select(&:invalid?)
static_recurring_tasks.select(&:invalid?)
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't need to change names. It's clear the tasks here are static since this comes from the recurring.yml configuration. We don't need to rename anything here.

recurring_schedule.update_scheduled_tasks.tap do |updated_tasks|
if updated_tasks.any?
process.update_columns(metadata: metadata.compact)
end
Copy link
Member

Choose a reason for hiding this comment

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

This code is mixing actions at very different levels, making it aware of details it shouldn't need to know, like how to update the metadata for its registered process record, or whether the recurring schedule changed. It should change, perhaps to something like

recurring_schedule.reload!
if recurring_schedule.changed?
  refresh_registered_process
end

And refresh_registered_process would go in SolidQueue::Processes::Registrable.

Choose a reason for hiding this comment

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

Hi @rosa do you think this Dynamic scheduled tasks will merge by the end of this year 2026? thanks

Copy link
Member

Choose a reason for hiding this comment

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

@lrjbrual oh yes, for sure.

@cupatea cupatea force-pushed the feature/dynamic_recurring_tasks branch from ad943cc to 6a883a7 Compare June 19, 2025 15:03
@cupatea
Copy link
Author

cupatea commented Jun 19, 2025

Hi @rosa, thank you for the feedback! I think it's ready for the second round of review

@cupatea cupatea requested a review from rosa June 19, 2025 15:11
@cupatea cupatea force-pushed the feature/dynamic_recurring_tasks branch from 6a883a7 to c754746 Compare July 12, 2025 09:28
@cupatea cupatea force-pushed the feature/dynamic_recurring_tasks branch from c754746 to 214a7f6 Compare August 14, 2025 17:06
@bruno-berchielli
Copy link

bruno-berchielli commented Aug 21, 2025

@cupatea , in the docs files you mention SolidQueue.delete_recurring_task but I think it should be SolidQueue.destroy_recurring_task, also, you might consider using the key instead of id, because that's is what probably gonna be easily available on the app level, no?
SolidQueue::RecurringTask.find_by(key: solid_queue_key)&.destroy!

@cupatea
Copy link
Author

cupatea commented Aug 21, 2025

@cupatea , in the docs files you mention SolidQueue.delete_recurring_task but I think it should be SolidQueue.destroy_recurring_task, also, you might consider using the key instead of id, because that's is what probably gonna be easily available on the app level, no? SolidQueue::RecurringTask.find_by(key: solid_queue_key)&.destroy!

Thanks for noticing, on a way to fix that!
And yes, using a key instead of ID makes more sense to me as well. To prevent deleting a static task, this method will raise a record not found error in case of specifying a static task key (or if you try deleting a task that does not exist)

@cupatea cupatea force-pushed the feature/dynamic_recurring_tasks branch from 017c34e to dd72f10 Compare November 24, 2025 09:27
@cupatea cupatea force-pushed the feature/dynamic_recurring_tasks branch from dd72f10 to 62bfa37 Compare February 12, 2026 22:33
cupatea and others added 13 commits February 24, 2026 21:58
 Two assertions were using assert value, message instead of assert_equal/assert_empty, meaning they always passed regardless of the actual metadata content. Fixed to use assert_empty. Also fixed a typo ("unschedule" -> "unscheduled").
DB queries in reload! (dynamic_tasks.where.not(...), RecurringTask.pluck(:key)) were not wrapped in the app executor, which could cause connection management issues. Wrapped in wrap_in_app_executor.
 empty? checked stale configured_tasks (lib/solid_queue/scheduler/recurring_schedule.rb) -- configured_tasks was set once in initialize and never updated with dynamic tasks. This meant empty? could return true even when dynamic tasks existed, causing the scheduler to exit prematurely in inline mode. Changed empty? to check scheduled_tasks.empty? && dynamic_tasks.none?.
 Changes not cleared after consumption (lib/solid_queue/scheduler.rb, lib/solid_queue/scheduler/recurring_schedule.rb) -- Added clear_changes method and call it in the scheduler's run loop after refresh_registered_process, preventing stale change state from persisting.
cupatea and others added 12 commits February 24, 2026 21:58
Remove extra blank lines, column-aligned hashes, and inline comments
on test assertions. Revert unrelated Gemfile.lock platform addition.
Filter :static from schedule_task options since dynamic tasks are
always non-static by definition.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Dynamic tasks require explicit `dynamic_tasks: true` in the scheduler
config. Without it, the scheduler behaves as before — no extra DB
queries, no polling for dynamic changes. Default polling interval
changed from 1s to 5s.

Rename schedule_task/unschedule_task to schedule_recurring_task/
unschedule_recurring_task so the API clearly communicates these are
recurring tasks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tasks_enabled

Have from_configuration read static from options instead of
hardcoding it. Callers that create static tasks (YAML config) pass
static: true via reverse_merge. schedule_recurring_task passes
static: false directly.

Rename the config key from dynamic_tasks to dynamic_tasks_enabled
for clarity.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Keep the longer sleep interval when dynamic tasks are disabled.

Add tests for `dynamic_tasks_enabled` opt-in in configuration and for
verifying dynamic tasks are ignored when not enabled.
We don't need to track dynamic task changes, we can just reload the
metadata in every case. If it hasn't changed, Rails won't issue any new
update to the process record.

Also, we can just use `dynamic_tasks` everywhere, as an empty AR
relation if dynamic tasks are disabled, avoiding extra queries but
keeping the code simpler.
@rosa rosa force-pushed the feature/dynamic_recurring_tasks branch from 62bfa37 to b4f7c99 Compare February 25, 2026 15:13
@rosa
Copy link
Member

rosa commented Feb 25, 2026

Hey @cupatea, sorry for the delay here. I'm finally working towards Solid Queue 2.0 with the plan of including this. I've pushed a bunch of changes. Some are stylistic and simplifications, but I've also changed the default behaviour. I think this feature should be opt-in, disabled by default, as people might not need dynamic tasks at all and could save the extra queries to the DB.

We need the dynamic task keys there and were doing a new query every
time. We only need to do a query when explicitly reloading them.
@cupatea
Copy link
Author

cupatea commented Feb 25, 2026

Hey @cupatea, sorry for the delay here. I'm finally working towards Solid Queue 2.0 with the plan of including this. I've pushed a bunch of changes. Some are stylistic and simplifications, but I've also changed the default behaviour. I think this feature should be opt-in, disabled by default, as people might not need dynamic tasks at all and could save the extra queries to the DB.

Thank you!

I noticed something in app/models/solid_queue/recurring_task.rb:40.
It looks like this should use **options.merge(static: false) instead of reverse_merge, since we want to ensure the static option is always overridden

@rosa
Copy link
Member

rosa commented Feb 25, 2026

I noticed something in app/models/solid_queue/recurring_task.rb:40.
It looks like this should use **options.merge(static: false) instead of reverse_merge, since we want to ensure the > static option is always overridden

Ohh, totally! My bad. I had it backwards before, switched it around, but forgot to update the method. Thanks!

We want to set that value overriding whatever we get in options, so it
should be merged into options, not the other way around.

Co-Authored-By: Vladyslav Davydenko <vladyslav@hey.com>
@rosa
Copy link
Member

rosa commented Feb 25, 2026

This should be ready to go but I'd like to test it a bit better before merging. Have you been using this already in production by any chance, @cupatea @lrjbrual?

@cupatea
Copy link
Author

cupatea commented Feb 25, 2026

This should be ready to go but I'd like to test it a bit better before merging. Have you been using this already in production by any chance, @cupatea @lrjbrual?

Not really - I migrated from Resque to this and deployed to the test server, but I haven’t deployed it to production yet.
Our production is pretty light in terms of jobs number, so it's more for getting rid of Redis dependency

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.

Dynamic scheduled tasks

4 participants