oban
oban copied to clipboard
Support an "extended", second-granularity cron syntax
Is your feature request related to a problem? Please describe.
We have many use cases for scheduling jobs every few seconds (usually ~5s). Additionally, only one instance of each job should ever be running at a time. Our current solution is a mix of the quantum library (which supports a second-granularity cron syntax) + our own locking mechanism to prevent multiple nodes in our cluster from running this job at once.
Oban would be a great fit here, and a big improvement over our current approach:
- Job uniqueness is already supported by Oban
- Only a single node (the Oban leader) will schedule these jobs. Today, every node attempts to get a lock and run these jobs, resulting in a lot of wasted effort.
- We'd gain all the observability and monitoring that we get from Oban (i.e. telemetry, web UI). Just generally, we'd rather have a single tool to do the job of scheduling work (we already use Oban for jobs that run less frequently, as well as many other things).
Describe the Solution You'd Like
From a user perspective, the easiest solution would be supporting second-granularity syntax like:
*/5 * * * *
=> every five seconds
Describe Alternatives You've Considered
We could solve this today with existing Oban tools:
# A worker that would be scheduled to run every minute via the Oban cron plugin
defmodule Scheduler do
@moduledoc """
This exists to kick off the recursive job when the app starts
and to re-insert it in case the recursive job fails to re-enqueue itself (e.g. bug or exception).
"""
def perform(%Oban.Job{}) do
Oban.insert(
SomeWorker.new(
%{},
unique: [fields: [:worker]]
)
)
end
end
defmodule SomeWorker do
@interval 5
def perform(%Oban.Job{}) do
# ... do work
Oban.insert(
SomeWorker.new(
%{},
unique: [fields: [:worker]],
schedule_in: @interval
)
)
end
end
This mostly solves the problem, but there's still the chance for up to a minute of gap between jobs if the recursive job dies unexpectedly. Plus, especially as you add more jobs like this, the scheduler grows in complexity.
Additional Context
Open to suggestions how others have solved this problem with Oban. We use Oban pro, so should have the full feature set available to us.
We have loose plans to add an extended syntax to Pro's DynamicCron plugin. With a little work, you can fake it currently:
defmodule SpacedWorker do
use Oban.Worker
def perform(%Job{args: %{"seconds" => seconds}) do
0..55//seconds
|> Enum.map(&new(%{}, schedule_in: &1))
|> Oban.insert_all()
end
def perform(job) do
# do real stuff here
end
end
{"* * * * *", SpacedWorker, args: %{seconds: 5}}
Interesting, so I assume you'd allow for multiple "scheduled" jobs to exist, but only a single "executing" job. Is that right?
If so, would it be an issue if the job - for whatever reason - started to take longer than 5 seconds? I.e. would this cause a build up of jobs you'd never work through? I suppose you really oughta adjust the interval anyway in that cause.
If so, would it be an issue if the job - for whatever reason - started to take longer than 5 seconds?
If you're enqueuing jobs every 5 seconds, and those jobs each take more than 5 seconds, then yes, it would be a problem. Your queue would keep growing indefinitely. You can add a timeout and set max_attempts: 1
to combat accumulation.
Makes sense, thank you! I'll go with that approach for now, though I'm curious if you have an idea of when direct support for this will be available
No idea when that would be available at this point, sorry.
No problem! And thanks for your work on Oban, I'm a big fan
After extensive thought (over a year's worth), a lack of additional requests, caveats about down-to-the-second timing, and the simple workaround outlined earlier in this issue, we've decided not to pursue this feature.
Thanks for the suggestion and discussion!
No problem! It was easy enough to make our own Oban plugin that queues jobs every n seconds