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

Copy Markdown View Source

Durable session store backed by EKV (SQLite in WAL mode, via a vendored NIF).

Requires the optional :ekv dependency:

{:ekv, "~> 0.4"}

Start the store in a supervision tree and point sessions at it:

children = [
  {FIX.Session.Store.EKV, data_dir: "/var/lib/my_app/fix_session"},
  {FIX.Session, FIX.Session.Config.new!(store: FIX.Session.Store.EKV, ...)}
]

Store operations take the EKV instance name as the store_ref.

Options

  • :name — the EKV instance name, FIX.Session.Store.EKV by default, which also serves as the store_ref. Pass a distinct atom per instance to run several stores side by side.

  • :data_dir — required. Directory holding the SQLite files; created if absent. Reusing a data_dir resumes its persisted state, which is what makes this store durable.

All other options are passed through to EKV.start_link/1.

Durability

Writes are committed to SQLite before the store call returns, so state survives session, store, and VM crashes. SQLite runs in WAL mode with synchronous=NORMAL: the most recent commits can still be lost to an OS crash or power failure.

commit_outbound/5 writes the wire record and then the advanced next_out pointer as two single-key EKV writes (EKV has no multi-key transactions). A crash between the two leaves a wire record that was never sent — commits happen before the socket write — so on reload the stale pointer hands out that sequence number again and the next commit overwrites the orphan. No sequence number is reused on the wire or skipped.

Session ids become part of durable keys 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).

Error semantics

load/2, save_inbound/3, and commit_outbound/5 report an unreachable EKV instance as {:error, :store_unavailable}. get_outbound/3 instead raises when the instance is gone: its :error return must mean "never stored" and nothing else, because the resend path answers it with a SequenceReset-GapFill. This store never deletes entries or sets TTLs, so a nil read strictly means the sequence number was never committed.

Summary

Types

store_ref()

@type store_ref() :: atom()

Functions

child_spec(options)

@spec child_spec(keyword()) :: Supervisor.child_spec()

start_link(options \\ [])

@spec start_link(keyword()) :: Supervisor.on_start()