B2.1.6 · dive 6 of 6 · module B2.1
The property store, in full.
EchoData.Bcs.PropertyStore: start_link, the export list, init opening one :private :ordered_set, every ingress gated, page_desc walking the keyspace, placement pure. The system, entire.
The idea
echo_data · bcs/property_store.exEchoData.Bcs.PropertyStore is the whole system in one file. start_link/1 reads its name and namespace; init/1 proves the contract once more for this process, then opens a single :ets table as :ordered_set and :private; and the state is two fields, the namespace and the table. The export list — put, get, window, page_desc, record_entity, placement — is the boundary B2.1.1 described.
Every write and read crosses the gate first. Each handle_call and the one handle_cast ask Bcs.gate to admit the id’s namespace before touching the table, so an id of the wrong namespace is refused at the door, not stored and found wrong later. window/3 gates both bounds and runs an :ets.select over the half-open range; page_desc/2 walks descending from the table’s last key. One function stands apart: placement/1 is pure, parsing and hashing the id with no call to the process at all.
This is the file Codemojex’s rooms are built from. A room is a property store keyed by player and round ids, its writes gated by the room’s namespace, its recent history read straight off the ordered keyspace by page_desc — no timestamp column, because the id carries the time. Read it once and the rest of the core is variations on it: the CHAMP map, the edges store, and the gates are this same discipline, specialized.
The source
echo_data · committed, verbatimecho_data/lib/echo_data/bcs/property_store.ex — complete, 103 lines
defmodule EchoData.Bcs.PropertyStore do
@moduledoc """
A BCS system in skeleton: a GenServer owning one private ordered_set
property table keyed by the 14-byte branded string. Rung bcs1.1.
"""
use GenServer
alias EchoData.{BrandedId, Bcs}
def start_link(opts) do
name = Keyword.fetch!(opts, :name)
ns = Keyword.fetch!(opts, :namespace)
GenServer.start_link(__MODULE__, ns, name: name)
end
def put(store, id, value) when is_binary(id), do: GenServer.call(store, {:put, id, value})
def get(store, id) when is_binary(id), do: GenServer.call(store, {:get, id})
def page_desc(store, n) when is_integer(n) and n > 0,
do: GenServer.call(store, {:page_desc, n})
def record_entity(store, id) when is_binary(id), do: GenServer.cast(store, {:entity, id})
@doc """
Ascending ids in [lo, hi) — Chapter 1.5's synthetic cursors landed on the
ordered_set. Bounds are branded ids (synthetic via min_for or real) and are
gated like any ingress. Added by the Chapter 2.2 architecture review.
"""
def window(store, lo, hi) when is_binary(lo) and is_binary(hi),
do: GenServer.call(store, {:window, lo, hi})
@spec placement(binary()) :: {:ok, non_neg_integer()} | {:error, :invalid}
def placement(id) when is_binary(id) do
case BrandedId.parse(id) do
{:ok, _ns, snow} -> {:ok, BrandedId.hash32(snow)}
:error -> {:error, :invalid}
end
end
@impl true
def init(ns) do
{:ok, _mode} = BrandedId.self_check!()
table = :ets.new(:bcs_props, [:ordered_set, :private])
{:ok, %{ns: ns, table: table}}
end
@impl true
def handle_call({:put, id, value}, _from, s) do
case Bcs.gate(id, s.ns) do
{:ok, _snow} ->
:ets.insert(s.table, {id, value})
{:reply, :ok, s}
{:error, _} = err ->
{:reply, err, s}
end
end
def handle_call({:get, id}, _from, s) do
case Bcs.gate(id, s.ns) do
{:ok, _snow} ->
case :ets.lookup(s.table, id) do
[{^id, value}] -> {:reply, {:ok, value}, s}
[] -> {:reply, {:error, :not_found}, s}
end
{:error, _} = err ->
{:reply, err, s}
end
end
def handle_call({:window, lo, hi}, _from, s) do
with {:ok, _} <- EchoData.Bcs.gate(lo, s.ns),
{:ok, _} <- EchoData.Bcs.gate(hi, s.ns) do
spec = [{{:"$1", :_}, [{:>=, :"$1", {:const, lo}}, {:<, :"$1", {:const, hi}}], [:"$1"]}]
{:reply, {:ok, :ets.select(s.table, spec)}, s}
else
{:error, _} = err -> {:reply, err, s}
end
end
def handle_call({:page_desc, n}, _from, s) do
{:reply, {:ok, walk_desc(s.table, :ets.last(s.table), n)}, s}
end
@impl true
def handle_cast({:entity, id}, s) do
case Bcs.gate(id, s.ns) do
{:ok, _snow} -> :ets.insert(s.table, {id, true})
{:error, _} -> :ok
end
{:noreply, s}
end
defp walk_desc(_table, :"$end_of_table", _n), do: []
defp walk_desc(_table, _key, 0), do: []
defp walk_desc(table, key, n),
do: [key | walk_desc(table, :ets.prev(table, key), n - 1)]
endThe doors
the sibling coursesEchoMQ — the protocol, in depth
The bus B3 narrates, taught rung by rung: the keyspace, the Lua inventory, and the 3.0 Stream Tier — XADD, XREADGROUP, XACK — with conformance on Valkey.
Redis Patterns Applied
The substrate patterns under the bus: sorted sets, atomic Lua moves, locks, and streams — the judgement layer.
/echo-persistenceEcho Persistence — the durable floor
Beneath the volatile tiers: the Graft page engine built twice, the commit-LSN loop, and the stream archive on Tigris. Where Part I says state lives somewhere, this says where it lives durably.
References
sources · relatedSources
- Erlang/OTP — the gen_server behavioura system as a process: one mailbox, one private state, a message-only surface
- Erlang/OTP — the ets modulethe :private :ordered_set table a property store owns behind its boundary
- King — Announcing Snowflake (2010)the lock-free, time-ordered id the codec wraps
Related
- BCS — the course homethe law, the id anatomy, the chapter map
- Echo Persistencethe durable floor beneath the bus