anotherpit
A graft inside another graft causes Oban.Pro.Workflow.status to recurse forever. oban_pro 1.7.5
defmodule App.NestedGraftRepro.Test do
# Minimal, self-contained reproduction of an Oban.Pro nested-graft bug (oban_pro 1.7.5).
#
# A graft that executes while its own job's `workflow_id` is already "grafted_"-prefixed
# (i.e. a graft nested inside a grafted workflow) produces a sub-job whose
# `workflow_id == sup_workflow_id`. `Oban.Pro.Workflow.status/1` walks sub-workflows via
# `sup_workflow_id -> workflow_id` with no visited-set, so that self-edge makes
# `expand_status` recurse forever (here: child process killed at max_heap).
#
# Topology:
# - W -> add_graft(:outer);
# - outer grafts a workflow containing add_graft(:inner);
# - inner grafts a leaf -> leaf.workflow_id == leaf.sup_workflow_id.
use ExUnit.Case, async: false
# `prefix: "sunrise"` matches this app's Oban schema; upstream you'd drop it (default "public").
use Oban.Pro.Testing, repo: App.Repo
import Ecto.Query
setup do
pid = Ecto.Adapters.SQL.Sandbox.start_owner!(App.Repo, shared: true)
on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
:ok
end
defmodule Leaf do
use Oban.Pro.Worker, queue: :general, max_attempts: 1
@impl Oban.Pro.Worker
def process(%Oban.Job{}), do: :ok
end
defmodule InnerGrafter do
use Oban.Pro.Worker, queue: :general, max_attempts: 1
@impl Oban.Pro.Worker
def process(%Oban.Job{}) do
Oban.Pro.Workflow.new()
|> Oban.Pro.Workflow.add(:leaf, Leaf.new(%{}))
|> Oban.Pro.Workflow.apply_graft()
|> Oban.insert_all()
:ok
end
end
defmodule OuterGrafter do
use Oban.Pro.Worker, queue: :general, max_attempts: 1
@impl Oban.Pro.Worker
def process(%Oban.Job{}) do
# Graft a workflow that itself contains a graft point -> nesting.
Oban.Pro.Workflow.new()
|> Oban.Pro.Workflow.add_graft(:inner, InnerGrafter.new(%{}))
|> Oban.Pro.Workflow.apply_graft()
|> Oban.insert_all()
:ok
end
end
test "nested graft produces workflow_id == sup_workflow_id, and status/1 never terminates" do
Oban.Testing.with_testing_mode(:manual, fn ->
Oban.Pro.Workflow.new()
|> Oban.Pro.Workflow.add_graft(:outer, OuterGrafter.new(%{}))
|> Oban.insert_all()
# Recursive drain (default) runs the dynamically-grafted jobs as they're inserted.
drain_jobs(queue: :general, with_safety: true)
jobs =
from(j in Oban.Job,
select: %{
name: fragment("?->>'name'", j.meta),
wid: fragment("?->>'workflow_id'", j.meta),
sup: fragment("?->>'sup_workflow_id'", j.meta)
}
)
|> App.Repo.all()
for j <- jobs, do: IO.puts("JOB #{j.name} wid=#{j.wid} sup=#{j.sup}")
# 1. The broken state: a job whose workflow_id == its own sup_workflow_id.
selfloop = Enum.find(jobs, fn j -> not is_nil(j.sup) and j.wid == j.sup end)
assert selfloop, "expected a workflow_id == sup_workflow_id self-loop job, found none"
IO.puts("SELF-LOOP NODE: name=#{selfloop.name} wid=#{selfloop.wid} sup=#{selfloop.sup}")
# 2. status/1 on that workflow never returns (infinite expand_status recursion).
# Run it in a heap-capped child so the test process survives.
parent = self()
{pid, ref} =
:erlang.spawn_opt(
fn ->
result = Oban.Pro.Workflow.status(selfloop.wid)
send(parent, {:returned, result})
end,
[:monitor, {:max_heap_size, %{size: 2_000_000, kill: true, error_logger: false}}]
)
outcome =
receive do
{:returned, _} -> :returned
{:DOWN, ^ref, :process, ^pid, reason} -> {:died, reason}
after
8_000 ->
Process.exit(pid, :kill)
:timeout
end
IO.puts("status/1 outcome: #{inspect(outcome)}")
refute outcome == :returned,
"status/1 returned — infinite recursion NOT reproduced (bug may be fixed)"
end)
end
end
Trending in Troubleshooting
I’m using Postgrex in a Phoenix app to connect to Postgres. The database for my dev instance is in a local VM, with hostname jot.local. M...
New
Job args skip format_job_args/1 in the job edit form and in search hints, even with read-only access
Hi Parker and Shannon,
First, thank you for Oban and Oban Web. They’re a pleasure to build on, and the resolver’s format_job_args/1 and ...
New
Other Trending Topics
Edit: 2026 May 15 - This post is archived.
Mob is alive!!
Main docs: mob v0.7.11 — Documentation
A bit of explanation for the slightly c...
New
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
I am happy to introduce the very α version of the new programming language compiled to BEAM.
Welcome Cure.
It has literally three kille...
New
Hobbes is a low-level distributed database for the Elixir programming language.
Hobbes provides a simple, safe, and scalable storage lay...
New
Hi everyone!
The first release candidate for the Expert language server project is now available!
We’ve published a press release detai...
New
A little off-topic, but I feel like people here have a good head on their shoulders.
I used to be quite good at making software. Was luc...
New
Latest Oban Threads
Questions & Help>Troubleshooting
Latest on Elixir Forum
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #library
- #deployment
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #podcasts
- #javascript
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ai
- #ecto-query
- #elixirconf-us
- #blog-post
- #elixir-ls
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #elixirconf-eu
- #api
- #forms
- #metaprogramming
- #hex











Showing Posts 1 to 1- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
sorenone
Welcome
@anotherpit,
Thanks for catching a strange edge case and giving us the report. It’s fixed and ready for the next patch release.