STM transactions involving persistent storage.
While Haskell's STM monad allows you to execute code transactionally, it does not allow you to persist the state on disk. This package implements a persistent storage bridge that runs inside the already existing STM monad.
persistent-stm - STM transactions involving persistent storage
Haskell's STM
monad implements composable transactions on in-memory state, offering atomicity, isolation, and consistency. However, they lack persistence, so changes are not durable at all. This package adds a limited form of persistence to the existing STM monad, allowing for transactions that include writing values durably to external storage.
The persistence is limited in the following senses:
- First, it is only suitable for use by a single process at a time. It is not possible to access the storage from multiple processes at the same time, evenif some of those processes are merely readers.
- While the view of data in memory is always consistent, the consistency of data on disk depends on the
Persistence
implementation, which you choose. The included implementation,filePersistence
, does not guarantee that data will be in a consistent or readable state if the process is suddenly terminated with a power outage, system crash, etc. This can be fixed by using aPersistence
implementation built on a transactional storage layer such as a database.
The persistence essentially works as a key-value store. The key is a String
, and the value can be of any type that implements DBStorable
. The DBStorable
class works like Binary
or other serialization classes, except that it's designed to have access to the DB
so that it can contain other DBRef
s. This way, at runtime, you can maintain complex data structures that point directly to each other, but are persisted via their keys.
Quick Start
A simple example of using persistent-stm
follows:
import PersistentSTM.DB
main :: IO ()
main = do
persistence <- filePersistence "./my-data"
withDB persistence $ \db -> do
n <- atomically $ do
ref <- getDBRef db "my-key"
readDBRef ref >>= \case
Nothing -> do
writeDBRef ref 1
return 1
Just n -> do
writeDBRef ref (n + 1)
return (n + 1)
putStrLn $ "Number of times program was run: " ++ show n
Here, filePersistence
creates a Persistence
implementation that stores data in a directory called ./my-data
on disk. The withDB
function brackets the portion of code that uses the directory for storage. During the execution of withDB
, one can use db
to read and write persistent values inside of STM transactions. That is done using getDBRef
, readDBRef
, writeDBRef
, and deleteDBRef
.
FAQ
Does this work reliably?
I'm publishing this now to get more feedback, but I am confident that within the limitations described above, this is a correct implementation. Until there is a broader community consensus, I'll still label this experimental.
How does this compare with the TCache package?
The implementation here was inspired by TCache, and involves some similar ideas. I was motivated to implement this package because of several details in which use of TCache was hard to justify. These include:
- Playing too fast and loose with unsafe operations for my taste.
- Far too much use of global state and overlapping instances.
- Many more possible states making it hard to reason about the correctness of the implementation.
- Failure to build with newer GHC versions.
All things put together, I reached the conclusion that I could trust a new implementation more than TCache.
Note that TCache has more features that this package. I don't intend to implement features like triggers, indexes, and so on, all of which can be implemented on top of the basic functionality in this package if desired.