FIX.Session.Store.Postgres (fix_session v0.1.3)

Copy Markdown View Source

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

store_ref()

@type store_ref() :: module()