A simple database to store a stream of events (facts) and retrieve them by index. This is aimed at systems based on Event Sourcing where complex state modelling/querying is left to other subsystems (e.g. your app, or a relational/graph database).
The database is thread-safe for concurrent reads/writes, though writes are locked to operate sequentially.
For more insight into the event driven/sourced programming style, see Practical event driven & sourced programs in Haskell.
Only POSIX (i.e. non-Windows) platforms are supported.
A database is expected to make guarantees about the data it holds, which are
collectively known as "ACID". Note in particular that all guarantees hold only
on systems/filesystems that honour the contract of
writev
, i.e. that "The
data transfers performed by readv() and writev() are atomic: the data written by
writev() is written as a single block that is not intermingled with output from
writes in other processes", and also honour the O_SYNC
option on file
descriptors as specified in
open
which behaves as if
fsync
was called
after each write, forcing data through the userspace and kernel to ensure it's
flushed to the physical disk drive. Failures of a physical device to atomically
write its buffers are not accounted for.
Isolation is catered for via STM;
writes are queued after an STM commit. Setting a queue size to 1
will result
in immediate writes in cases where performance matters less than saving data
quickly.
Grouped writes (i.e. transactions) to the database are guaranteed to be atomic, i.e. they will either all be written, or none of them will be.
The database is guaranteed to remain consistent after each successful or failing write. This is trivial to achieve with such a simple database where there is no support for multiple tables and the relationships between them.
Client code run in STM gets the
benefits of isolation through maintaining its own state and invalidating
transactions at a more fine-grained level than can be achieved at the database
level. Put another way; the database could invalidate any transactions if a
change has taken place in the meantime - but this is like using a sledgehammer
to crack a nut. On the other hand, client code knows in great detail whether or
not a transaction is valid, so passing off the isolation responsibility allows
for more efficient operation. See demo as an
example - examine in particular the transact
, exec
and apply
functions.
Successful writes to the database are guaranteed to persist even in the case of system failures like a crash or power outage. Uncommitted/interrupted writes will not affect the state of the database once the system is operational.
A sanity test script is run as a pre-commit hook to ensure that the database maintains consistency.
The excellent libfiu is used to introduce write errors, allowing tests on consistency - see script. In addition power-pull tests were executed on a laptop with a consumer-grade SSD.
- doctest to keep examples legit
- Test using dejafu
- Benchmark against the streaming benchmarks
- Property-based tests
- Randomly generated transactions, following close and read,
output == join input
- Randomly generated transactions, following close and read,
- .cabal file with tested lower bounds
- Consider
unliftio
module to replacesafe-exceptions