As an exercise in OTP for myself & my team, I was building an in-memory user accounts system. I am roughly following @lance’s path laid out in his book.
Each Account
is a GenServer
. The primary reason behind this is to enforce that the operations and state transitions between “unregistered”, “unconfirmed”, “active”, “deactivated” states are wrapped in application-level transaction and are no race conditions such as 2 users registered with the same email because double form submit etc. This is enforced by simple state machine module that guards the transition rules.
What I am doing is that I start an AccountsSupervisor
with :simple_one_for_one
strategy. It is responsible for starting new Account
servers.
I want to be able to find each account either by ID, or by e-mail. Both are unique. So I created 2 unique registries: AccountsByEmail
and AccountnsById
. Whenever I start server for particular account, in it’s init
function it tries to register in both registries. If this fails at any point - this likely means server is already started for particular account. When I attempt to log user in, I use registry AccountsByEmail
, when I have account ID - the other one.
I have a few questions to the above:
-
Is my use of
Registry
as node-wide unique index good? I am especially concerned by the fact that I had to create 2 indices because I am looking theAccounts
up in 2 different ways. -
I am designing the persistence mechanism for this now. What I think is that along the
Account
, I will start anAccountDiskWriter
or something similar - another process that would be aGenServer
,accepting asynccasts
wheneverAccount
changes. It would serialize these changes and write to disk (most likely just insert to DETS). -
I need a way to bring everything up on the server start up. I was thinking about simply going through all my
Accounts
from DETS tables, and restoring it one by one on system start up. Does this make sense? -
Where do I load the saved state of the
GenServer
in case of restart by supervisor, or in case of 3)? I am having some trouble figuring out it. As far as I understand theinit
function is blocking the parentSupervisor
until it returns, so it’ll become a bottleneck if I put the disk reads there. As alternative, I could send myself a message from theinit
function, then handle it inhandle_cast
, where I would read the state from DETS, and only then register to the both of myRegistries
. I am interested, however, from my parent process (like web worker) to know if theAccount
was started & restored properly. So in such solution I no longer can rely on the returned value ofinit
function, as the account will likely to start properly always - it will fail later in it’s life cycle when it restores state. So I have to block waiting to see if it appeared in Registry, possibly with some timeout. Is this correct solution? I suspect there may be simpler one.