Oban Pro DynamicCron using incorrect config with multiple nodes

In my job, I have 2 nodes running that uses Oban but should have different configs for them (queues, cron, etc).

For each node, I have a different DynamicCron config:

Node 1:

Application.get_env(:core, Oban)
[
  name: :pacman_oban,
  ...
  plugins: [
    ...
    {Oban.Pro.Plugins.DynamicCron,
     [
       name: :pacman_oban,
       sync_mode: :automatic,
       crontab: [
         {"@daily", Core.Workers.RemoveOldTriedSkipTraces, [name: "remove-old-tried-skip-traces"]},
         {"@daily", Core.Workers.ProcessData, [name: "process-data"]}
       ]
     ]}
  ]
}

Node 2:

Application.get_env(:core, Oban)
[
  name: :marketplace_oban,
  ...
  plugins: [
    ...
    {Oban.Pro.Plugins.DynamicCron,
     [
       name: :marketplace_oban,
       sync_mode: :automatic,
       crontab: [
         {"@hourly", Core.Workers.ProcessPropertyViewCountWorker, [name: "process-property-view-count", args: %{interval: "1h"}]},
         {"@daily", Core.Workers.ExpireShortUrl, [name: "expire-short-url"]},
         {"@hourly", Core.Workers.StartFetchEmails, [name: "start-fetch-emails"]},
         {"@hourly", Core.Workers.StartDealEmails, [name: "start-deal-emails"]},
         {"0 23 * * *", Core.Workers.SendPushNotification, [name: "upcoming-appointment-notification", args: %{type: "upcoming_appointment"}]},
         {"0 */2 * * *", Core.Workers.SendPushNotification, [name: "pending-appointment-notification", args: %{type: "pending_appointment"}]},
         {"*/10 * * * *", Core.Workers.SendPushNotification, [name: "start-appointment-reminder-notification", args: %{type: "start_appointment_reminder"}]}
       ]
     ]}
  ]
}

As you can see, each node has a specific oban name (:pacman_oban and :marketplace_oban) and a specific DynamicCron config as-well.

The issue I’m seeing, is that, when I run these nodes and then run Oban.Pro.Plugins.DynamicCron.all(:pacman_oban) for Node 1 and Oban.Pro.Plugins.DynamicCron.all(:marketplace_oban) for Node 2, both shows the config of one of them two nodes randomly (sometimes it shows the node 1 cron jobs, sometimes the node 2 cron jobs, I think it depends on which node starts first).

For example, right now, the node loaded Node 1 config on both nodes:

Node 1:

Oban.Pro.Plugins.DynamicCron.all(:pacman_oban)
[
  %Oban.Pro.Cron{
    __meta__: #Ecto.Schema.Metadata<:loaded, "public", "oban_crons">,
    name: "remove-old-tried-skip-traces",
    expression: "@daily",
    worker: "Core.Workers.RemoveOldTriedSkipTraces",
    opts: %{},
    paused: false,
    insertions: [],
    lock_version: 2,
    parsed: nil,
    inserted_at: ~U[2025-06-17 13:19:00.699969Z],
    updated_at: ~U[2025-06-17 13:19:00.699969Z]
  },
  %Oban.Pro.Cron{
    __meta__: #Ecto.Schema.Metadata<:loaded, "public", "oban_crons">,
    name: "process-data",
    expression: "@daily",
    worker: "Core.Workers.ProcessData",
    opts: %{},
    paused: false,
    insertions: [],
    lock_version: 2,
    parsed: nil,
    inserted_at: ~U[2025-06-17 13:19:00.703162Z],
    updated_at: ~U[2025-06-17 13:19:00.703162Z]
  }
]

Node 2:

Oban.Pro.Plugins.DynamicCron.all(:marketplace_oban)
[
  %Oban.Pro.Cron{
    __meta__: #Ecto.Schema.Metadata<:loaded, "public", "oban_crons">,
    name: "remove-old-tried-skip-traces",
    expression: "@daily",
    worker: "Core.Workers.RemoveOldTriedSkipTraces",
    opts: %{},
    paused: false,
    insertions: [],
    lock_version: 2,
    parsed: nil,
    inserted_at: ~U[2025-06-17 13:19:00.699969Z],
    updated_at: ~U[2025-06-17 13:19:00.699969Z]
  },
  %Oban.Pro.Cron{
    __meta__: #Ecto.Schema.Metadata<:loaded, "public", "oban_crons">,
    name: "process-data",
    expression: "@daily",
    worker: "Core.Workers.ProcessData",
    opts: %{},
    paused: false,
    insertions: [],
    lock_version: 2,
    parsed: nil,
    inserted_at: ~U[2025-06-17 13:19:00.703162Z],
    updated_at: ~U[2025-06-17 13:19:00.703162Z]
  }
]

Initially, I thought the issue was that I was not setting the oban name field in the DynamicCron config, but, as you can see above, I now have they set correctly and it still loads the cron jobs incorrectly.

Am I doing something wrong here or is this a bug?

It’s not a bug. It’s a slight config issue.

DynamicCron works by storing the jobs definitions in a db table. If you have two different configs that are pointed at the same table, they’re going to read all the jobs from the same table. It’s kinda the dynamic part.

There are two straight forward solutions:

  1. Use the regular Cron, instead of the DynamicCron

  2. Use a separate prefix for each config. Instance and Database Isolation — Oban v2.19.4

So, I’m trying the second approach by adding a prefix to one of the nodes

      config :core, Oban,
        name: :pacman_oban,
        repo: Core.Repo,
        prefix: "pacman",
        ...

I also added this migration:

defmodule Core.Repo.Migrations.AddObanPrefixes do
  use Ecto.Migration

  def up do
    Oban.Migrations.up(prefix: "pacman")
    Oban.Pro.Migration.up(prefix: "pacman")
  end

  def down do
    Oban.Pro.Migration.down(prefix: "pacman")
    Oban.Migrations.down(prefix: "pacman")
  end
end

But, when I run the migration, I get this error:

15:50:59.908 [info] execute "CREATE OR REPLACE FUNCTION \"pacman\".oban_state_to_bit(state \"pacman\".oban_job_state)\nRETURNS jsonb AS $$\nSELECT CASE\n       WHEN state = 'scheduled' THEN '0'::jsonb\n       WHEN state = 'available' THEN '1'::jsonb\n       WHEN state = 'executing' THEN '2'::jsonb\n       WHEN state = 'retryable' THEN '3'::jsonb\n       WHEN state = 'completed' THEN '4'::jsonb\n       WHEN state = 'cancelled' THEN '5'::jsonb\n       WHEN state = 'discarded' THEN '6'::jsonb\n       END;\n$$ LANGUAGE SQL IMMUTABLE STRICT\n"
** (Postgrex.Error) ERROR 55P04 (unsafe_new_enum_value_usage) unsafe use of new value "cancelled" of enum type pacman.oban_job_state

    hint: New enum values must be committed before they can be used.
    (ecto_sql 3.12.1) lib/ecto/adapters/sql.ex:1096: Ecto.Adapters.SQL.raise_sql_call_error/1
    (elixir 1.18.1) lib/enum.ex:1714: Enum."-map/2-lists^map/1-1-"/2
    (ecto_sql 3.12.1) lib/ecto/adapters/sql.ex:1203: Ecto.Adapters.SQL.execute_ddl/4
    (ecto_sql 3.12.1) lib/ecto/migration/runner.ex:348: Ecto.Migration.Runner.log_and_execute_ddl/3
    (elixir 1.18.1) lib/enum.ex:1714: Enum."-map/2-lists^map/1-1-"/2
    (oban_pro 1.6.0-rc.3) lib/oban/pro/migration.ex:275: anonymous fn/4 in Oban.Pro.Migration.inner_change/3
    (elixir 1.18.1) lib/enum.ex:4503: Enum.reduce_range/5
    (oban_pro 1.6.0-rc.3) lib/oban/pro/migration.ex:261: Oban.Pro.Migration.inner_change/3

Thanks for posting the error!

These ^ need to be in two separate migrations:

defmodule Core.Repo.Migrations.AddObanPrefix do
  use Ecto.Migration

  def up do
    Oban.Migrations.up(prefix: "pacman")
  end

  def down do
    Oban.Pro.Migration.down(prefix: "pacman")
  end
end

defmodule Core.Repo.Migrations.AddObanProPrefix do
  use Ecto.Migration

  def up do
    Oban.Pro.Migration.up(prefix: "pacman")
  end

  def down do
    Oban.Pro.Migration.down(prefix: "pacman")
  end
end