How to find and kill an application-supervised, static (unnamed) Task in ExUnit (but only for some tests)?

I have an Agent and an infinite-running static Task that are application-supervised on startup. The Task is generating an array of numbers that are being stored in the Agent.

When testing, ExUnit starts these processes automatically, which I want for most of my tests (i.e., I don’t want to run mix test --no-start).

For both the Agent and the Task, I want to test their interactions. But, since they start automatically, by the time I can actually test them, they have been populated already with an arbitrary amount of data. It seems like a reasonable approach would be to kill both process and then recreate them with ExUnit.start_supervised. But, the static Task doesn’t have a name. So, the main question is: How do I find and kill an application-supervised static Task? (I don’t have this problem with my Agent because it is ‘named’, i.e., name: __MODULE__.

But, this doesn’t really solve my testing problem. Why? Because if I kill my application processes, the rest of my tests, which test functionality that depends on the Agent data being there, will all fail. What I really need is the processes to exist for the majority of my test suite, and then, somehow, I need to be able to isolate the tests of my Agent and Task from the greater application environment. (This presumably includes the complication of have the Agent have the same __MODULE__ name, so, maybe would involve a separate Registry? And, I’d prefer to not refactor my Agent module to receive different atom names just so I could test it, but if that’s necessary to get it work, I’d be happy to do it.)

I’ve been reading and experimenting all day, trying to find a way to get a passing test suite, but have failed so far. I can’t manage to find a way. Any feedback would be appreciated :smiley:

Instead of testing the processes started by your application start another set of those processes for testing the interactions. Basically splitting up the parts of “the processes are started as part of the application” from the tests about “assert that the processes correctly interact with each other”.

1 Like

Thank you for your reply :smiley:

I’m confused on how to do that. Do I create a Supervisor.start_link in the setup? If the name of a process is the name of the module, will the Registry be okay with that? How do I tell, e.g., my Task process which of the two available Agent processes to use (each with presumably the same name)? Any code examples you know of I could look to?

Thanks!

Okay, well, this post seems to imply that long-running Tasks are almost an anti-pattern, and you should instead use a genserver: Best way to name permanent tasks? - #8 by ityonemo

The TL;DR as it applies to my issue involves using Process.register inside the Task module, so it’s named. And, also, to probably not use a Task, and instead use a GenServer.

I will attempt to rewrite my Task as a GenServer. It seems a bit like overkill, but maybe the added functionality potential will serve me well in the long run. What do you think @LostKobrakai ?

1 Like

So, it seems like you are building some sort of “random number service” as the Agent. I guess, my question to you would be then why not just use a single GenServer and instead of using a task to generate your numbers, generate numbers in your GenServer between requests (you could probably also do this with Agent, but i kind of have a bias against Agent, in almost all cases you should not be using Agent). Do you really need the number generation to be asynchronous? Because once it becomes asynchronous, your complexity increases, you need to think about strange failure modes (what happens when the Task disappears but not the Agent? what about vice versa?), and if you have a “synchronous” genserver service doing the generation, then you only have to think about one line of execution and stuff stays (relatively) sane.

You could also build out a single instance of the server and test it in isolation.

Finally, if you are really adamant about having this Agent/Task architecture, I would put the Agent and the Task under a common supervisor, and start a “fresh supervisor” in every test you run. Don’t forget to hoist the “naming” option to an option in the supervisor setup, so that in your Application.start call it gets properly named, but when you spawn off new ones for testing, they’re not bound to a locally unique name

1 Like

Hey @ityonemo , thanks for the reply here as well. :smiley:

After looking into it some more, I agree with you that a GenServer seems like a better option. It seems like I could just combine both my Agent and long-running Task into a single, stateful GenServer. I’m trying to do that now but I’m getting confused about the best way to “control the loop” of the GenServer.

I want the GenServer to be constantly generating new numbers and putting them in an ever-growing array of numbers. I also want to occasionally query that server for the array generated thus far. Sending messages via the GenServer API (e.g., GenServer.call & handle_call) makes sense when asking for the array generated thus far. But, how do I tell the GenServer to constantly update it’s own internal state? handle_continuous, and handle_info + Process.send_after both seem promising, but also feel kind of hacky, since really I don’t want to send a message to the GenServer to tell it to find the next number, I want it to be doing it in the background, all the time.

This is my first day writing a GenServer, so I’m highly likely to be missing something, lol.

Thanks for all the help! :smiley:

you might want to do something like :timer.send_interval(interval, self(), :tick) on init, Erlang -- timer if you need the generation to be triggered indepedently of other events happening to your GenServer and even if no other events happen.

My personal preference is that I don’t like Process.send_after(self(), :tick, interval), because it feels like you if you are not careful coding, you could accidentally retrigger it n times and wind up with n times more events than you wanted.

If you only need it in a responsive sense (only need to update the list after some event has happened), I recommend using the handle_continue, I think if you can get away with doing that that is the best way, because your event schedule is more “deterministic” for testing purposes, and probably it’s easier to reason about.

1 Like

Okay, so, I half didn’t expect this to work, but you can leverage the timeout functionality of the GenServer to work until interrupted. A timeout is handled by handle_info, which can itself “set a timeout”. The timeout is cleared by any other message. So handle_call must also set a timeout in order to the sequence to continue generating.

I highly doubt I’m using the timeout feature as intended, but it’s the easiest (albeit ‘hackiest’) way I found to do it.

Here’s example code:

@impl true
def init(_) do
  initial_state = ...
  {:ok, initial_state, 0}
end

@impl true
def handle_call(:state, _from, state) do
  {:reply, state, state, 0}
end

@impl true
def handle_info(:timeout, state) do
  {:noreply, step(state), 0}
end

def step(state) do
  # update the state
  ...
end

@ityonemo I think your idea about :timer.send_interval or using Process.register on the Task approach are the two more sane approaches. I feel like there are a multitude of ways to approach my problem, since all of the things are just processes at the end of the day. I’m going to move forward with this timeout hack approach, but it will probably be too brittle if I ever want to develop the GenServer functionality further. :man_shrugging:

I’m skeptical that using the timeout feature is a good idea – it has the opposite problem of the Process.send_after trick, if you fail to put a timeout in any callback handler in the future, then you will lose the cycle. In theory every possible message coming in to GenServer should be handled by the handlers… But if you were to say switch from GenServer to some other type of process (possibly which wraps GenServer) that someone else has built as a library, you very well could lose your loop, if that GenServer-wrapped system is presenting some sort of message passing API behind the scenes.

It also makes it hairier to do some things, like one thing I like to do sometimes is have a dump() call which lets me inspect the state of the GenServer for tests, you’ll have to reimplement the timer functionality in that call too. And besides which, if your GenServer gets interrupted, you have to recalculate the timeout using a delta, and that’s going to drift, etc…

The biggest problem I feel people have with testing “global” processes, is that the fact that they’re hard coded to be global. Things get quite a bit simpler if naming a process (or registering elsewhere) becomes something controlled by start parameters, where an application started instance can still make sure its registered, but e.g. tests can easily start their own instances unnamed and therefore be more flexible in how to test the complete livecycle of the process.

3 Likes

Do You have a way to make it lazy?

1 Like

By lazy I assume you mean: generate new numbers when the GenServer doesn’t have pending messages. If that’s what you meant, then they hacky answer I have now is to use the :timeout hack, which I wouldn’t recommend for the reasons @ityonemo mentioned above. Also, in line with @LostKobrakai 's points, I think I could’ve taken a less “global” approach to my Agent/Task setup, which would’ve made it more testable, which would’ve solved my testing problem (I assume) and also would’ve kept my code in a two-process solution (instead of the one-process solution of the GenServer).

My naive gut-reaction is that a two-process solution is the more appropriate one, given that I want something infinitely generating numbers, and another serving that data to my main process. Also, my naive gut-reaction is that Agent/Task are semantically most appropriate. But, also, I like my hacky GenServer solution more.

I wonder if there’s an idiomatic way to tell a GenServer: “when not responding to messages, do this work: …”. That’s essentially how I’m using the :timeout trick for the moment. But maybe there’s a way to leverage the async nature of GenServers to achieve the same? I’m too noob at the moment to figure it out XD

No, I mean lazy like a stream.

Instead of generating in advance, You could generate numbers just in time, without GenServer.

Also You are using one server, when You could use many. One to generate, one to answer calls.

No need to hack timeout…

Oh I see, yes I am using a Stream to stream the values coming out of the process. I’m using Stream.resource to find the newest values from the process and then streaming them in my main process. The reason that the stream alone is not enough is because the computations I want to perform per-streamed-number are also made more efficient by access to all previous values. I could attempt to store all streamed numbers in some global list and map, but as I looked into how to do that, it seemed that Agents/Tasks/GenServers/Processes were the more idiomatic way to approach global stores and global generators.

The core desire is to stream numbers and meanwhile collect those numbers into variety of data shapes (i.e., lists and maps) which in turn efficiently aid in a variety of calculations. I want to do this all in as Elixir-idiomatically as possible. And, of course, well-tested.

The reason that the stream alone is not enough is because the computations I want to perform per-streamed-number are also made more efficient by access to all previous values

Sounds like you need a reducer, not a global store? A reducer will have “memory” without the overhead of multiple processes communicating. Worst case scenario, there’s also the Process dictionary hack. Are more than one processes consuming this stream?

Great advice @ityonemo , I think Stream.transform would be sufficient for my purposes. Also, based on your own video series, I want to avoid the process dictionary (great series, btw :D).

The minor reason I want to use multiple processes is because this is personally a really enjoyable way for me to explore Elixir process interactions. The major reason is because I want to build a robust calculation system that is capable of concurrently calculating multiple number sequences and being able to leverage arbitrary sets of them to perform main-process calculations. (The third reason is that I feel like Elixir processes, being lightweight and concurrent, has the potential to shine in hyper-parallelizable calculations, so I’m keen to learn first-hand about the performance characteristics of inter-process calculations.)

I was thinking about how to solve your original problem, without worrying about why you were doing this and whether there was an alternative solution. Having a supervised process that parts of your test suite depend upon, while being running it independently to unit test, is a problem that comes up everyone once in a while.

I’m assuming that you’re running one Agent in your supervision tree, and that the name it registers itself with is __MODULE__ or some other globally unique atom.

One approach I’ve used is for the application supervisor not start the process in test, and to manually start the process for each test that requires it. That can get painful when you have a bunch of these, or if every other test winds up needing the running process in some fashion.

This blog post by Samuel Mullen describes how to write your start_link/2 function such that the application supervisor starts it with a default name, but where you can provide a different name with starting the process with ExUnit.Callbacks.start_supervised!/1.

1 Like