Add a scope option to share a store between sessions - #14
Open
drozdzynski wants to merge 1 commit into
Open
drozdzynski wants to merge 1 commit into
drozdzynski wants to merge 1 commit into
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Storex has always started one store process per
{session, store}pair, so twoconnections to the same store module hold two independent states. Anything where
the state is the shared thing — a chat room, a collaborative document, a
dashboard several people watch — could not be expressed. The workaround was an
external source of truth plus
Storex.mutate/3, which fans out over:pgandre-runs
mutation/5once per session, so the callback had to be written tosurvive running N times against N independent copies of the same state.
use Storex.Storenow takes a:scope:A scope decides the identity a store process registers under.
Storex.Store.scope_id/3derives it andStorex.Supervisor.name/2takes thatscope id where it used to take a session. For
:sessionthe scope id is thesession, so stores that do not pass the option resolve to exactly the processes
and names they did before.
Under a shared scope several sessions call into one process. A mutation is
applied once, the diff is computed once, and it reaches every attached session:
the one that issued it as the reply to its own request, the rest as a push. The
push is a new
{:storex_diff, ...}message, encoded byStorex.Socket.diff_handle/3and sent by both handlers. The fan-out isgenerated away for
:sessionscope rather than branched on at runtime, so theexisting path pays nothing for it.
What sharing a process changes about the callbacks
init/2runs once, for the first session to attach. The params of everysession that attaches later are ignored.
mutation/5receives the session that issued the mutation rather than theone
init/2ran with, so a shared store can tell its clients apart. Under:sessionscope those are the same value.terminate/3runs when the last session detaches.Storex.Supervisor.remove_store/2reference counts to make that true: theregistry row is deleted first, then the process is stopped only if no session
is left.
Attaching instead of starting
Storex.Supervisor.add_store/4looks the scope up inStorex.StoreRegistrybefore starting anything. Relying on
{:error, {:already_started, _}}alonewould run the user's
init/2, side effects included, and then throw the resultaway. The lookup narrows that to a genuine race between two sessions attaching
at the same moment, which the
:already_startedbranch still covers. The key anattaching session reports comes from the process itself, through a new
:get_keycall, because the registry row of the session that started it may notbe written yet.
Broadcast dispatch
Storex.PGnow dispatches a broadcast once per store process instead of onceper registry row. Without that a shared store would run a
Storex.mutate/3mutation once per attached session against the state they all share. Nothing
changes for
:sessionscoped stores, where every row already has a process ofits own.
The registry table is unchanged
A shared store is several rows pointing at one
store_pid, so none of the matchpatterns in
registry.ex,pg.exand the handlers had to move.Storex.Registry.store_sessions/1is the one new read, and it backs both thefan-out and the reference count.
The frontend needs no change
The client already falls through to its mutation listeners for a
mutationframe whose
requestmatches no pending promise, and filters on store andsession; the pushed frame carries the receiving session's own id, so it matches.
No rebuilt bundles in
priv/static.Limitations
:globalis per node, not per cluster.Storex.StoreRegistryis node-local andStorex.mutate/3broadcasts to every node, so a:globalstore takes a mutationonce per node, against that node's own copy of the state. Cluster-wide
singletons need
:global, Horde or an owning-node rule, and are not attemptedhere.
Storex.Store.scope/1checksCode.ensure_loaded?/1beforefunction_exported?/3. Asking the latter alone answersfalsefor a module thatis not loaded yet — the same trap
__terminate__/4fell into — and every storethen silently degrades to
:sessionscope depending on what the code serverhappens to hold.
Tests
test/storex/scope_test.exscovers scope resolution, sharing, isolation betweenkeys, the diff fan-out, the reference count and the
Storex.mutate/3dedupe.Two end-to-end tests in
test/storex/handler/plug_test.exsdrive two realsockets through a shared store. 136 non-browser tests pass on three seeds;
mix compile --warnings-as-errorsandmix format --check-formattedare clean.The 21 browser tests still need chromedriver and did not run here.