Notes

What the unique period parameter means for enqueing jobs in Oban

04 June 2024

Oban lets you set unique parameters to avoid creating duplicate jobs.

If I’m sending an email I want to make sure the job is only inserted once:

defmodule WelcomeEmailWorker do
  use Oban.Worker, unique: [period: 60]
	...
end

DailyEmailWorker.new(%{email: "bob@example.com"}) |> Oban.insert()

My mental model of how this works was that the period ensured no duplicates were inserted after that job. I was wrong. The period is actually subtracted from the current time.

This makes sense. A lot of the time this is exactly what you want: if a job is created to send a welcome email you want to make sure that job is created and executed only once. You don’t want a duplicate job to be inserted and looking back 60 seconds prevents this.

iex> WelcomeEmailWorker.new(%{email: "bob@example.com"}) |> Oban.insert()
{:ok,
  %Oban.Job{
    id: 1,
    inserted_at: ~U[2024-06-01 12:00:00Z],
    conflict?: false
    ...
  }
}


iex> WelcomeEmailWorker.new(%{email: "bob@example.com"}) |> Oban.insert()
# An existing job conflicts, so that job is returned
{:ok,
  %Oban.Job{
    id: 1,
    inserted_at: ~U[2024-06-01 12:00:00Z],
    conflict?: true
    ...
  }
}

If I want to schedule multiple jobs in advance this becomes a problem. Even if unique: [timestamp: :scheduled_at] is used Oban still looks ahead for existing jobs.

Soren, the creator of Oban, explains a simple solution: set a field in the args that is unique for the day.

defmodule DailyEmailWorker do
  use Oban.Worker, unique: [period: :infinity, keys: [:email, :date]]
  ...
end

iex> DailyEmailWorker.new(%{email: "bob@example.com", date: ~D[2024-06-01]}) |> Oban.insert()
{:ok,
  %Oban.Job{
    id: 1
    ...
  }
}

iex> DailyEmailWorker.new(%{email: "bob@example.com", date: ~D[2024-06-02]}) |> Oban.insert()
{:ok,
  %Oban.Job{
    id: 2
    ...
  }
}