Best way to name permanent tasks?

I’m trying to name a supervised, permanent task so I can find it easier. What’s the best way to go about this? I tried out this right here:

https://hexdocs.pm/elixir/1.10.3/Task.html#module-supervised-tasks

However, when I run Process.whereis(MyApplication.MyTask) instead of getting a PID back, I get nil. This is an ongoing looping task. From MyApplication.Application I also tried moving the starting of the Task via a different Supervisor call like so:

Supervisor.start_link([
  {MyTask, arg}
], strategy: :one_for_one, id: :"MyApplication.MyTask")

And that didn’t seem to work either. I don’t know what exactly the :id would do, but I thought it was worth trying to pass that along.

1 Like

When starting the Task you need to provide a :name, probably that’s missing, but you haven’t shown how you actually start it, therefore I’m just guessing.

Tasks are really not the right kind of thing to have running for a long time, you should use a genserver instead.

However to your specific question, id is not the value used to set the name, you need a name option, but I’m not even sure if tasks take a name option.

3 Likes

Indeed, Task.start_link/3 does not allow for something like the :name option.

https://hexdocs.pm/elixir/v1.10/%20/Task.html

2 Likes

I built gen_loop for when I want Task-like processes and need to communicate with them. (Actually I mostly packaged it, it is just :plain_fsm under the hood).

But if you just want to be able to address a Task by name, why not just call Process.register/2 at the beginning of the task ?

Process.register/2 should work. But yeah. At the point where you have a stateless datastructure receiving messages inside a task, it’s time to use gen_statem. If you prefer a genserver like interface and enforced state graph on top of gen_statem, I wrote StateServer library for this purpose.

Otoh, if you’re doing this for fun and exploration of otp I highly encourage it!!

2 Likes

This will be a production program using this, it’s not an overly complicated project but I do have some IPC going on. Mainly, my Tasks right now are talking to a GenServer and Agent to get stored entries and run some functions in a loop. I figured since I’m not storing any state with these modules, they would make the most sense being a Task. The getting started examples seem to promote that idea as well for my same use case

I guess I don’t really need to see that my Task is running, since I created my module with use Task, restart: :permanent, I should just assume it is working. Right now I’m just finishing my biggish project with Elixir so I like to remote in and verify things are working in addition to what goes in the logs :slight_smile:

@ityonemo’s mention of Process.register/2 looks like it’s the best answer for me right now. I’ll have to see what exactly gen_statem is and how it would work with this. I’m happy that right now my project is running well, but I also like to be correct, so thank you for mentioning that!

use Task, restart: :permanent

seems not right, IMO. I don’t think statefulness/not statefulness is the right way to think about whether or not you should use a GenServer vs. a Task. There are times where I have GenServer equivalents with state nil.

I would say a GenServer is supposed to be a service, and a Task is supposed to be an action. If you speak a romance language, GenServers are imperfective and Tasks are perfective. Tasks IIRC are a creation of Elixir to give OTP a sane default way to supervise simple things with strictly internal control flow, but as soon as you’re doing message passing to interface with a task, you’re in GenServer territory.

If you are having a Task that is permanent, it must somehow be responding to a message (probably a receive block?). Although that might work for now, I would suggest changing that over to a GenServer call. If for no other reason than the caller will know if the somehow the GenServer is unavailable. Even if all your call does is reply :ok and performs a {:continue, info} to actually do its thing. That will let the GenServer manage your message queue instead of doing it manually. This is good because you’ll get solid error warnings (the calling GenServer will crash), and you’ll also get backpressure management.

If your needed throughput for your service starts go get really high, you’re going to want to do process pools, and buliding out a process pool from a GenServer is a well-trodden, battle-tested, and relatively formulaic process; doing so with a Task I suspect could be challenging.

1 Like

I have basically been spawning this task to call a GenServer, which is why it didn’t make sense to use another GenServer. This Task doesn’t do any receives, it only makes calls and then does something with the data that is returned with the call. I did repurpose my task into a GenServer, and though the behavior is no different at all, except that I do loops with :timer.send_interval/2 to trigger a call to handle_info/2, it has allowed me to handle a couple casts that could come in handy at certain times.

It also allows use of the :sys module to do things like tracing or other debugging, so it’s well worth it for long running things.

1 Like

Sorry for digging up this old question. I’m facing a use case that I need a globally named process that works almost like a GenServer, but needs to do something after 5 seconds without receiving any message, then continue receiving messages. I don’t know how to do that with a GenServer. Any help would be welcome. Thanks in advance.

https://www.erlang.org/doc/system/statem

Genstatem was mentioned above and I think meets your requirements. I guess need to clarify, the process needs to listen for messages and if it goes 5 seconds without receiving one it performs some action? Or does it perform some action ever 5 seconds regardless of messages received?

Not every 5 seconds, but after 5 seconds when no messages come.

As stated, :gen_statem has a concept of event timeouts, where the timeout will be automatically cancelled if an event is received before the timeout expires. You create an event timeout by returning an “event timeout” action from a callback

{state, data, [:timer.seconds(5)]}
# or
{state, data, [{:timeout, :timer.seconds(5), value_to_include_in_callback}]}

Alternatively, if you want control over when the timeout is cancelled, you can use a generic timeout. As long as you keep returning the same generic timeout name from callbacks, then the timeout will keep being reset (or you can explicitly cancel it), otherwise the timeout expires and the process will receive a message about it.

{state, data, [{{:timeout, :my_name}, :timer.seconds(5), value_to_include}]}

GenServer also has builtin timeout support that can be used to detect inactivity:

def handle_info(:timeout, state) do
  dbg("5 seconds of inactivity have elapsed")
  {:noreply, state}
end

def handle_info(_, state) do
  {:noreply, state, :timer.seconds(5)}
end

And, if you want to be in control over when the timeout is cancelled, you can do so with Process.send_after/4 and Process.cancel_timer/2:

def handle_info(:timeout, state) do
  dbg("5 seconds of inactivity have elapsed")
  {:noreply, state}
end

def handle_info(_, state) do
  if state.timer, do: Process.cancel_timer(state.timer)
  state = put_in(state.timer, Process.send_after(self(), :timeout, :timer.seconds(5)))
  {:noreply, state}
end
1 Like

Thank you so much! I didn’t know there can be a timeout() in the return values of init and handle_*. That really helps.