thiagomajesk

thiagomajesk

PartitionSupervisor architecture/ optimizations

Hey folks! :waving_hand:

I think I have an interesting optimization challenge, let me know what you think…

A few months ago, I created my own ECS (Entity Component System) implementation based on a different interpretation of the pattern (because why not!?). Here’s the link to the repo in case you are curious. It’s ok if you haven’t heard of the ECS pattern before. I will try to describe the problem more generically:

I have a list of “objects” (which are just numeric identifiers), and those “objects” have “behaviors” attached to them. The only thing that those “behaviors” do is handle events sent to “objects” they are attached to. A really basic example

defmodule Ping do
  def handle_event(:play, object, _args), 
    do: IO.puts("Ping for #{object}")
end

Those “behaviors” are just a module that implements a handle_event callback, not a GenServer. To make things easier to visualize, imagine you have two “behaviors”: Ping and Pong:

object1 = System.unique_integer([:positive])  # 1314
object2 = System.unique_integer([:positive])  # 1416

Ping.attach(object1)
Pong.attach(object1)

Ping.attach(object2)
Pong.attach(object2)

World.send(object1, :play) 
# Ping for 1314
# Pong for 1314

World.send(object2, :play) 
# Ping for 1416
# Pong for 1416

There are a few interesting properties about this problem:

  • A “behavior” can also send additional events

  • The handling event logic is synchronous and can potentially block.

  • Events sent to the same “object” should always be processed in order:

    Enum.each([Ping, Pong, Other], &(&1.handle_event(object, event, args))
    
  • If you send another event to the same “object”, it should be queued up and processed after the current event finishes being processed by all its “behaviors”:

    (object1) :play -> [Ping] -> [Pong] -> :forfit -> [Other]
    
  • Events sent to different “objects” can be parallelized as they don’t affect each other:

    (object1) :play   -> [Ping] -> [Pong]
    (object2) :play   -> [Ping] -> [Pong]
    (object3) :forfit -> [Other]
    

The current solution: There’s a single GenServer called World that is responsible for dispatching messages to registered objects. This GenServer uses a PartitionSupervisor, which ensures that “objects” are correctly “sharded”, causing events for the same object to always be processed sequentially in the same places, while events sent to other “objects” are processed in parallel:

[World] 
| ---(send :play)-----> [Router 1] (object1, Ping -> Pong)
|----(send :play)-----> [Router 2] (object2, Ping -> Pong)
|----(send :forfit)---> [Router 3] (object3, Ping -> Pong -> Other)

The challenge with the current solution is that we only have a fixed number of partitions, and those processes can quickly become a bottleneck if, let’s say, you have something like multiple LiveViews sending messages at the same time, which increases the load, the process reductions increase, and they slow to a halt. I could potentially work on resizing the partitions, but that would require some additional logic to check when those processed are being overwhelmed (which might be too late).

I briefly explored GenStage, which would change the topology to something like:

[World] 
| ---(send :play)-----> [Ping] -> [Pong] -> [Other]
|----(send :play)-----> [Ping] -> [Pong] -> [Other]
|----(send :forfit)---> [Ping] -> [Pong] -> [Other]

This problem almost looks like the perfect use case for GenStage; however, there’s one additional aspect of this problem that makes things a little trickier: “behaviors” and events are completely dynamic! They can be attached/ removed from an object at any point in time, and this affects which stages need to execute.

So, even though the order of the stages is always consistent, not all objects need to go through all the stages, for instance, “object1” might need ot handle: Ping → Pong → Other, while “object2” needs to handle just: Pong → Other or even Ping → Other.

I haven’t given up on GenStage, yet, but I still have to figure out if a demand of 1 makes sense to keep an influx of events, and how to properly handle partitions (GenStage.PartitionDispatcher perhaps) that can grow/ shrink.


To finish this up, I think an ideal solution (in my mind at least) would be something like a dynamic pool of partitions that grow to fit the demand and shrink when there’s a lull in events. So:

  1. An object gets a new event
  2. Checks if there’s any available process to handle the event for this object
    2.1. If yes, send the event to be processed (potentially blocking)
    2.2 If not, spawns a new process and registers that process as the object’s “handler”
  3. Processes periodically send their queue length somewhere so we can decide if the demand is too high and needs the pool to grow or shrink (because we need to ensure that processes are always processed by the same partition, we might need some logic to shed/ redirect messages between partitions if one particular object it taking too long).

This is all theoretical; I haven’t tried it yet. So, what are your thoughts? How would you solve this?

Where Next?

Popular in Discussions Top

bartblast
With the core component system and HTTP/WebSocket infrastructure solid, it’s time to tackle Pub/Sub support. What We Have vs What’s Miss...
New
artimath
I think I’ve tried 5 different graph database libraries in the last two days and not a single one has been able to connect to a remote/lo...
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
szsoppa
Hey folks! For the third year in a row, we’re running the Elixir Survey by Curiosum! It’s been live since yesterday, and 300+ people hav...
New
AstonJ
@Garrison’s comment in another thread reminded me of this post by Joe: With the big five exerting more control than ever, new (AI) play...
New
jonator
FLAME is basically the dream for running AI agents that need system command access such as coding agents. However, I have concerns about ...
New
axelson
Hi there! :wave: @frigidcode and I (but mostly him) have been running an Elixir Book club, we’re almost done with Designing Elixir Syste...
New
stevensonmt
Has anyone else ever wanted to merge two maps, but have the resulting map only include keys common to both maps? I think of it as analogo...
New
paulanthonywilson
I like Umbrella projects and pretty much always use them for personal Elixir stuff, especially Nerves things. But I don’t think this is ...
New
isaacsanders
When I try to run my applications and I haven’t updated the dependencies, the output tells me to run mix deps.get. Why doesn’t the binar...
New

Other popular topics Top

Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
peerreynders
Manning 2016 Halloween weekend sale via Deal of the Day Friday, October 28 - Half off all MEAPs - code WM102816LT Saturday, October 29 ...
326 29600 154
New
dotdotdotPaul
Okay, I'm having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I'm sure I'...
New
yawaramin
In the Dialyzer docs ( http://erlang.org/doc/man/dialyzer.html#requesting-or-suppressing-warnings-in-source-files ), there is a way to tu...
New
ycv005
I have followed this StackOverflow post to install the specific version of Erlang. And When I am running mix ecto.setup then getting fol...
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod -- where is this set? Thanks.
New
msaraiva
Surface is an experimental library built on top of Phoenix LiveView and its new LiveComponent API that aims to provide a more declarative...
564 42633 214
New
AstonJ
by Lance Halvorsen Elixir and Phoenix are generating tremendous excitement as an unbeatable platform for building modern web application...
460 27162 124
New
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

We're in Beta

About us Mission Statement