oban icon indicating copy to clipboard operation
oban copied to clipboard

Support an "extended", second-granularity cron syntax

Open gordonnoble opened this issue 2 years ago • 2 comments

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:

  1. Job uniqueness is already supported by Oban
  2. 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.
  3. 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.

gordonnoble avatar Sep 14 '22 17:09 gordonnoble

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}}

sorentwo avatar Sep 16 '22 18:09 sorentwo

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.

gordonnoble avatar Sep 16 '22 19:09 gordonnoble

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.

sorentwo avatar Sep 20 '22 12:09 sorentwo

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

gordonnoble avatar Sep 20 '22 13:09 gordonnoble

No idea when that would be available at this point, sorry.

sorentwo avatar Sep 20 '22 13:09 sorentwo

No problem! And thanks for your work on Oban, I'm a big fan

gordonnoble avatar Sep 20 '22 13:09 gordonnoble

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!

sorentwo avatar Nov 07 '23 13:11 sorentwo

No problem! It was easy enough to make our own Oban plugin that queues jobs every n seconds

gordonnoble avatar Nov 09 '23 21:11 gordonnoble