Advice needed on Genservers vs ETS tables for global state in Phoenix

Hi, I’m looking for some advice on making data available to all my LiveComponents without passing it as a prop.

I was originally looking at doing this with a simple Genserver, but from the docs, it looks like Genservers run in a single thread so this could be a bottleneck under load if all requests need to be serialised?

I’m also looking at ETS tables but I can’t find much talk of this anywhere in relation to Phoenix, so I’m thinking it might not be a great approach?

Am I overthinking this? Is a Genserver a reasonable approach?
Is it an anti-pattern to just pull data from an ETS table where ever I need it?

In a perfect world, I’d assign data in the LiveView mount and have it magically appear in the assigns of every component’s update method, but I can’t seem to find a clean way to do this.

Thanks for any advice.

2 Likes

Is the data that you want to be globally accessible going to change? Or is it immutable constants?

It’ll change. A component could grab in an event handler, change it, update it, then add the result back to the socket assigns for re-rendering.

You need parallel access, you reach for ETS. There’s no anti-pattern here, people use ETS in Phoenix projects all the time.

2 Likes

I’m no Elixir pro but I’m pretty sure if you just use ETS directly you could have race conditions. If you grab value, change it and put it back something could have change value in ETS between those two calls. So when designing your system make sure to take it into account.

If you were to use ETS I think you’d need to regularly poll it for updates.

An alternative is to broadcast changes to all LiveViews. For example, you could start a new pg group. A LiveView could register itsself in the group in mount, and the LiveView could implement the handle_call callback. This callback could update the socket assigns. A LiveComponent can loop through all pids in the pg group and call GenServer.call on all the LivewView pids with the new data. This could be too chatty though, might not be great if the value is very frequently updated and if you have many LiveViews.

1 Like

Is there any particular reason to use pg over phoenix pubsub? The latter is probably more familiar to most people here and scales better.

I’m curious how PubSub would scale better? And fair point, I use Erlang more than Elixir.

are you looking to share it between LiveComponents of a single connection? Or LiveComponents across multiple connections? If you are sharing LiveComponents in multiple connections, be careful, as the clustered node that any given connection can “live on” may change (suppose a backhoe digs up some fiber on the way to some datacenter and triggers a TCP reconnect) and ETS tables are local to a single node. In the former case, don’t forget to tag the ETS table with some connection identifier, etc… It could get hairy, because you’ll want to automatically evict those items when the connection dies… – use props if you can.

2 Likes

With Phoenix PubSub the registration and actual broadcasts happen locally, and only the PubSub instances are members of pg. When all processes are members of pg both registration (each process needs to register with multiple nodes) and broadcasts (each message needs to be sent to each member, likely resulting in duplicate messages sent between nodes) become costlier. I’ll prepare and post some basic benchmarks from a few t4g instances tomorrow (I’ve been in a process of benchmarking different approaches, it’s a good idea to add pg to them).

1 Like

Interesting, thanks for the explanation.

I’ll prepare and post some basic benchmarks from a few t4g instances tomorrow

It has everything needed to start up the same infra (which is a vpc, two ec2 instances with public ips, a few security groups, and an ecs service) in terraform/ folder. But before that, .envrc or similar file needs to be created to export some necessary env vars (aws keys, ssh key name). After that terraform apply should just work.

1 Like