darkmarmot

darkmarmot

I have many actor processes in my application with event counts associated with each actor.

I’d like to have a live sum of the total actor events across the node (basically map-reduce across the processes).

I was thinking that I could register each process in the Registry with a count – and store a separate sum in the Registry that gets incremented with new events and decremented when actors crash (using trap_exit?)

Does that sound like an appropriate solution in Elixir or is there a more canonical OTP way to handle this?

Thanks,
Scott S.

Showing Posts 1 to 10

christhekeele

christhekeele

Hey Scott! I’d probably use ETS for this, that should get you started down a more idiomatic path.

dimitarvp

dimitarvp

ETS has a function for atomically increasing a single number variable.

Called from Elixir like this: :ets.update_counter.

darkmarmot

darkmarmot OP

Thanks, update_counter looks good!

Should I make a genserver that monitors every actor to decrement counts when they crash?
Or is there a better way for handing that part?

christhekeele

christhekeele

Depending on if your workers implement OTP behaviours, they should have terminate call backs you could decrement from.

cmkarlsson

cmkarlsson

With the warning that the terminate callback is not always called and cannot be relied upon to run.

sasajuric

sasajuric

Author of Elixir In Action

+1 that terminate callback is not the appropriate place to cleanup counters of processes which are terminated. You need another GenServer to monitor these processes and perform the cleanup.

Instead of rolling your own, you could use gproc aggregated counters.

To make that work, you need to register the aggregated counter, e.g. in your application start callback, or in some top-level process:

:gproc.add_local_aggr_counter(:my_counter)

Now, in every process, you initialize the local counter when the process starts:

# invoke in each actor process
:gproc.add_local_counter(:my_counter, 0)

Where 0 is the initial count for that process.

When you want to change the counter value, you can use update_counter:

# invoke in each actor process
:gproc.update_counter({:c, :l, :my_counter}, increment)

The :c and :l indicate that you’re updating a local counter which is tied to the current process. If the process terminates, its count will be removed from the aggregated count.

To get the aggregate value (sum of all counters), you need to invoke:

:gproc.lookup_local_aggr_counter(:my_counter)

Demo:

:gproc.add_local_aggr_counter(:my_counter)

:gproc.lookup_local_aggr_counter(:my_counter)
# 0

# start one agent and bump its count by 1
{:ok, agent1} = Agent.start_link(fn -> :gproc.add_local_counter(:my_counter, 0) end)
Agent.update(agent1, fn _ -> :gproc.update_counter({:c, :l, :my_counter}, 1) end)

# The aggregated count is now 1
:gproc.lookup_local_aggr_counter(:my_counter)
# 1

# start another agent and bump its count by 2
{:ok, agent2} = Agent.start_link(fn -> :gproc.add_local_counter(:my_counter, 0) end)
Agent.update(agent2, fn _ -> :gproc.update_counter({:c, :l, :my_counter}, 2) end)

# the aggregated count is now 3 (1 from agent1 and 2 from agent2)
:gproc.lookup_local_aggr_counter(:my_counter)
# 3

# stop agent2
Agent.stop(agent2)

# The aggregated count is now 1 (1 from agent1)
:gproc.lookup_local_aggr_counter(:my_counter)
amnu3387

amnu3387

But is there any way you can decide if using terminate’s callback is appropriate or not? Like, if it’s a clean exit, it will always be called, if it’s a crash it might or might not, has it to do if you’re using distributed erlang, what are the guidelines to decide if using terminate is an appropriate decision or not?

For instance, I have a genserver responsible for processing files, assuming it doesn’t crash, is it ok to, for instance, remove the file from the terminate callback?

It just seems that if you can “never” rely on it, under any circumstances that it doesn’t make sense to even exist? Or am I missing something about its implementation and use cases?

sasajuric

sasajuric

Author of Elixir In Action

This is a very good question. I personally mostly avoid terminate, because it won’t be invoked if the process crashes, or if it’s forcefully terminated (killed) from the outside. Thus, if some cleanup code must be executed, I prefer having another process to do it.

However, using a cleanup process isn’t synchronous, since the cleanup code will run after the “main” process has terminated. Therefore, there are some special cases where terminate works better. For example, supervisor terminates children from the terminate callback. This ensures that when the supervisor goes down, its complete subtree is already down. I can’t think of a way to ensure such synchronism by using a separate cleanup process.

However, this approach suffers from potential theoretical issues. If a supervisor process is brutally killed or if it crashes, then this guarantee doesn’t hold. The child processes will still be taken down eventually, but not immediately. If some descendant is trapping exits and ends up in an infinite loop, it might never happen. This in turn could prevent the restart of the crashed supervisor, which could take down the entire system, or it could lead to duplicate processes running, which could cause some strange behaviour of the system.

However, we can assume that the supervisor process is thoroughly tested and hopefully free from unexpected crashes. In addition, in a properly constructed supervision tree, a supervisor is never brutally killed (because :shutdown of a supervisor is :infinity by default), so I’d say that these issues are theoretical, and very unlikely to occur in practice.

So my take would be to use terminate to implement a synchronous cleanup (things are cleaned up before the process terminates). For example, I use it in Parent to terminate children, similarly to supervisor. In such cases, you probably want to keep the logic of the process simple, to reduce the chance of it crashing. You may also consider setting it’s shutdown option to :infinity to prevent its parent from brutally killing it.

If the synchronism is not required, and a cleanup code must be executed when a process goes down, regardless of how/why it goes down, I’d suggest using a separate process.

amnu3387

amnu3387

Thank you @sasajuric
So the takeaway is that usually creating the process (ie. genserver) and monitoring it from another process (ie. another genserver) is a more sturdy solution for effectively dealing with cleanups?
And the monitor would be part (usually) of the application root supervision since it would only deal with monitoring/cleanup, while the processes executing “work” would be part of their own subtree/supervisor?

I might be wrong, but I had the idea that while developing some genservers in the past that held “game state”, sometimes I would crash them while working on the code, and yet the terminate was still called?

sasajuric

sasajuric

Author of Elixir In Action

Yes, it’s a bit more nuanced. A crash in init/1 won’t lead to terminate being invoked, while an exception raised by handle_* will. However, a linked error (e.g. a child task crashes) won’t lead to terminate, because the exit signal will take the process down (unless it’s trapping exits). Moreover, if a parent supervisor decides to stop the server, terminate is invoked only if the server is trapping exits.

As you can see, there are all sorts of edge cases here, but the main point is that, no matter what you do, you can’t be completely sure that the termination logic is invoked, so if you want stronger guarantees, it’s IMO better to use a separate process. Of course, not even that will ensure that the cleanup code is invoked, e.g. if BEAM OS process is killed, or someone pulls the power plug :slight_smile:, but within the BEAM instance, you have more guarantees than when using terminate.

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
nseaSeb
Hello, I know there is an approach for handling lists that allows for optimized traversal, but I can’t recall the specific method (somet...
New
kpanic
Hi everyone, I am toying with the idea of building a “match maker” for giving personal help to people that wants to start coding. I sta...
New
brecabral
Documentation While reading the Scoped Routes section, I noticed that the documentation currently refers to a problem without explainin...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
asweet-confluent
I recently noticed that Elixir’s Logger defaults its primary log level to :debug when no :logger, :level application configuration is pre...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
mhanberg
Hi everyone! The first release candidate for the Expert language server project is now available! We’ve published a press release detai...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New

Latest on Elixir Forum

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews