What's the simplest way to get the next scheduled execution time for a crontab job?

I have a crontab entry scheduling PocketTax.Finexer.TransactionsSyncProducer and want to show users the next time it will be run

Application.get_env(:pocket_tax, Oban)
[
  repo: PocketTax.Repo,
  engine: Oban.Pro.Engines.Smart,
  queues: [
    default: 10,
    transactions_sync_producer: [limit: 1, global_limit: 1],
    transactions_sync: [
      limit: 20,
      global_limit: [allowed: 1, partition: [meta: :workflow_name]]
    ]
  ],
  plugins: [
    {Oban.Plugins.Cron,
     [
       crontab: [
         {"0 8-20/4 * * *", PocketTax.Finexer.TransactionsSyncProducer,
          [queue: :transactions_sync_producer]}
       ]
     ]},
    Oban.Pro.Plugins.DynamicLifeline
  ]
]

Right now the code is brittle.
First, I need to get the crontab plugin config with

{:crontab, entries} =  
  :pocket_tax
  |> Application.get_env(Oban)
  |> Keyword.get(:plugins)
  |> Enum.find(& elem(&1, 0) == Oban.Plugins.Cron) 
  |> elem(1) 
  |> List.first()

Then I need to find crontab entry for the module of interest and extract its schedule

sync_schedule = 
  entries
  |> Enum.find(, & elem(&1, 1) == PocketTax.Finexer.TransactionsSyncProducer)
  |> elem(0)

Now I can use Oban.Cron.Expression to get the next time it will run

sync_schedule
|> Oban.Cron.Expression.parse!() 
|> Oban.Cron.Expression.next_at(DateTime.utc_now())

Is there a more convenient way to do it?

You can get the static cron schedule more reliably with Oban.config()

conf = Oban.config()

conf.plugins
|> get_in([Oban.Plugins.Cron, :crontab])
|> Enum.find(fn {expr, worker, _} -> worker == PocketTax.Finexer.TransactionsSyncProducer end)
|> elem(0)
|> Oban.Cron.Expression.parse!()
|> Oban.Cron.Expression.next_at(DateTime.utc_now())

Alternatively, expose the schedule on the worker itself and reference that in the crontab:

defmodule PocketTax.Finexer.TransactionsSyncProducer do
  use Oban.Pro.Worker

  def schedule, do: "0 8-20/4 * * *"
end

Then reference it as TransactionsSyncProducer.schedule().

2 Likes

That would be the neatest, but then I need to move the configuration to runtime.exs, which I’m not keen at this point.

Honestly I’ve moved basically ALL config that doesn’t absolutely require compile time info to runtime.exs. It’s so much more flexible.

2 Likes

Have you found a good way to split it?

There isn’t any compile time config in the standard Oban options. It can easily be cut and pasted into runtime.exs. You’ll still get the runtime validation of the config when the app boots, and it won’t cause a full recompilation when you change Oban configuration.

For Oban, everything can go in runtime.exs. Otherwise, put everything that isn’t needed for compile time config in runtime.exs.