Durable session store backed by PostgreSQL through the host
application's Ecto.Repo.
Requires the optional :ecto_sql and :postgrex dependencies:
{:ecto_sql, "~> 3.10"},
{:postgrex, "~> 0.19"}Create the tables from a migration in the host application (see
FIX.Session.Store.Postgres.Migrations):
def up, do: FIX.Session.Store.Postgres.Migrations.up()
def down, do: FIX.Session.Store.Postgres.Migrations.down()The store owns no processes: the store_ref is the repo module, and
the repo must be supervised before any session that uses it:
children = [
MyApp.Repo,
{FIX.Session,
FIX.Session.Config.new!(
store: FIX.Session.Store.Postgres,
store_ref: MyApp.Repo,
...
)}
]Durability
commit_outbound/5 writes the outbound wire bytes and the advanced
next_out in a single database transaction, so the two can never be
observed apart, even across a crash at any point. Durability is
PostgreSQL's: state survives session, store, VM, and machine crashes
subject to the server's own durability configuration.
Session ids become part of durable rows via
:erlang.term_to_binary(session_id, [:deterministic]), so a session
id must have a stable external term format (atoms, binaries, numbers,
and tuples/lists/maps thereof).
Concurrency
The upserts assume one session process is the sole writer for its
session_id — which FIX.Session guarantees — and therefore take no
row locks. Running two live sessions against the same session_id
is a configuration error with any store.
Error semantics
load/2, save_inbound/3, and commit_outbound/5 report an
unreachable repo or database as {:error, :store_unavailable}.
Misconfiguration is not unavailability: missing tables (the migration
never ran) and invalid data raise. get_outbound/3 never rescues:
its :error return must mean "never stored" and nothing else,
because the resend path answers it with a SequenceReset-GapFill.
This store never deletes rows, so a missing row strictly means the
sequence number was never committed.
Summary
Types
@type store_ref() :: module()