Skip to content

Instantly share code, notes, and snippets.

@damonvjanis
Created October 12, 2023 20:18
Show Gist options
  • Save damonvjanis/4cefead8c5f783aa9e4965dc678ac0ed to your computer and use it in GitHub Desktop.
Save damonvjanis/4cefead8c5f783aa9e4965dc678ac0ed to your computer and use it in GitHub Desktop.
Oban batching slow test example
# my_app.ex
defmodule MyApp do
alias MyApp.Jobs.TestJob
def test_work(use_batch?) do
1..100
|> Enum.map(fn i -> %{job_number: i} end)
|> then(fn args_list ->
if use_batch? do
TestJob.new_batch(args_list, batch_callback_args: %{batch: :my_batch})
else
Enum.map(args_list, &TestJob.new/1)
end
end)
|> Oban.insert_all()
end
end
# my_app/jobs/test_job.ex
defmodule MyApp.Jobs.TestJob do
use Oban.Pro.Workers.Batch
@impl Oban.Pro.Worker
def process(%Job{args: %{"job_number" => _job_number}}) do
# Quick queries to the database, processing etc
Process.sleep(25)
:ok
end
@impl Batch
def handle_completed(%Job{meta: %{"batch_id" => batch_id}}) do
{:ok, batch_id}
end
end
# my_app_test.exs
defmodule MyAppTest do
use MyApp.DataCase, async: true
require Logger
test "uses batch and goes slow" do
start = DateTime.utc_now()
MyApp.test_work(true)
diff = DateTime.diff(DateTime.utc_now(), start)
Logger.warn("Took #{diff} seconds to process jobs using batch")
end
test "doesn't use batch and goes fast" do
start = DateTime.utc_now()
MyApp.test_work(false)
diff = DateTime.diff(DateTime.utc_now(), start)
Logger.warn("Took #{diff} seconds to process jobs inline")
end
end
@sorentwo
Copy link

Here's a modification of the test that allowed me to run it in the Pro repo directly (there aren't any changes to MyApp or TestJob):

defmodule Oban.Pro.Workers.BatchSpeedTest do
  use Oban.Pro.Case, async: true

  # my_app.ex
  defmodule MyApp do
    alias MyApp.Jobs.TestJob

    def test_work(use_batch?) do
      1..100
      |> Enum.map(fn i -> %{job_number: i} end)
      |> then(fn args_list ->
        if use_batch? do
          TestJob.new_batch(args_list, batch_callback_args: %{batch: :my_batch})
        else
          Enum.map(args_list, &TestJob.new/1)
        end
      end)
      |> Oban.insert_all()
    end
  end

  # my_app/jobs/test_job.ex
  defmodule MyApp.Jobs.TestJob do
    use Oban.Pro.Workers.Batch

    @impl Oban.Pro.Worker
    def process(%Job{args: %{"job_number" => _job_number}}) do
      # Quick queries to the database, processing etc
      Process.sleep(25)

      :ok
    end

    @impl Batch
    def handle_completed(%Job{meta: %{"batch_id" => batch_id}}) do
      {:ok, batch_id}
    end
  end

  require Logger

  setup do
    start_supervised_oban!(
      name: Oban,
      testing: :disabled,
      queues: [default: 10],
      stage_interval: 10
    )

    :ok
  end

  test "uses batch and goes slow" do
    {time, _} = :timer.tc(fn -> MyApp.test_work(true) end)

    await_complete()

    Logger.warning("Took #{time} microseconds to insert jobs using batch")

    stop_supervised!(Oban)
  end

  test "doesn't use batch and goes fast" do
    {time, _} = :timer.tc(fn -> MyApp.test_work(false) end)

    await_complete()

    Logger.warning("Took #{time} microseconds to insert jobs inline")

    stop_supervised!(Oban)
  end

  defp await_complete do
    with_backoff(fn ->
      assert ~w(completed) ==
               Oban.Job
               |> Repo.all()
               |> Enum.map(& &1.state)
               |> Enum.uniq()
    end)
  end
end

That inserts the jobs and waits for them all to complete (you should use a :manual testing mode and drain_jobs or run_batch to do this in testing, but I digress).

Here's the output showing that inline takes ~14ms and the batch takes ~44ms (waiting for the callback to trigger):

Screen Shot 2023-10-12 at 4 10 40 PM

Something seems off with your database or application setup.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment