ouroboros-consensus-0.1.0.1: Consensus layer for the Ouroboros blockchain protocol
Safe Haskell None
Language Haskell2010

Ouroboros.Consensus.Util.MonadSTM.RAWLock

Description

A Read-Append-Write (RAW) lock

Intended for qualified import

Synopsis

Public API

data RAWLock m st Source #

A Read-Append-Write (RAW) lock

A RAW lock allows multiple concurrent readers, at most one appender, which is allowed to run concurrently with the readers, and at most one writer, which has exclusive access to the lock.

The following table summarises which roles are allowed to concurrently access the RAW lock:

         │ Reader │ Appender │ Writer │
─────────┼────────┼──────────┼────────┤
Reader   │   V    │     V    │    X   │
Appender │░░░░░░░░│     X    │    X   │
Writer   │░░░░░░░░│░░░░░░░░░░│    X   │

It is important to realise that a RAW lock is intended to control access to a piece of in-memory state that should remain in sync with some other state that can only be modified using side-effects, e.g., the file system. If, for example, you're only maintaining a counter shared by threads, then simply use a TVar or an MVar .

Example use case: log files

A RAW lock is useful, for example, to maintain an in-memory index of log files stored on disk.

  • To read data from a log file, you need "read" access to the index to find out the file and offset where the requested piece of data is stored. While holding the RAW lock as a reader, you can perform the IO operation to read the data from the right log file. This can safely happen concurrently with other read operations.
  • To append data to the current log file, you need "append" access to the index so you can append an entry to the index and even to add a new log file to the index when necessary. While holding the RAW lock as an appender, you can perform the IO operation to append the piece of data to the current log file and, if necessary start a new log file. Only one append can happen concurrently. However, reads can safely happen concurrently with appends. Note that the in-memory index is only updated after writing to disk.
  • To remove the oldest log files, you need "write" access to the index, so you can remove files from the index. While holding the RAW lock as a writer, you can perform the IO operations to delete the oldest log files. No other operations can run concurrently with this operation: concurrent reads might try to read from deleted files and a concurrent append could try to append to a deleted file.

Analogy: Chicken coop

Think of readers as chickens, the appender as the rooster, and the writer as the fox. All of them want access to the chicken coop, i.e., the state protected by the RAW lock.

We can allow multiple chickens (readers) together in the chicken coop, they get along (reasonably) fine. We can also let one rooster (appender) in, but not more than one, otherwise he would start fighting with the other rooster (conflict with the other appender). We can only let the fox in when all chickens and the rooster (if present) have left the chicken coop, otherwise the fox would eat them (conflict with the appender and invalidate the results of readers, e.g, closing resources readers try to access).

Usage

To use the lock, use any of the three following operations:

If the standard bracketing the above three operations use doesn't suffice, use the following three acquire-release pairs:

NOTE: an acquire must be followed by the corresponding release, otherwise the correctness of the lock is not guaranteed and a dead-lock can happen.

NOTE: nested locking of the same lock is not allowed, as you might be blocked on yourself.

Notes

  • Only use a RAW lock when it is safe to concurrently read and append.
  • We do not guarantee fairness for appenders and writers. They will race for access each time the RAW lock changes.
  • When you have many writers and/or very frequent writes, readers and appenders will starve. You could say we have "unfairness", as writers win over readers and appenders. A RAW lock will not be the best fit in such a scenario.
  • When you have no writers and you only need a read-append lock, consider using a StrictMVar instead. The "stale" state can be used by the readers.
  • The state st is always evaluated to WHNF and is subject to the NoThunks check when enabled.
  • All public functions are exception-safe.

new :: ( IOLike m, NoThunks st) => st -> m ( RAWLock m st) Source #

Create a new RAWLock

poison :: ( IOLike m, Exception e, HasCallStack ) => RAWLock m st -> ( CallStack -> e) -> m ( Maybe st) Source #

Poison the lock with the given exception. All subsequent access to the lock will result in the given exception being thrown.

Unless the lock has already been poisoned, in which case the original exception with which the lock was poisoned will be thrown.

read :: IOLike m => RAWLock m st -> STM m st Source #

Read the contents of the RAWLock in an STM transaction.

Will retry when there is a writer.

In contrast to withReadAccess , this transaction will succeed when there is a writer waiting to write, as there is no IO-operation during which the lock must be held.

withAppendAccess :: forall m st a. IOLike m => RAWLock m st -> (st -> m (st, a)) -> m a Source #

Access the state stored in the RAWLock as an appender.

NOTE: it must be safe to run the given append action concurrently with readers.

Will block when there is another appender, a writer, or when a writer is waiting to take the lock.

withReadAccess :: forall m st a. IOLike m => RAWLock m st -> (st -> m a) -> m a Source #

Access the state stored in the RAWLock as a reader.

Will block when there is a writer or when a writer is waiting to take the lock.

withWriteAccess :: forall m st a. IOLike m => RAWLock m st -> (st -> m (st, a)) -> m a Source #

Access the state stored in the RAWLock as a writer.

Will block when there is another writer or while there are readers and/or an appender.

Exposed internals: non-bracketed acquire & release

unsafeAcquireAppendAccess :: IOLike m => RAWLock m st -> STM m st Source #

Access the state stored in the RAWLock as an appender.

Will block when there is another appender, a writer, or when a writer is waiting to take the lock.

Composable with other STM transactions.

NOTE: must be followed by a call to unsafeReleaseAppendAccess .

unsafeAcquireReadAccess :: IOLike m => RAWLock m st -> STM m st Source #

Acquire the RAWLock as a reader.

Will block when there is a writer or when a writer is waiting to take the lock.

Composable with other STM transactions.

NOTE: must be followed by a call to unsafeReleaseReadAccess .

unsafeAcquireWriteAccess :: IOLike m => RAWLock m st -> m st Source #

Access the state stored in the RAWLock as a writer.

Will block when there is another writer or while there are readers and/or an appender.

Does not compose with other STM transactions.

NOTE: must be followed by a call to unsafeReleaseWriteAccess .

unsafeReleaseAppendAccess Source #

Arguments

:: IOLike m
=> RAWLock m st
-> st

State to store in the lock

-> STM m ()

Release the RAWLock as an appender.

Doesn't block.

Composable with other STM transactions.

NOTE: must be preceded by a call to unsafeAcquireAppendAccess .

unsafeReleaseReadAccess :: IOLike m => RAWLock m st -> STM m () Source #

Release the RAWLock as a reader.

Doesn't block.

Composable with other STM transactions.

NOTE: must be preceded by a call to unsafeAcquireReadAccess .

unsafeReleaseWriteAccess Source #

Arguments

:: IOLike m
=> RAWLock m st
-> st

State to store in the lock

-> m ()

Release the RAWLock as a writer.

Doesn't block.

Does not compose with other STM transactions.

NOTE: must be preceded by a call to unsafeAcquireWriteAccess .