markmark206

markmark206

Doing something every few hours, the simplest approach?

I would like to perform an action periodically, and I am looking for the simplest reasonable way to do this.

As an example, let’s say I want to delete old entries from a db table, every few hours. The action is idempotent, relatively inexpensive, precision “doesn’t matter,” and concurrent executions of multiple runs (e.g. from multiple replicas of the service) is not a problem (db transactions will handle them safely).

A commonly recommended approach for doing this seems to be a variation of using a GenServer with send_after (or, I suppose, spinning up an oban job;).

This makes a lot of sense, but I coded up just running an infinite supervised “do”+“sleep” recursion, and it seems to work, and it seems ridiculously concise and simple, and I can’t explain to myself why that wouldn’t be enough.

Is there any reason why I shouldn’t do this? ; )

If you have any thoughts / guidance on this, I would much appreciate them!

Thank you!

PS An example of what this might look like in code:

application.ex (starts the task, restart: :permanent):

defmodule MyApp.Application do
    def start(_type, _args) do
        children = [
           ...,
           Supervisor.child_spec(
               {Task, fn -> MyApp.Sweeper.delete_old_data(sleep_ms) end},
               restart: :permanent
           ), 
           ...
        ]
        Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
    end
end

where delete_old_data() just keeps doing the thing and sleeping, forever:

defmodule MyApp.Sweeper do
    def delete_old_data(wait_ms) do
        ... delete old things ...
        Process.sleep(wait_ms)
        delete_old_data(wait_ms)
    end
end

Most Liked

sasajuric

sasajuric

Author of Elixir In Action

If you want the shortest possible out-of-the-box solution with no external dependency, there’s :timer.apply_interval.

Such solution should be no worse than a custom GenServer +send_after.

My preferred approach though is a GenServer which starts the task as a child process. It also has to be one GenServer per each periodic job, so I can inject each job in the proper place in a supervision tree. This is a big reason why I don’t like and avoid quantum & similar libs.

Instead I wrote my own periodic abstraction which follows the principles outlined above. I blogged a bit about it here.

kwando

kwando

It certainly works like you did but I think it is but I is more idiomatic to keep the details out of the application file. The very least you could do is to put the child_spec/1 inside the MyApp.Sweeper module…

defmodule MyApp.Application do
    def start(_type, _args) do
        children = [
           ...,
            MyApp.Sweeper, 
           ...
        ]
        Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
    end
end

defmodule MyApp.Sweeper do
  def child_spec(_) do
     Supervisor.child_spec(
       {Task, fn -> MyApp.Sweeper.delete_old_data(sleep_ms) end},
         restart: :permanent
       )
   end
end

but really I think a standard GenServer + send_after / :timer.send_interval is the way to go for this… easier to read and understand :slight_smile:

defmodule MyApp.Sweeper do
   use GenServer
   def init([]) do
     :timer.send_interval(self(), :timer.hours(5), :delete_old_data)
     {:ok, []}
   end

  def handle_info(:delete_old_data, state) do
     # delete old data here, or spawn a Task doing it to keep the Sweeper responsive
   {:noreply, state}
  end

  def start_link([]) do
    GenServer.start_link(__MODULE__, [])
  end
end
dimitarvp

dimitarvp

Seconding @sasajuric and @BartOtten here, just roll your own GenServer per task – it’s just 10-20 coding lines of boilerplate maximum.

Or use Periodic. It’s a very small and functional thing (last I used it at least, which was like 3 years ago). Or you can rip out the code you need from it because again, it’s very small and works fine.

I would make a few GenServers though. It’s a one-time investment that can pay huge dividends. If you want to get slightly fancy maybe you can store “when was the last time task X ran” in a small file; though you mentioned that it’s not critical if a task gets executed a bit more rarely every now and then (when the app is rebooted) so if that’s indeed the case then it’s probably best to not bother with keeping state outside of memory.

Where Next?

Popular in Questions Top

siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID<0.412.0> terminating ** (Postgrex.Error) FATAL...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
vac
Hi, I’m quite new in Elixir and I’m trying to format a string to a PEM format. I have the certificate value like MIIDBTCCAe2...... and I...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
lucidguppy
I have a super simple question about elixir - how would I take a file like this foo bar baz and output a new file that enumerates th...
New
srinivasu
How to handle excepions in elixir? Suppose i have A, B, C ,D, E modules. and each module has get() function. A.get() method will call t...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 records...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New

Other popular topics Top

marius95
Hello everyone, I try to use an Javascript Event Handler in my root.html.leex file. Therefore I created a function in the app.js file: ...
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID<0.412.0> terminating ** (Postgrex.Error) FATAL...
New
aesmail
Hello guys, I have finally made it. I created an admin interface for a framework. It’s been on my todo list for years and with the curre...
New
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list. ...
New

We're in Beta

About us Mission Statement