In step 4 one process served every bucket. Now each
bucket is its own process: a crash in one bucket
can't touch the others, and buckets work in parallel.
Buckets are created on demand under a
DynamicSupervisor, and found by name in a Registry.
Crash reports contain pids that change every run, so
we silence the logger for this demo.
1
Logger.configure(level::none)defmoduleKV.BucketdouseAgent,restart::permanentdefstart_link(name),do:Agent.start_link(fn->%{}end,name:via(name))defvia(name),do:{:via,Registry,{KV.Registry,name}}defput(name,k,v),do:Agent.update(via(name),&Map.put(&1,k,v))defget(name,k),do:Agent.get(via(name),&Map.get(&1,k))enddefmoduleKVdo# Look the bucket up; start it if it doesn't exist.# If two callers race to create the same bucket, the# Registry lets only one win and the other gets# `:already_started`, which is fine.defbucket(name)docaseDynamicSupervisor.start_child(KV.BucketSup,{KV.Bucket,name})do{:ok,_}->name{:error,{:already_started,_}}->nameendendend
The tree: a Registry first (buckets need it to
register), then the DynamicSupervisor that owns the
buckets. Order matters: children start top to bottom.
The supervisor restarts it under the same name, but
with empty state. "users" never noticed.
4
_=DynamicSupervisor.which_children(KV.BucketSup)IO.inspect(KV.Bucket.get("carts","ada"),label:"carts/ada after crash")IO.inspect(KV.Bucket.get("users","ada"),label:"users/ada untouched")
carts/ada after crash: nil
users/ada untouched: :admin
Isolation worked, but every read still queues
behind writes in the bucket's mailbox. Step 6 keeps
writes in a process but moves reads to ETS, so
readers never wait. (Surviving crashes with data
intact needs persistence or replication; that comes
with the Distributed track.)