lud

lud

Architecture for updating multiple states in isolation with implicit locking

Sorry for the title, I did not know what to write.

Hello,

I’m trying to build an architecture for a game. In the game, there are
starbases which are basically event processors: they have a state,
receive different events and update their state accordingly.

I do not want to have a process per starbase because it will be many
of them, and they are idle most of the time. For example, they have
production lines: when a production of food is started, I calculate
the time of the production end, and I will send an event to the
starbase at this time. Durations can be in minutes so I have no use
of idling processes for several minutes. Also states take memory.

However I want all events for a given starbase to happen sequentially,
because I will load the state from database, handle the event, and
write the state.

So, what I plan to do is to have a worker pool and use
erlang:phash2/2 to hash the id of my starbase and send all events
for a given starbase to the same worker. This should be enough to
prevent concurrent read/write of the same state and work sequentially.

(A small word on distribution : if I have to work on different nodes,
those would represent solar systems, or solar system clusters, so a
given starbase will always be handled on the same node.)

If you think this is a bad design, i think you can skip the rest of
this post, and please tell my why.

Now, I have to update several starbase at a time, for example to
conclude a trade. Starbases declare what they need to but and what
they want to sell. I have some code running to figure out matching
orders. When I match is found, I want to run a task on a worker to
load both states, check if orders are still matching, update states
(mark orders as fullfilled, exchange money, move goods from the
“selling” cargo to the “sold-and-waiting-to-be-carried” cargo space).

The problem is that this task will run on the worker for starbase A,
and starbase B could be concurrently updated with another event that
will consume goods in cargo.

I am looking for a solution that could handle tasks for any amount of
starbases, not just 2.

So this is my current plan:

  1. Hash all the keys (starbase ids) to find the workers. If all keys
    give the same worker, just run the task as a single-key task.
  2. With multiple workers, fetch the pid of the workers, take one pid
    as :master and other pid(s) as :slave.
  3. Run an inner task on the master worker: pass the slave keys
    (starbase ids) to it, then the worker will wait for an ack message
    with those keys and the slave pids from all slaves.
  4. Run an inner task on each slave worker: pass the master pid, the
    worker will send the ack message with the slave key to it. Then,
    wait for a :finished message. All messages will be tagged with
    the same ref to identify the event and have selective receives.
  5. At this point, states cannot be changed by other events.
  6. When the master received all acknowledgements from the slaves, run
    the actual task (i.e. load states, handle the event, write states),
    then send the :finished message to all slave pids.

It seems a bit complicated, no ? The main problem is to make workers wait doing nothing to provide locking.

I have another approach with a mutex I wrote that can lock multiple
keys without deadlocks, but that would require to lock the keys for
all tasks, even the single-key ones, so a lot more message passing and
a bottleneck on the mutex. I expect to have way more events for a
single state than events involving multiple entities.

Most Liked

al2o3cr

al2o3cr

IMO worrying about this is premature; there are a lot of other problems you’ll need to solve before this particular one needs optimization.

What you’re describing with ack and finished messages sounds a lot like two-phase commit; there’s a substantial literature describing different approaches to solving that coordination problem.

The theme of your game has me wondering: should these interactions be instantaneous? The speed of light is already an issue in modern distributed systems, and interstellar distances (even with silly-fast FTL communication) would make it even more so.

Another thought: some real-world transactions are coordinated through escrow services. What about setting up something like that? A possible process:

  • party A wants to buy 1000 widgets from party B for a total of $10000
  • party A transfers the $10000 to the escrow service
  • party B transfers the 1000 widgets to the escrow service
  • the escrow service releases the money and the widgets

In a system context like this, you’d typically have the “transfers” above include a timeout so that if the escrow doesn’t complete the resources eventually revert to their original owners.

One note: the name escrow service shouldn’t be read as implying there’s one process doing this. It would seem simpler to spin up a new process per escrow interaction, to keep the state machine straightforward.

Fl4m3Ph03n1x

Fl4m3Ph03n1x

In Elixir you can have thousands and even millions of processes in a machine, BEAM is highly optimized and the amount of resources they cost is minimal.

As for state taking up memory, why would this be an issue? Your application needs state somehow, you will have to save it somewhere.

Ahh, cunning solution. I like it :smiley:


So, from what I can understand, your problem is that when you make a transaction, between several nodes, that transaction needs to be atomic in the sense that no other operations can take place while the transaction is happening so you can avoid inconsistent states.

This is a tough problem, but one with many solutions. One of way avoiding state on workers would be to have your state on an ETS table, and then have a mediator that changes and updates state for all processes. This mediator would eventually become a bottleneck but it would also serve as a point of synchronization in your system. (Regarding bottlenecks, always benchmark first! A mediator will only be a bottleneck if it handles more messages than it can get!)

Another possible solution would be the use of optimistic locks. Locking in DBs is a world on it’s own, but I find that optimistic locks, besides having great performance, do work in a lot of cases when you have few collisions:

https://docs.jboss.org/jbossas/docs/Server_Configuration_Guide/4/html/TransactionJTA_Overview-Pessimistic_and_optimistic_locking.html

Aside from that, the only remaining solution I can think of is defining a worker communication protocol, which you already have defined and explained in your post.

Anyway, hope it helps somehow!

lud

lud

Hi,

In Elixir you can have thousands and even millions of processes […] Your application needs state somehow, you will have to save it somewhere.

Thank you for your answer. I know that I can have more processes than I would ever need, but yes the main problem with that is memory. It is a game so state can be pretty complex, and I do not want to have thousands of starbases state idling for nothing. With this architecture I can keep a low memory profile. Actually I could have processes that hold state for faster read/writes (instead of database/disk) and shutdown with a timeout ; wether the state is written to a transient GenServer with timeout / an EST table or directly do disk (I’m going to start with that) is another question I believe, the main problem beeing queuing updates and multi-state transactions.

Another thing with processes is errors handling. If I want to act on two states and rollback on errors, my state-holding-update-handling processes will need a way to cancel the last update, or at least a savepoint/commint mechanism. Whereas handling the two states update in a single process and rolling back is just a matter of not saving the new states.

So, from what I can understand, your problem is that when you make a transaction, between several nodes, that transaction needs to be atomic in the sense that no other operations can take place while the transaction is happening so you can avoid inconsistent states.

Not nodes, just states. Everything that I describe will happen on a single node. But yes, it is a transaction/concurrency problem. A mediator would be kind of equal to have only one worker. It is simpler, that is true, but I like the idea of having concurrency there. Maybe that is my mistake.

Optimistic locks are interesting and may be applicable in my case, I will research on that point.

Thank you very much.

dimitarvp

dimitarvp

Sorry I can’t be more useful but just as a quick pointer, this latest ElixirRadar article was extremely interesting:

I know you’re not doing Phoenix in this case (or I think so) but Horde and DeltaCrdt are amazing and under-appreciated libraries doing a good chunk of what you intend to do.

Where Next?

Popular in Questions Top

JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
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
polypush135
As many of you may have realized by now (sorry for all the posts here) I’ve been working on a db problem where I’m trying to aggregate a ...
New
sacepums
Hey guys. I'm new to elixir and im really stocked about it. But I ran into a bit of problem - I need to convert a date sting, for examp...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
mgjohns61585
Could someone help me? I'm making my first elixir program, number guessing game. I can't figure out how to convert the user's guess from ...
New
chewm
Hi guys, nice to meet you to the whole forum, I’m new here, I’m trying to configure visual studio code for elixir, right now the intellis...
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
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers' Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New

Other popular topics Top

JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1140 51847 244
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
Tee
can someone please explain to me how Enum.reduce works with maps
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 45766 226
New
grych
Hi folks, Few months ago I have announced the proof-of-concept of the library to manipulate the browsers DOM objects directly from Elixi...
639 49522 488
New
script
If I have a string “1000 cfu/ml” . I want to remove the characters and / and space . So the string is like this "1000" What is the ...
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
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
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New

We're in Beta

About us Mission Statement