jbsf2 / process-tree

A module for avoiding global state in Elixir applications
https://hexdocs.pm/process_tree
MIT License
30 stars 4 forks source link

Feature request: support `$callers` #2

Open axelson opened 2 weeks ago

axelson commented 2 weeks ago

Currently this project supports $ancestors which means that process tracking across Task.async works great. But if you use Task.Supervisor.async (or async_nolink) then the ex_unit test is not in the ancestor tree at all and the environment cannot be injected at all. I have a PoC for adding $callers support but I wanted to raise this as an issue first before creating the PR.

And thank you for this project, it has really helped clean up a bunch of our tests at Felt! :heart:

jbsf2 commented 2 weeks ago

Hey that's cool you're finding value from the project - thanks for sharing! Definitely open to adding support for $callers if there's a way to do it reasonably. I haven't thought through the issues, though it sounds like maybe you have :-) Seems like the main question is, for a process whose lineage might (or might not) include $callers and $ancestors, is there a way to discover the true lineage and preserve correct ordering? If you've got a POC, happy to see it - thank you!

axelson commented 2 weeks ago

I don't think there's really the concept of a "true lineage". I'd look at it more as the process having two separate lineages. I was planning to lookup values via $ancestors first, and then look in $callers. I've pushed up the PoC as #3, let me know what you think!

axelson commented 1 week ago

Another question is if we want to always look at $callers or if it should be configurable. For our use-case we'd want to always look at $callers because we're using process_tree solely for testing.

jbsf2 commented 1 week ago

Thanks Jason! I'm intrigued by your approach and hope to get something along those lines into the project. First I need to understand the wrinkles around $callers and how people use Task.Supervisor.

To that end, I wonder if you could help me out a bit by showing me the scenarios you have in which "the ex_unit test is not in the ancestor tree at all", as you pointed out in your original post.

I've pushed a branch called callers-investigation that has a test called "$callers test":

    test "$callers test" do
      dbg(self())

      {:ok, supervisor} = Task.Supervisor.start_link()

      Task.Supervisor.async_nolink(supervisor, fn ->
        dbg(Process.get(:"$callers"))
        parent = ProcessTree.parent(self())
        dbg(parent)
        grandparent = ProcessTree.parent(parent)
        dbg(grandparent)
      end)

      :timer.sleep 1000
    end

In this scenario, the ex_unit test is in the ancestor tree, as seen by the output from the test:

jb ~/projects/process_tree (main) % mix test test/process_tree_test.exs:353
Running ExUnit with seed: 749369, max_cases: 16
Excluding tags: [:test]
Including tags: [location: {"test/process_tree_test.exs", 353}]

[test/process_tree_test.exs:353: ProcessTreeTest."test $callers callers"/1]
self() #=> #PID<0.180.0>

[test/process_tree_test.exs:358: ProcessTreeTest."test $callers callers"/1]
Process.get(:"$callers") #=> [#PID<0.180.0>]

[test/process_tree_test.exs:360: ProcessTreeTest."test $callers callers"/1]
parent #=> #PID<0.181.0>

[test/process_tree_test.exs:362: ProcessTreeTest."test $callers callers"/1]
grandparent #=> #PID<0.180.0>

Clearly your real-life scenario is doing something different, and if you have time to show me, I'd love to understand what that is. Thank you!!

jbsf2 commented 1 week ago

I'm guessing you're using a Supervisor that isn't started in your test? Would love to see the details. Thanks!

jbsf2 commented 1 week ago

Check out the callers-implementation branch and take it for a spin in your environment. Let me know if it does the trick!

You can see the approach here: https://github.com/jbsf2/process-tree/pull/4

axelson commented 1 week ago

Clearly your real-life scenario is doing something different, and if you have time to show me, I'd love to understand what that is. Thank you!!

Yeah the main difference is that Task.Supervisor is started as part of the application supervision tree. I was surprised to notice that the Task.Supervisor docs didn't give any specific guidance on how to start the supervisor, but especially if you want to use Task.Supervisor.async_nolink then you will want to start the supervisor as part of your supervision tree. Here's a blog post I found that covers it decently: https://dev.to/felipearaujos/the-power-of-elixir-task-module-into-task-supervisor-5365

Check out the callers-implementation branch and take it for a spin in your environment. Let me know if it does the trick!

Nice! 🙌 I'm happy to report that it works in my test setup! 🎉

Also I like how you look up the entire tree, that makes a lot of sense now that I think about it.