rindek

rindek

Hi everyone,

I’m currently working on a project using Oban Pro 1.4.0 with Smart Engine, and I need some guidance on configuring a queue with multiple workers. Specifically, I have the following requirements:

  1. The queue should execute a maximum of 10 jobs per node.
  2. It should not execute more than one unique worker with the same arguments globally (i.e., no more than one instance of the same worker/arguments combination should run simultaneously across the entire cluster).
  3. If a worker with the same arguments is already executing, any new job with the same worker/arguments combination should be placed in the available or scheduled state, so it can be processed immediately after the current one finishes.

Here’s what I’ve tried so far:

Oban Queue Configuration:

my_queue: [
  local_limit: 10,
  global_limit: [allowed: 1, partition: [fields: [:args, :worker]]]
]

Worker Configuration:

use Oban.Worker,
  queue: :my_queue,
  unique: [
    fields: [:args, :worker],
    states: [:available, :scheduled, :retryable],
    period: :infinity
  ]

However, when running tests, I noticed that if I enqueue the same worker with the same arguments 15 times, it starts executing 10 workers and puts 1 in the available state. I expected it to execute 1 worker and also put 1 in the available state since it’s the same worker and arguments.

In contrast, if I enqueue 15 different unique worker/arguments combinations, I would expect it to start executing 10 jobs and puts the remaining 5 in the available state.

I’m running multiple different workers for the same queue, so I can’t rely only on args or only on the worker; I need to rely on both.

Is it possible to configure Oban in this way? If so, what adjustments do I need to make?

Thanks for your help!

Showing Posts 1 to 10

sorentwo

sorentwo

Oban Core Team

In your worker configuration the states list doesn’t include executing, so an executing job is no longer considered unique and you’ll end up with multiples.

That’s fine, global partitioning by worker and args is possible. However, I suggest using an explicit list of keys whenever possible for predictability and performance.

[allowed: 1, partition: [:worker, args: :some_key]]

Based on your criteria it sounds like you might want to use the Chain worker rather than uniqueness.

Chain workers link jobs together to ensure they run in a strict sequential order. Downstream jobs won’t execute until the upstream job is completed, cancelled, or discarded. Behaviour in the event of cancellation or discards is customizable to allow for uninterrupted processing, holding for outside intervention, or cascading cancellation.

Jobs in a chain only run after the previous job completes successfully, regardless of snoozing or retries.

Chains use the same partitioning format as queues, so optimally you’ll match the options to ensure only one job in a chain runs at once. There’s more in the Optimizing Chains section of the module docs.

rindek

rindek OP

Thank you for the answer.

I don’t specifically need the jobs to run in a particular order; my main requirement is to ensure that the same worker/args does not run concurrently.

Most of these workers index data in Elasticsearch after specific events occur in the app. Some workers might run for several minutes, and during this time, there could be multiple updates. In such cases, I need to schedule another indexing worker, but only the latest update is relevant. Therefore, I don’t need to run other indexing workers that might have been scheduled in the meantime.

Consider a simple worker:

defmodule SimpleWorker do
  use Oban.Worker,
    queue: :my_queue,
    unique: [
      fields: [:args, :worker],
      states: [:available, :scheduled, :retryable],
      period: :infinity
    ]

  @spec perform(Oban.Job.t()) :: :ok
  def perform(%Oban.Job{args: %{"client_id" => client_id} = _args}) do
    :timer.sleep(10000)

    :ok
  end
end

This is how I test this scenario:
[1, 2, 3] |> Enum.each(fn cid -> for i <- 1..10, do: Oban.enqueue(SimpleWorker, %{client_id: cid}) end)

When I added executing to states in the configuration, what happened was that SimpleWorker with client_id 1, 2, 3 started executing. There were no others in the executing state (so 3 concurrent total), but also there were no jobs waiting in the available state. My requirements are to additionally put SimpleWorker with client_id 1, 2, 3 in the available state so that when the current executing ones finish, they will just start working.

If I understand the unique option correctly, it searches for the specific worker/args combination in all states. So, if there is one in the executing state, it won’t create another one in the available state. Ideally, for my case, Oban would check for uniqueness separately for each state. This way, if there is currently an executing worker, it won’t start executing another one and instead “wait” in the available state, it won’t add another one with the same args.

Is there a way to configure Oban to handle this scenario?

Thanks again for your help!

sorentwo

sorentwo

Oban Core Team

It can be used to that effect, but there’s an easier way.

The use case you’re describing can be most easily accomplished by debouncing. Set the unique period to a shorter period, and then insert the jobs in a scheduled state. That will prevent accumulating a string of jobs, so you’ll have one executing and then another one ready to run next.

Here’s a tweak on the worker you shared above:

defmodule SimpleWorker do
  use Oban.Worker,
    queue: :my_queue,
    unique: [states: [:available, :scheduled, :retryable], period: 30]

  @impl Oban.Worker
  def perform(%Job{args: %{"client_id" => client_id}}) do
    Process.sleep(10_000)

    :ok
  end
end

Then build new workers with SimpleWorker.new(%{}, schedule_in: 30). Set the period to prevent overlapping jobs, and you don’t even need the global partitioning because there’s only one of each job anyhow.

rindek

rindek OP

Thank you very much, I will collect all the info and try to craft the appropriate solution for my needs :slight_smile:

begedin

begedin

Sorry to resurrect this, but would your approach not work with a period of :infinity. As in, does the uniqueness period have to be 30 or does the thing that really matter here the fact that the job is being scheduled in 30 seconds?

If infinity doesn’t work, why?

sorentwo

sorentwo

Oban Core Team

It may work, depending on your scenario. For the original problem of debouncing updates you have a good chance of losing updates with uniqueness set to :infinity.

begedin

begedin

Hm, I have a confusing scenario then. This is my worker config:

use Oban.Worker,
    max_attempts: 1,
    queue: :my_queue,
    unique: [
      period: :infinity, # now changed to 60
      states: [:available, :scheduled, :retryable],
      fields: [:worker, :queue, :args],
      keys: [:team_id, :type]
    ]

it’s what got me to this topic. Enqueuing multiple jobs with the same args in quick succession, scheduled in 60 seconds, ends up in multiple jobs running at the same time, even though their args is exactly the same.

I must be missing something obvious, but can’t for the life of me figure out what.

sorentwo

sorentwo

Oban Core Team

That’s because it doesn’t include the executing state. If another job is already running it will enqueue another.

In contrast to what I shared above, it’s least confusing to use the default states or at least include the incomplete states (available, scheduled, retryable, executing)

begedin

begedin

I’m using schedule_in: 60, though, and seeing multiple scheduled jobs spawning at the same time.

sorentwo

sorentwo

Oban Core Team

Uniqueness is only applied at insert time, not at runtime. Without chains or partitioned globally limited queues there’s nothing to automatically stop related jobs from running at the same time.

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
kszambelanczyk
Hello! Could someone please give me a help/sample code, how to delete a file from s3 using waffle/waffle_ecto from Phoenix app. I creat...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New
nseaSeb
Hello, I know there is an approach for handling lists that allows for optimized traversal, but I can’t recall the specific method (somet...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews