packages feed

rawlock (empty) → 0.1.0.0

raw patch · 7 files changed

+1042/−0 lines, 7 filesdep +QuickCheckdep +basedep +io-classes

Dependencies added: QuickCheck, base, io-classes, io-sim, mtl, nothunks, rawlock, strict-mvar, strict-stm, tasty, tasty-quickcheck

Files

+ CHANGELOG.md view
@@ -0,0 +1,7 @@+# rawlock Changelog++# Changelog entries++## 0.1.0.0 -- 2024-10-22++* First version. Released on an unsuspecting world.
+ LICENSE view
@@ -0,0 +1,177 @@++                                 Apache License+                           Version 2.0, January 2004+                        http://www.apache.org/licenses/++   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION++   1. Definitions.++      "License" shall mean the terms and conditions for use, reproduction,+      and distribution as defined by Sections 1 through 9 of this document.++      "Licensor" shall mean the copyright owner or entity authorized by+      the copyright owner that is granting the License.++      "Legal Entity" shall mean the union of the acting entity and all+      other entities that control, are controlled by, or are under common+      control with that entity. For the purposes of this definition,+      "control" means (i) the power, direct or indirect, to cause the+      direction or management of such entity, whether by contract or+      otherwise, or (ii) ownership of fifty percent (50%) or more of the+      outstanding shares, or (iii) beneficial ownership of such entity.++      "You" (or "Your") shall mean an individual or Legal Entity+      exercising permissions granted by this License.++      "Source" form shall mean the preferred form for making modifications,+      including but not limited to software source code, documentation+      source, and configuration files.++      "Object" form shall mean any form resulting from mechanical+      transformation or translation of a Source form, including but+      not limited to compiled object code, generated documentation,+      and conversions to other media types.++      "Work" shall mean the work of authorship, whether in Source or+      Object form, made available under the License, as indicated by a+      copyright notice that is included in or attached to the work+      (an example is provided in the Appendix below).++      "Derivative Works" shall mean any work, whether in Source or Object+      form, that is based on (or derived from) the Work and for which the+      editorial revisions, annotations, elaborations, or other modifications+      represent, as a whole, an original work of authorship. For the purposes+      of this License, Derivative Works shall not include works that remain+      separable from, or merely link (or bind by name) to the interfaces of,+      the Work and Derivative Works thereof.++      "Contribution" shall mean any work of authorship, including+      the original version of the Work and any modifications or additions+      to that Work or Derivative Works thereof, that is intentionally+      submitted to Licensor for inclusion in the Work by the copyright owner+      or by an individual or Legal Entity authorized to submit on behalf of+      the copyright owner. For the purposes of this definition, "submitted"+      means any form of electronic, verbal, or written communication sent+      to the Licensor or its representatives, including but not limited to+      communication on electronic mailing lists, source code control systems,+      and issue tracking systems that are managed by, or on behalf of, the+      Licensor for the purpose of discussing and improving the Work, but+      excluding communication that is conspicuously marked or otherwise+      designated in writing by the copyright owner as "Not a Contribution."++      "Contributor" shall mean Licensor and any individual or Legal Entity+      on behalf of whom a Contribution has been received by Licensor and+      subsequently incorporated within the Work.++   2. Grant of Copyright License. Subject to the terms and conditions of+      this License, each Contributor hereby grants to You a perpetual,+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable+      copyright license to reproduce, prepare Derivative Works of,+      publicly display, publicly perform, sublicense, and distribute the+      Work and such Derivative Works in Source or Object form.++   3. Grant of Patent License. Subject to the terms and conditions of+      this License, each Contributor hereby grants to You a perpetual,+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable+      (except as stated in this section) patent license to make, have made,+      use, offer to sell, sell, import, and otherwise transfer the Work,+      where such license applies only to those patent claims licensable+      by such Contributor that are necessarily infringed by their+      Contribution(s) alone or by combination of their Contribution(s)+      with the Work to which such Contribution(s) was submitted. If You+      institute patent litigation against any entity (including a+      cross-claim or counterclaim in a lawsuit) alleging that the Work+      or a Contribution incorporated within the Work constitutes direct+      or contributory patent infringement, then any patent licenses+      granted to You under this License for that Work shall terminate+      as of the date such litigation is filed.++   4. Redistribution. You may reproduce and distribute copies of the+      Work or Derivative Works thereof in any medium, with or without+      modifications, and in Source or Object form, provided that You+      meet the following conditions:++      (a) You must give any other recipients of the Work or+          Derivative Works a copy of this License; and++      (b) You must cause any modified files to carry prominent notices+          stating that You changed the files; and++      (c) You must retain, in the Source form of any Derivative Works+          that You distribute, all copyright, patent, trademark, and+          attribution notices from the Source form of the Work,+          excluding those notices that do not pertain to any part of+          the Derivative Works; and++      (d) If the Work includes a "NOTICE" text file as part of its+          distribution, then any Derivative Works that You distribute must+          include a readable copy of the attribution notices contained+          within such NOTICE file, excluding those notices that do not+          pertain to any part of the Derivative Works, in at least one+          of the following places: within a NOTICE text file distributed+          as part of the Derivative Works; within the Source form or+          documentation, if provided along with the Derivative Works; or,+          within a display generated by the Derivative Works, if and+          wherever such third-party notices normally appear. The contents+          of the NOTICE file are for informational purposes only and+          do not modify the License. You may add Your own attribution+          notices within Derivative Works that You distribute, alongside+          or as an addendum to the NOTICE text from the Work, provided+          that such additional attribution notices cannot be construed+          as modifying the License.++      You may add Your own copyright statement to Your modifications and+      may provide additional or different license terms and conditions+      for use, reproduction, or distribution of Your modifications, or+      for any such Derivative Works as a whole, provided Your use,+      reproduction, and distribution of the Work otherwise complies with+      the conditions stated in this License.++   5. Submission of Contributions. Unless You explicitly state otherwise,+      any Contribution intentionally submitted for inclusion in the Work+      by You to the Licensor shall be under the terms and conditions of+      this License, without any additional terms or conditions.+      Notwithstanding the above, nothing herein shall supersede or modify+      the terms of any separate license agreement you may have executed+      with Licensor regarding such Contributions.++   6. Trademarks. This License does not grant permission to use the trade+      names, trademarks, service marks, or product names of the Licensor,+      except as required for reasonable and customary use in describing the+      origin of the Work and reproducing the content of the NOTICE file.++   7. Disclaimer of Warranty. Unless required by applicable law or+      agreed to in writing, Licensor provides the Work (and each+      Contributor provides its Contributions) on an "AS IS" BASIS,+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or+      implied, including, without limitation, any warranties or conditions+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A+      PARTICULAR PURPOSE. You are solely responsible for determining the+      appropriateness of using or redistributing the Work and assume any+      risks associated with Your exercise of permissions under this License.++   8. Limitation of Liability. In no event and under no legal theory,+      whether in tort (including negligence), contract, or otherwise,+      unless required by applicable law (such as deliberate and grossly+      negligent acts) or agreed to in writing, shall any Contributor be+      liable to You for damages, including any direct, indirect, special,+      incidental, or consequential damages of any character arising as a+      result of this License or out of the use or inability to use the+      Work (including but not limited to damages for loss of goodwill,+      work stoppage, computer failure or malfunction, or any and all+      other commercial damages or losses), even if such Contributor+      has been advised of the possibility of such damages.++   9. Accepting Warranty or Additional Liability. While redistributing+      the Work or Derivative Works thereof, You may choose to offer,+      and charge a fee for, acceptance of support, warranty, indemnity,+      or other liability obligations and/or rights consistent with this+      License. However, in accepting such obligations, You may act only+      on Your own behalf and on Your sole responsibility, not on behalf+      of any other Contributor, and only if You agree to indemnify,+      defend, and hold each Contributor harmless for any liability+      incurred by, or claims asserted against, such Contributor by reason+      of your accepting any such warranty or additional liability.++   END OF TERMS AND CONDITIONS
+ NOTICE view
@@ -0,0 +1,13 @@+Copyright 2024 INTERSECT++   Licensed under the Apache License, Version 2.0 (the "License");+   you may not use this file except in compliance with the License.+   You may obtain a copy of the License at++       http://www.apache.org/licenses/LICENSE-2.0++   Unless required by applicable law or agreed to in writing, software+   distributed under the License is distributed on an "AS IS" BASIS,+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+   See the License for the specific language governing permissions and+   limitations under the License.
+ README.md view
@@ -0,0 +1,22 @@+# RAWLock++A writer-biased RAW lock.++It allows for multiple readers to run concurrently with at most one appender, or+a single writer running on isolation.++The code is safe in the presence of async exceptions, meaning that each actor+will cleanup after itself if an exception is received.++## Contributing++Contributions are welcome! Feel free to open issues or submit pull requests.++Before contributing, please ensure that:++- Code is well-documented+- Tests are written for new features or bug fixes++## License++This project is licensed under the terms of the [Apache 2.0](./LICENSE) license.
+ rawlock.cabal view
@@ -0,0 +1,83 @@+cabal-version: 3.0+name: rawlock+version: 0.1.0.0+synopsis: A writer-biased RAW lock.+description:+  A writer-biased RAW lock.++  It allows for multiple readers to run concurrently with at most one+  appender, or a single writer running on isolation.++  The code is safe in the presence of async exceptions, meaning that each+  actor will cleanup after itself if an exception is received.++license: Apache-2.0+license-files:+  LICENSE+  NOTICE++copyright: 2024 INTERSECT+author: IOG Engineering Team+maintainer: operations@iohk.io+category: Concurrency+build-type: Simple+extra-doc-files:+  CHANGELOG.md+  README.md++bug-reports: https://github.com/IntersectMBO/io-classes-extra/issues+tested-with: ghc ==8.10 || ==9.2 || ==9.4 || ==9.6 || ==9.8 || ==9.10++source-repository head+  type: git+  location: https://github.com/IntersectMBO/io-classes-extra+  subdir: rawlock++source-repository this+  type: git+  location: https://github.com/IntersectMBO/io-classes-extra+  subdir: rawlock+  tag: rawlock-0.1.0.0++common warnings+  ghc-options:+    -Wall+    -Wunused-packages+    -Wcompat+    -Wincomplete-uni-patterns+    -Wincomplete-record-updates+    -Wpartial-fields+    -Widentities+    -Wredundant-constraints+    -Wmissing-export-lists+    -Wno-unticked-promoted-constructors++library+  import: warnings+  hs-source-dirs: src+  default-language: Haskell2010+  exposed-modules: Control.RAWLock+  default-extensions: ImportQualifiedPost+  build-depends:+    base >=4.14 && <4.21,+    io-classes ^>=1.5,+    nothunks ^>=0.2,+    strict-mvar ^>=1.5,+    strict-stm ^>=1.5,++test-suite rawlock-test+  import: warnings+  default-language: Haskell2010+  type: exitcode-stdio-1.0+  hs-source-dirs: test+  main-is: Main.hs+  build-depends:+    QuickCheck,+    base,+    io-classes,+    io-sim,+    mtl,+    rawlock,+    strict-stm,+    tasty,+    tasty-quickcheck,
+ src/Control/RAWLock.hs view
@@ -0,0 +1,403 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE UndecidableInstances #-}++-- | 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:+--+-- * 'withReadAccess'+-- * 'withAppendAccess'+-- * 'withWriteAccess'+--+-- If the standard bracketing the above three operations use doesn't suffice,+-- use the following three acquire-release pairs:+--+-- * 'unsafeAcquireReadAccess'   & 'unsafeReleaseReadAccess'+-- * 'unsafeAcquireAppendAccess' & 'unsafeReleaseAppendAccess'+-- * 'unsafeAcquireWriteAccess'  & 'unsafeReleaseWriteAccess'+--+-- 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. Once the lock is released, all waiting+--    actors will race for the access.+--+--  * The state @st@ is always evaluated to WHNF and is subject to the+--    'NoThunks' check when enabled.+--+--  * All public functions are exception-safe.+module Control.RAWLock (+    -- * API+    RAWLock+  , new+  , poison+  , read+  , withAppendAccess+  , withReadAccess+  , withWriteAccess+    -- * Unsafe API+    -- $unsafe-api+  , unsafeAcquireAppendAccess+  , unsafeAcquireReadAccess+  , unsafeAcquireWriteAccess+  , unsafeReleaseAppendAccess+  , unsafeReleaseReadAccess+  , unsafeReleaseWriteAccess+  ) where++import Control.Concurrent.Class.MonadMVar.Strict+import Control.Concurrent.Class.MonadSTM.Strict+import Control.Monad.Class.MonadThrow+import GHC.Generics+import GHC.Stack (CallStack, HasCallStack, callStack)+import NoThunks.Class+import Prelude hiding (read)++-- | Any non-negative number of readers+newtype Readers = Readers Word+  deriving newtype (Eq, Ord, Enum, Num, NoThunks)+  deriving stock (Show)++-- | Any non-negative number of writers+newtype Writers = Writers Word+  deriving newtype (Eq, Ord, Enum, Num, NoThunks)+  deriving stock (Show)++-- | Any non-negative number of appenders+newtype Appenders = Appenders Word+  deriving newtype (Eq, Ord, Enum, Num, NoThunks)+  deriving stock (Show)++data RAWState = RAWState {+    waitingReaders   :: !Readers+  , waitingAppenders :: !Appenders+  , waitingWriters   :: !Writers+  } deriving Show++noWriters :: Poisonable RAWState -> Bool+noWriters (Healthy (RAWState _ _ w)) = w == 0+noWriters _                          = True++onlyWriters :: Poisonable RAWState -> Bool+onlyWriters (Healthy (RAWState r a _)) = r == 0 && a == 0+onlyWriters _                          = True++pushReader :: Poisonable RAWState -> Poisonable RAWState+pushReader = fmap (\(RAWState r a w) -> RAWState (r + 1) a w)++pushAppender :: Poisonable RAWState -> Poisonable RAWState+pushAppender = fmap (\(RAWState r a w) -> RAWState r (a + 1) w)++pushWriter :: Poisonable RAWState -> Poisonable RAWState+pushWriter = fmap (\(RAWState r a w) -> RAWState r a (w + 1))++popReader :: Poisonable RAWState -> Poisonable RAWState+popReader = fmap (\(RAWState r a w) -> RAWState (r - 1) a w)++popAppender :: Poisonable RAWState -> Poisonable RAWState+popAppender = fmap (\(RAWState r a w) -> RAWState r (a - 1) w)++popWriter :: Poisonable RAWState -> Poisonable RAWState+popWriter = fmap (\(RAWState r a w) -> RAWState r a (w - 1))++-- | Data that can be replaced with an exception that should be thrown when+-- found.+data Poisonable st =+    Healthy !st+  | Poisoned !(AllowThunk SomeException)+  deriving (Generic, NoThunks, Functor)++data RAWLock m st = RAWLock {+    resource :: !(StrictTMVar m (Poisonable st))+  , appender :: !(StrictMVar m ())+  , queues   :: !(StrictTVar m (Poisonable RAWState))+  } deriving (Generic)++deriving instance ( NoThunks (StrictTMVar m (Poisonable st))+                  , NoThunks (StrictMVar m ())+                  , NoThunks (StrictTVar m (Poisonable RAWState))+                  ) => NoThunks (RAWLock m st)++new ::+     ( MonadMVar m+     , MonadLabelledSTM m+     )+  => st+  -> m (RAWLock m st)+new !st = do+  s <- newTMVarIO (Healthy st)+  atomically $ labelTMVar s "state"+  a <- newMVar ()+  q <- newTVarIO (Healthy emptyRAWState)+  atomically $ labelTVar q "queues"+  pure $ RAWLock s a q++read :: (MonadSTM m, MonadThrow (STM m)) => RAWLock m st -> STM m st+read (RAWLock var _ _) = readTMVar var >>= throwPoisoned++-- | When a lock is poisoned all subsequent access to it is overridden by the+-- poison. This means that the current actor that holds the lock will free it,+-- and any other concurrent actors will be able to release their access,+-- possibly rising the poison exception in the process.+--+-- There is no need (although it is harmless) to release again the current+-- actor once it has poisoned the lock.+poison ::+     (Exception e, MonadMVar m, MonadSTM m, MonadThrow (STM m), HasCallStack)+  => RAWLock m st+  -> (CallStack -> e)+  -> m (Maybe st)+poison (RAWLock var apm q) mkExc = do+  st <- atomically $+    tryReadTMVar var >>= \case+      -- Keep original exception+      Just (Poisoned (AllowThunk exc)) -> throwIO exc+      Just (Healthy st) -> do+        writeTMVar var (Poisoned (AllowThunk (toException (mkExc callStack))))+        writeTVar q (Poisoned (AllowThunk (toException (mkExc callStack))))+        pure (Just st)+      Nothing -> do+        writeTMVar var (Poisoned (AllowThunk (toException (mkExc callStack))))+        writeTVar q (Poisoned (AllowThunk (toException (mkExc callStack))))+        pure Nothing+  _ <- tryPutMVar apm ()+  pure st++-- | Create an initial, empty, unlocked 'RAWState': no readers, no appender,+-- no writer (waiting).+emptyRAWState :: RAWState+emptyRAWState = RAWState 0 0 0++-- | Acquire the 'RAWLock' as a reader.+--+-- Will block when there is a writer or when a writer is waiting to take the+-- lock.+withReadAccess ::+     (MonadSTM m, MonadCatch m, MonadThrow (STM m))+  => RAWLock m st+  -> (st -> m a)+  -> m a+withReadAccess lock =+  bracket+    (atomically (unsafeAcquireReadAccess lock))+    (const (atomically (unsafeReleaseReadAccess lock)))++-- | Acquire the 'RAWLock' as a writer.+--+-- Will block when there is another writer, readers or appenders.+withWriteAccess ::+     (MonadSTM m, MonadCatch m, MonadThrow (STM m))+  => RAWLock m st+  -> (st -> m (a, st))+  -> m a+withWriteAccess lock f =+  fst . fst <$> generalBracket+   (unsafeAcquireWriteAccess lock)+   (\orig -> \case+       ExitCaseSuccess (_, st) -> unsafeReleaseWriteAccess lock st+       _ -> unsafeReleaseWriteAccess lock orig+   )+   f++-- | Acquire the 'RAWLock' as an appender.+--+-- Will block when there is a writer or when there is another appender.+withAppendAccess ::+     (MonadThrow (STM m), MonadSTM m, MonadCatch m, MonadMVar m)+  => RAWLock m st+  -> (st -> m (a, st))+  -> m a+withAppendAccess lock f = do+  fst . fst <$> generalBracket+   (unsafeAcquireAppendAccess lock)+   (\orig -> \case+       ExitCaseSuccess (_, st) -> unsafeReleaseAppendAccess lock st+       _ -> unsafeReleaseAppendAccess lock orig+   )+   f++{-------------------------------------------------------------------------------+  Unsafe API+-------------------------------------------------------------------------------}++throwPoisoned :: MonadThrow m => Poisonable st -> m st+throwPoisoned (Healthy st)                = pure st+throwPoisoned (Poisoned (AllowThunk exc)) = throwIO exc++-- $unsafe-api+--+-- These functions are unsafe in the sense that they do not guard against+-- exceptions, meaning that if you don't take care and ensure exception safety,+-- you might make the RAWLock unusable.+--+-- To be safe, you should ensure that every @unsafeAcquireXAccess@ is paired with+-- @unsafeReleaseXAccess@, __even in the presence of exceptions__.+--+-- Note that for writing and appending, you should restore the original value in+-- presence of an exception!++unsafeAcquireReadAccess ::+     (MonadThrow (STM m), MonadSTM m)+  => RAWLock m st+  -> STM m st+unsafeAcquireReadAccess (RAWLock var _ qs) = do+  -- wait until there are no writers+  readTVar qs >>= check . noWriters+  -- queue myself+  modifyTVar qs pushReader+  -- read the state+  throwPoisoned =<< readTMVar var++unsafeReleaseReadAccess :: MonadSTM m => RAWLock m st -> STM m ()+unsafeReleaseReadAccess (RAWLock _ _ qs) =+  -- unqueue myself+  modifyTVar qs popReader++unsafeAcquireWriteAccess ::+     (MonadThrow (STM m), MonadCatch m, MonadSTM m)+  => RAWLock m st+  -> m st+unsafeAcquireWriteAccess (RAWLock var _ qs) = do+  -- queue myself if there are no other writers+  atomically $ do+    -- wait until there are no writers+    readTVar qs >>= check . noWriters+    -- queue myself+    modifyTVar qs pushWriter+  atomically (do+    -- wait until there are no readers (and as I queued myself above, I'm the+    -- only waiting writer)+    readTVar qs >>= check . onlyWriters+    -- acquire the state+    throwPoisoned =<< takeTMVar var) `onException` atomically (modifyTVar qs popWriter)++unsafeReleaseWriteAccess ::+     (MonadThrow (STM m), MonadSTM m)+  => RAWLock m st+  -> st+  -> m ()+unsafeReleaseWriteAccess (RAWLock var _ qs) !st =+  atomically $ do+    -- write the new state+    tryReadTMVar var >>= \case+      Nothing -> putTMVar var (Healthy st)+      Just (Poisoned (AllowThunk exc)) -> throwIO exc+      Just Healthy{} -> error "Double put"+    -- unqueue myself+    modifyTVar qs popWriter++unsafeAcquireAppendAccess ::+     (MonadThrow (STM m), MonadCatch m, MonadMVar m, MonadSTM m)+  => RAWLock m st+  -> m st+unsafeAcquireAppendAccess (RAWLock var apm qs) = do+  atomically $ do+    -- wait until there are no writers+    readTVar qs >>= check . noWriters+    -- queue myself+    modifyTVar qs pushAppender+  (do+      -- lock the append access+      takeMVar apm+      -- acquire the state+      atomically (readTMVar var >>= throwPoisoned) `onException` putMVar apm ()+    ) `onException` atomically (modifyTVar qs popAppender)++unsafeReleaseAppendAccess ::+     (MonadThrow (STM m), MonadMVar m, MonadSTM m)+  => RAWLock m st+  -> st+  -> m ()+unsafeReleaseAppendAccess (RAWLock var apm qs) !st = do+  atomically $ do+    -- write the new state+    tryReadTMVar var >>= \case+      Just (Poisoned (AllowThunk exc)) -> throwIO exc+      _ -> writeTMVar var (Healthy st)+    -- unqueue myself+    modifyTVar qs popAppender+  -- release the append access+  putMVar apm ()
+ test/Main.hs view
@@ -0,0 +1,337 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE NumericUnderscores #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Main (main) where++import Control.Concurrent.Class.MonadSTM.Strict+import Control.Exception (throw)+import Control.Monad.Class.MonadAsync+import Control.Monad.Class.MonadFork+import Control.Monad.Class.MonadSay+import Control.Monad.Class.MonadTest+import Control.Monad.Class.MonadThrow+import Control.Monad.Class.MonadTimer+import Control.Monad.Except+import Control.Monad.IOSim+import Control.RAWLock+import Data.Either+import Test.QuickCheck.Gen.Unsafe+import Test.QuickCheck.Monadic+import Test.Tasty+import Test.Tasty.QuickCheck++main :: IO ()+main = defaultMain $ testGroup "RAWLock" [+    testProperty "Exception safe" $ conjoin (map prop_exception_safe allCommandCombinations)+  , testProperty "correctness" prop_correctness+  , testProperty "unsafe functions do not deadlock" $ conjoin (map prop_unsafe_actions allCommandCombinations)+  ]++{-------------------------------------------------------------------------------+  Exception safe+-------------------------------------------------------------------------------}++data Action = Read | Incr | Append+  deriving (Show)++allCommandCombinations :: [(Action, Action, Action)]+allCommandCombinations =+  [ (a, b, c) | let cmds = [Read, Incr, Append], a <- cmds, b <- cmds, c <- cmds ]++prop_exception_safe :: (Action, Action, Action) -> Property+prop_exception_safe actions@(a1, a2, a3) = counterexample (show actions) $+    exploreSimTrace id action+      (\_ tr -> counterexample (ppTrace tr) . property . isRight . traceResult False $ tr)+  where+    action :: IOSim s ()+    action = do+      exploreRaces+      l <- new (0 :: Int)+      let c = \case+            Read -> withReadAccess l $ \s -> say (show s)+            Incr -> withWriteAccess l $ \s -> say (show s) >> pure ((), s + 1)+            Append -> withAppendAccess l $ \s -> say (show s) >> pure ((), s + 1)+      t1 <- async $ c a1+      t2 <- async $ c a2+      t3 <- async $ c a3+      async (cancel t1) >>= wait+      (_ :: Either SomeException ()) <- waitCatch t1+      (_ :: Either SomeException ()) <- waitCatch t2+      (_ :: Either SomeException ()) <- waitCatch t3+      pure ()++{-------------------------------------------------------------------------------+  Correctness+-------------------------------------------------------------------------------}++-- | Test the correctness of the RAWLock+--+-- For a generated number of readers, appenders, and writers: spawn a thread+-- for each. Each thread will process a list of 'ThreadDelays'. For each+-- 'ThreadDelays': wait the generated 'beforeLockTime', then lock the RAWLock+-- with the respective @RAWLock.withXAccess@, increment a 'TVar' that stores+-- the number of readers/appenders/writers that have access, hold the lock for+-- a generated 'withLockTime', decrement the 'TVar', and release the lock.+--+-- In a separate thread, we watch for any changes in the three 'TVar's and+-- write each changed 'RAWState' to a trace (in a separate 'TVar').+-- Afterwards, we check the 'RAWState's in the trace for consistency (using+-- 'isConsistent'), e.g., not more than one concurrent appender.+prop_correctness :: TestSetup -> Property+prop_correctness (TestSetup rawDelays) =+    monadicSimWithTrace tabulateBlockeds test+  where+    RAW readerDelays appenderDelays writerDelays = rawDelays++    test :: forall s. PropertyM (IOSim s) ()+    test = do+      rawVars@(RAW varReaders varAppenders varWriters) <- run newRAWVars++      trace <- run $ do+        rawLock  <- new ()+        varTrace <- newTVarIO []++        let traceState :: STM (IOSim s) ()+            traceState = do+              rawState <- readRAWState rawVars+              modifyTVar varTrace (rawState:)++        threads <- mapM (async . (labelThisThread "testThread" >>)) $+          map (runReader   rawLock traceState varReaders)   readerDelays   <>+          map (runAppender rawLock traceState varAppenders) appenderDelays <>+          map (runWriter   rawLock traceState varWriters)   writerDelays++        mapM_ wait threads+        reverse <$> atomically (readTVar varTrace)++      checkRAWTrace trace++    runReader+      :: RAWLock (IOSim s) ()+      -> STM (IOSim s) ()  -- ^ Trace the 'RAWState'+      -> StrictTVar (IOSim s) Int+      -> [ThreadDelays]+      -> IOSim s ()+    runReader rawLock traceState varReaders =+      mapM_ $ \(ThreadDelays before with) -> do+        threadDelay before+        withReadAccess rawLock $ const $ do+          atomically $ modifyTVar varReaders succ *> traceState+          threadDelay with+          atomically $ modifyTVar varReaders pred *> traceState++    runAppender+      :: RAWLock (IOSim s) ()+      -> STM (IOSim s) ()  -- ^ Trace the 'RAWState'+      -> StrictTVar (IOSim s) Int+      -> [ThreadDelays]+      -> IOSim s ()+    runAppender rawLock traceState varAppenders =+      mapM_ $ \(ThreadDelays before with) -> do+        threadDelay before+        withAppendAccess rawLock $ const $ do+          atomically $ modifyTVar varAppenders succ *> traceState+          threadDelay with+          atomically $ modifyTVar varAppenders pred *> traceState+          return ((), ())++    runWriter+      :: RAWLock (IOSim s) ()+      -> STM (IOSim s) ()  -- ^ Trace the 'RAWState'+      -> StrictTVar (IOSim s) Int+      -> [ThreadDelays]+      -> IOSim s ()+    runWriter rawLock traceState varWriters =+      mapM_ $ \(ThreadDelays before with) -> do+        threadDelay before+        withWriteAccess rawLock $ const $ do+          atomically $ modifyTVar varWriters succ *> traceState+          threadDelay with+          atomically $ modifyTVar varWriters pred *> traceState+          return ((), ())++-- | Like 'monadicSim' (which is like 'monadicIO' for the IO simulator), but+-- allows inspecting the trace for labelling purposes.+monadicSimWithTrace ::+     Testable a+  => (forall x. SimTrace x -> Property -> Property)+  -> (forall s. PropertyM (IOSim s) a)+  -> Property+monadicSimWithTrace attachTrace m = property $ do+    tr <- runSimGenWithTrace (monadic' m)+    case traceResult False tr of+      Left failure -> throw failure+      Right prop   -> return $ attachTrace tr prop+  where+    runSimGenWithTrace :: (forall s. Gen (IOSim s a)) -> Gen (SimTrace a)+    runSimGenWithTrace f = do+      Capture eval <- capture+      return $ runSimTrace (eval f)++-- | Tabulate the number of times a thread is blocked.+--+-- The higher this number, the higher the contention. If there's no+-- contention, we're not testing the lock properly.+tabulateBlockeds :: SimTrace a -> Property -> Property+tabulateBlockeds tr =+    tabulate "number of times blocked" [classifyBand (count isBlocked tr)]+  where+    isBlocked (EventTxBlocked {}) = Just ()+    isBlocked _                   = Nothing++    count :: (SimEventType -> Maybe x) -> SimTrace a -> Int+    count p = length . selectTraceEvents (const p)++    classifyBand :: Int -> String+    classifyBand n+      | n < 10+      = "n < 10"+      | n < 100+      = "n < 100"+      | n < 1000+      = "n < 1,000"+      | n < 10_000+      = "1,000 < n < 10,000"+      | n < 100_000+      = "10,000 < n < 100,000"+      | n < 1000_000+      = "100,000 < n < 1,000,000"+      | otherwise+      = "1,000,000 < n"++{-------------------------------------------------------------------------------+  State checking+-------------------------------------------------------------------------------}++-- | Data type reused whenever we need something for all three of them.+data RAW a = RAW+    { readers   :: a+    , appenders :: a+    , writers   :: a+    }+  deriving (Show, Eq, Functor)++type RAWVars m = RAW (StrictTVar m Int)++newRAWVars :: IOSim s (RAWVars (IOSim s))+newRAWVars = RAW <$> newTVarIO 0 <*> newTVarIO 0 <*> newTVarIO 0++type RAWState' = RAW Int++readRAWState :: RAWVars (IOSim s) -> STM (IOSim s) RAWState'+readRAWState RAW { readers, appenders, writers } =+    RAW+      <$> readTVar readers+      <*> readTVar appenders+      <*> readTVar writers++isConsistent :: RAWState' -> Except String ()+isConsistent RAW { readers, appenders, writers }+    | appenders > 1+    = throwError $ show appenders <> " appenders while at most 1 is allowed"+    | writers > 1+    = throwError $ show writers <> " writers while at most 1 is allowed"+    | writers == 1, readers > 0+    = throwError $ "writer concurrent with " <> show readers <> "reader(s)"+    | writers == 1, appenders > 0+    = throwError $ "writer concurrent with an appender"+    | otherwise+    = return ()++type RAWTrace = [RAWState']++checkRAWTrace :: Monad m => RAWTrace -> PropertyM m ()+checkRAWTrace = mapM_ $ \rawState ->+    case runExcept $ isConsistent rawState of+      Left msg -> do+        monitor (counterexample msg)+        Test.QuickCheck.Monadic.assert False+      Right () ->+        return ()++{-------------------------------------------------------------------------------+  Generators+-------------------------------------------------------------------------------}++newtype TestSetup = TestSetup (RAW [[ThreadDelays]])+  deriving (Show)++instance Arbitrary TestSetup where+  arbitrary = do+    nbReaders   <- choose (0, 3)+    nbAppenders <- choose (0, 3)+    nbWriters   <- choose (0, 3)+    readers   <- vectorOf nbReaders   arbitrary+    appenders <- vectorOf nbAppenders arbitrary+    writers   <- vectorOf nbWriters   arbitrary+    return $ TestSetup RAW { readers, appenders, writers }+  shrink (TestSetup raw@RAW { readers, appenders, writers }) =+    [TestSetup raw { readers   = readers'   } | readers'   <- shrink readers  ] <>+    [TestSetup raw { appenders = appenders' } | appenders' <- shrink appenders] <>+    [TestSetup raw { writers   = writers'   } | writers'   <- shrink writers  ]++data ThreadDelays = ThreadDelays+    { beforeLockTime :: Int+      -- ^ How long the thread should wait before it starts to take the lock+    , withLockTime   :: Int+      -- ^ How long the thread should wait while holding the lock+    }+  deriving (Eq, Show)++instance Arbitrary ThreadDelays where+  arbitrary = do+    beforeLockTime <- choose (0, 1000)+    withLockTime   <- choose (0, 2000)+    return ThreadDelays { beforeLockTime, withLockTime }++{-------------------------------------------------------------------------------+  unsafe functions+-------------------------------------------------------------------------------}++prop_unsafe_actions :: (Action, Action, Action) -> Property+prop_unsafe_actions actions@(a1, a2, a3) = counterexample (show actions) $+  exploreSimTrace id action+      (\_ tr -> counterexample (ppTrace tr) . property . isRight . traceResult False $ tr)+  where+    action :: IOSim s ()+    action = do+      exploreRaces+      l <- new (0 :: Int)+      let c = \case+            Read -> bracket+                      (atomically (unsafeAcquireReadAccess l))+                      (const $ atomically (unsafeReleaseReadAccess l))+                      (say . ("Read: " <>) . show)+            Incr -> generalBracket+                      (unsafeAcquireWriteAccess l)+                      (\orig -> \case+                          ExitCaseSuccess s' -> unsafeReleaseWriteAccess l s'+                          _ -> unsafeReleaseWriteAccess l orig+                      )+                      (\s -> do+                          say ("Incr: " <> show s)+                          pure (s + 1)+                      ) >> pure ()+            Append -> generalBracket+                        (unsafeAcquireAppendAccess l)+                        (\orig -> \case+                            ExitCaseSuccess s' -> unsafeReleaseAppendAccess l s'+                            _ -> unsafeReleaseAppendAccess l orig+                        )+                        (\s -> do+                            say ("Append: " <> show s)+                            pure (s + 1)+                        ) >> pure ()+      t1 <- async $ c a1+      t2 <- async $ c a2+      t3 <- async $ c a3+      async (cancel t1) >>= wait+      (_ :: Either SomeException ()) <- waitCatch t1+      (_ :: Either SomeException ()) <- waitCatch t2+      (_ :: Either SomeException ()) <- waitCatch t3+      pure ()