tiagodavi
Cannot find ownership process for #PID<0.564.0>
It’s a little annoying testing async code in eixir…
Is it possible to run async tests with database by manually controlling my connections?
I have a piece of code that does:
caller = self()
Task.Supervisor.start_child(X.TaskSupervisor, fn ->
Ecto.Adapters.SQL.Sandbox.allow(X.Repo, caller, self())
it returns: cannot find ownership process for pid…
I put
setup do
# Explicitly get a connection before each test
:ok = Ecto.Adapters.SQL.Sandbox.checkout(X.Repo)
end
it returns: {:already, :owner}
and still: cannot find ownership process for pid…
I want to run async: true.
Should I always run async: false with databases?
Marked As Solved
ityonemo
This works for me:
test "async databases" do
{:ok, sup} = Task.Supervisor.start_link()
test_pid = self()
data = DB.Table.insert_new()
DB.Table.all() |> IO.inspect # has the new item
Task.Supervisor.start_child(sup, fn ->
DB.Table.all() |> IO.inspect # sees the same stuff
# we're finished. If you don't do this parent test pid closes and takes the db checkout with it
send(test_pid, :done)
end)
#keep the db checkout alive till it's done
receive do :done -> :ok end
end
Also Liked
ityonemo
Correct. GenServers don’t bind $callers. This is deliberate. I’m not saying it’s always a bad idea (because I do something similar for a real reason in prod [see note]), but if you have a GenServer calling a database, there’s a good chance you’re violating the basic good-practices principle of functional-core OTP design:
https://hexdocs.pm/elixir/GenServer.html#module-when-not-to-use-a-genserver
Use processes only to model runtime properties, such as mutable state, concurrency and failures, never for code organization.
If you’re using a GenServer to call a database there is a really good chance you are trying to use the GenServer to organize your code. If you truly have transient GenServers, after you spin them up in your task, 1) your test should be able to find it and 2) you should be then able to run Process.put(:“$callers”, [test_pid]) inside the GenServer, or (better Sandbox.allow), then your genserver will see the database.
If really really need some sort of Global Genserver with a name, then it’s Global. You can’t run async tests on it, because you have shared async state, and that’s that. I think that honestly most use cases don’t need this, so you may be able to rearchitect your system.
[note] I did the Process.put(:“$callers”) thing, and took the hit on convenience because it was important. I think elixir doing this is a great thing, because the roadblock made me stop and think, “do I really need this?”.
ityonemo
In your code you’re spawning a task that immediately dies after attempting to obtain ownership. Tasks also get bound into the caller chain anyways, so that’s why it says “already owner” when you run the allowance after you activate the repo.
You are probably attempting to access the database from some thread “out there” somewhere. You need to set the allowance on the thread that’s calling the database (not a random thread that is just going to die anyways). Find the pid at the site of the database repo call (maybe inspect it). If you can’t figure out how to communicate that pid back to the test, it may be that you have architecture your system in a way that makes it difficult to test. Imo, those systems will also be hard to maintain in the long run. In which case you have two options: roll with it, or rearchitect your system.
bruteforcecat
Are you sure you are using manual mode for Sandbox?
tiagodavi
Thank you guys!
I was able to solve it by thinking another approach. I decided to “await for messages” synchronously by using a recursive function. Since this functions runs inside a “Task” it has access to the database automatically… and it’s working.
defp consume(channel, queue, attempts \\ 1) do
if attempts <= 30 do
AMQP.Basic.get(channel, queue)
|> case do
{:empty, _} ->
:timer.seconds(10 * attempts)
|> Process.sleep()
consume(channel, queue, attempts + 1)
{:ok, payload, _meta} ->
AMQP.Queue.delete(channel, queue)
AMQP.Channel.close(channel)
similarities = Jason.decode!(payload)
update_similarities(similarities)
_ ->
{:error, "Rabbit is crazy"}
end
else
{:error, "Rabbit is offline"}
end
end
ityonemo
This code looks much nicer and easier to rationalize!!








