packages feed

concurrent-resource-map (empty) → 0.1.0.0

raw patch · 7 files changed

+373/−0 lines, 7 filesdep +basedep +concurrent-resource-mapdep +containerssetup-changed

Dependencies added: base, concurrent-resource-map, containers, random, stm

Files

+ ChangeLog.md view
@@ -0,0 +1,3 @@+# Changelog for concurrent-resource-map++## Unreleased changes
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Mateusz Kowalczyk (c) 2020++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Mateusz Kowalczyk nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,16 @@+# concurrent-resource-map++User-counted resource map with automatic resource collection, aimed to+be used in concurrent setting.++Consider having some sort of resource that you need properly+initialised and cleaned up but only once and only for as long as there+are interested users (threads). Further, instead of only a single+resource, you want a collection of such resources, keyed on some+value.++This package implements a simple bracket-based scheme that manipulates+a resource map by counting number of initalisations and clean-ups: if+it detects that the cleanup is from the last user, it removes the+resource from the map all together. See the code/hackage for+documentation.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ concurrent-resource-map.cabal view
@@ -0,0 +1,57 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.33.0.+--+-- see: https://github.com/sol/hpack+--+-- hash: 88961aac04b5bb12c97a5cb17d6ffa38769426d569c941425645c1799284d0f3++name:           concurrent-resource-map+version:        0.1.0.0+synopsis:       Concurrent resource map+description:    Please see the README on GitHub at <https://github.com/Fuuzetsu/concurrent-resource-map#readme>+category:       Data+homepage:       https://github.com/Fuuzetsu/concurrent-resource-map#readme+bug-reports:    https://github.com/Fuuzetsu/concurrent-resource-map/issues+author:         Mateusz Kowalczyk+maintainer:     fuuzetsu@fuuzetsu.co.uk+copyright:      2020 Mateusz Kowalczyk+license:        BSD3+license-file:   LICENSE+tested-with:    GHC ==8.10.1 || ==8.8.3 || ==8.6.3 || ==8.4.4 || ==8.2.2 || ==8.0.2 || ==7.10.3+build-type:     Simple+extra-source-files:+    README.md+    ChangeLog.md++source-repository head+  type: git+  location: https://github.com/Fuuzetsu/concurrent-resource-map++library+  exposed-modules:+      Data.ConcurrentResourceMap+  other-modules:+      Paths_concurrent_resource_map+  hs-source-dirs:+      src+  ghc-options: -O2+  build-depends:+      base+    , containers+  default-language: Haskell2010++test-suite concurrent-resource-map-test+  type: exitcode-stdio-1.0+  main-is: Spec.hs+  other-modules:+      Paths_concurrent_resource_map+  hs-source-dirs:+      test+  ghc-options: -threaded -rtsopts -with-rtsopts=-N+  build-depends:+      base >=4.8 && <5+    , concurrent-resource-map+    , random+    , stm+  default-language: Haskell2010
+ src/Data/ConcurrentResourceMap.hs view
@@ -0,0 +1,178 @@+module Data.ConcurrentResourceMap+  ( ConcurrentResourceMap+  , newResourceMap+  , withSharedResource+  ) where++import Control.Exception+import qualified Control.Concurrent.MVar as MVar+import Control.Concurrent.MVar (MVar)+import qualified Data.Map.Strict as Map+import Data.Map.Strict (Map)++data Resource r = Uninitialised | Initialised !r++-- | Some resource with a count of the users (threads) using it.+--+-- Internal invariant: if users = 0 then resource = Uninitialised+data CountedResource r = CountedResource+  { users :: !Int+  , resource :: !(Resource r)+  }++-- | A map of shared resources @r@ keyed by @k@.+newtype ConcurrentResourceMap k r = C+  (MVar (Map k (MVar (CountedResource r))))++-- | Create an empty resource map.+newResourceMap :: Ord k => IO (ConcurrentResourceMap k r)+newResourceMap = fmap C $ MVar.newMVar Map.empty++-- | Use a resource that can be accessed concurrently via multiple+-- threads but is only initialised and destroyed on as-needed basis.+-- If number of users falls to 0, the resource is destroyed. If a new+-- user joins and resource is not available, it's created.+--+-- Calls to 'withSharedResource' can even be nested if you need access+-- to resources with different keys in the same map. Calling+-- 'withSharedResource' in a nested matter on same resource key should+-- have no real adverse effects either.+withSharedResource+  :: Ord k+  => ConcurrentResourceMap k r+  -- ^ Resource map. Create with 'newResourceMap'.+  -> k+   -- ^ Key for the resource. This allows you to have many of the same+   -- type of resource but separated: for example, one group of+   -- threads could be holding onto a logging handle to syslog while+   -- another could be holding a handle to a file.+  -> IO r+  -- ^ Initialise resource. Only ran if the resource is not yet+  -- initialised. Does not run in masked context so if you need to+  -- stop async exceptions, you should use 'mask' yourself. If the+  -- action fails (throws an exception), the user fails and we enter+  -- cleanup.+  -> (r -> IO ())+  -- ^ Destroy the resource if it was initialised. Ran by last alive+  -- user when it's exiting. Unlike initialisation, this _is_ ran in+  -- masked context. If this action fails (by throwing an exception+  -- itself), the resource will be assumed to be uninitialised and the+  -- exception will be re-thrown.+  --+  -- Therefore, if your cleanup can fail in a way that you have to+  -- know about/recover from, you should catch exceptions coming out+  -- out 'withSharedResource'. As you get reference to the resource+  -- in the @act@, you're able to store it/monitor it yourself and+  -- decide to take any appropriate actions in the future such as+  -- blocking other threads from running initialisation again until+  -- you've cleaned up the resource yourself.+  -> (r -> IO a)+  -- ^ Run an actionwith the initialised resource. Note that the+  -- availability of this resource only ensures that the user-given+  -- initialisers/destructors have been ran appropriate number of+  -- times: it of course makes no guarantees as to what the resource+  -- represents. For example, if it's a 'System.Process.ProcessHandle'+  -- or a database connection, there's no guarantee that the process+  -- is alive or that the database connection is still available. For+  -- resources that can dynamically fail, you should implement some+  -- sort of monitoring yourself.+  -> IO a+withSharedResource vm k initResource destroyResource act = bracket+  (addUser vm k)+  (removeUser vm k destroyResource)+  -- Don't leak the internal MVar to the user! This ensures that we+  -- can safely remove it from the resource map when we exit through+  -- 'removeUser'.+  actWithResource+  where+    actWithResource rvar = do+      r <- MVar.modifyMVar rvar $ \cr -> case cr of+        CountedResource { resource = Uninitialised } -> do+          r <- initResource+          return (cr { resource = Initialised r }, r)+        CountedResource { resource = Initialised r } ->+          return (cr, r)+      act r++-- | Adds a user at given key. If it's the first user, creates the+-- underlying map.+--+-- Should be used as initialising action in 'bracket' along with+-- 'removeUser'.+addUser+    :: Ord k+    => ConcurrentResourceMap k r -> k -> IO (MVar (CountedResource r))+addUser (C vm) k = MVar.modifyMVar vm $ \m -> case Map.lookup k m of+  -- We're the first user of this resource, make the counted+  -- resource.+  Nothing -> do+    v <- MVar.newMVar CountedResource { users = 1, resource = Uninitialised }+    return (Map.insert k v m, v)+  -- Other users already exist, increase the count only.+  Just vc -> do+    MVar.modifyMVar_ vc $ \cr ->+      return cr { users = users cr + 1 }+    return (m, vc)++-- | Remove user for the given key. If it's the last user, removes the+-- counted resource from the map completely.+--+-- Should be used as cleanup action in 'bracket' along with 'addUser'.+removeUser+  :: Ord k+  => ConcurrentResourceMap k r+  -> k+  -- ^ The resource from inside the map.+  -> (r -> IO ())+  -- ^ Destroy resource.+  -> MVar (CountedResource r)+  -- ^ Internal ref+  -> IO ()+removeUser (C vm) k destroyResource vc = do+  cr <- MVar.takeMVar vc+  let newCount = users cr - 1+      cr' = cr { users = newCount }+  case cr' of+    -- We're the last ones around, uninitialise.+    CountedResource { users = 0, resource = Initialised r } -> do+      let uninitialise = MVar.putMVar vc cr' { resource = Uninitialised }+          -- Destroy the resource if we can. If we fail, uninitialise it+          -- anyway and re-throw the exception.+          destroy = do+            destroyResource r `onException` uninitialise+            uninitialise+      -- We were the last ones around and whether we managed to+      -- destroy the resource or not, we want to remove the internal+      -- MVar from the resource map if we're still the last ones.+      destroy `finally` cleanFromResourceMap++    -- Resource is uninitialised or there are some other users+    -- around, simply replace the content with updated user counter.+    _ -> MVar.putMVar vc cr'+  where+    cleanFromResourceMap = MVar.modifyMVar_ vm $ \m -> case Map.lookup k m of+      -- The resource is not even in the map anymore. This could+      -- happen if since we decreased the count, a new user came in,+      -- increased the count, finished and cleaned up before we did.+      -- Seems unlikely but not impossible: in this case, there's+      -- nothing left for us to do.+      Nothing -> return m+      Just rvar -> do+        m'cr <- MVar.tryTakeMVar rvar+        case m'cr of+          -- We took the lock and see that there are still no+          -- remaining users. We must be the last ones and must be the+          -- only user holding a useful reference to the MVar as it+          -- can only be created and passed from addUser and it's+          -- internal API. Remove it from the map.+          Just CountedResource { users = 0 } -> return (Map.delete k m)+          -- We were able to take the MVar but there are some other+          -- users around again: we want to keep the original map. Put+          -- the value back and keep original the original mapping.+          Just cr -> do+            MVar.putMVar rvar cr+            return m+          -- We were either unable to take the MVar which means+          -- someone else has started to use it and so, we shouldn't+          -- delete it. Leave it as-is in the map.+          Nothing -> return m
+ test/Spec.hs view
@@ -0,0 +1,87 @@+module Main (main) where++import Control.Concurrent+import Control.Concurrent.STM+import Control.Exception+import Control.Monad+import Data.ConcurrentResourceMap+import Data.IORef+import Data.Typeable+import System.Random++-- | A 'resource' that asserts a number of concurrent initialisations.+-- If it's ever not 0 or 1, something went wrong and we want to fail.+newtype SingleInitResource = SIR (IORef Int)++newSIR :: IO SingleInitResource+newSIR = fmap SIR $ newIORef 0++initSIR :: SingleInitResource -> IO ()+initSIR (SIR r) = atomicModifyIORef' r $ \x -> case x of+  0 -> (1, ())+  n -> error $ "Tried to init SIR when it was non-one, it's: " ++ show n++destroySIR :: SingleInitResource -> IO ()+destroySIR (SIR r) = atomicModifyIORef' r $ \x -> case x of+  1 -> (0, ())+  n -> error $ "Tried to destroy SIR when it was non-one, it's: " ++ show n++checkInitialised :: SingleInitResource -> IO ()+checkInitialised (SIR r) = readIORef r >>= \x -> case x of+  1 -> return ()+  n -> error $ "Checking initialised SIR found non-one value, it's: " ++ show n++checkDestroyed :: SingleInitResource -> IO ()+checkDestroyed (SIR r) = readIORef r >>= \x -> case x of+  0 -> return ()+  n -> error $ "Checking destroyed SIR found non-zero value, it's: " ++ show n++data IntentionalFailure = IntentionalFailure+  deriving (Typeable, Show)+instance Exception IntentionalFailure++main :: IO ()+main = do+  m <- newResourceMap+  leftThreads <- newTVarIO numThreads+  sirs <- fmap (zip [0 :: Int .. ]) $ replicateM 10 newSIR++  replicateM_ numThreads $ forkIO $ do+    let act [] = return ()+        act ((k, sir):ss) = withSharedResource m k+          (fragileInit sir >> return sir)+          (\r -> smallDelay >> destroySIR r)+          -- Introduce small delay to allow for resource contention+          -- instead of most threads executing instantly creating a fairly+          -- sequenential test scenario.+          --+          -- Further, this tests that we can nest withSharedResource.+          (\r -> smallDelay >> checkInitialised r >> act ss)+        -- Always modify thread exit count when main action finishes.++        actIncr = act sirs `finally` atomically (modifyTVar' leftThreads pred)+        -- Silence intentional failures, no need to pollute test+        -- output.+        silenceIntentional IntentionalFailure = return ()+    actIncr `catch` silenceIntentional++  -- Wait for everything to finish.+  atomically $ readTVar leftThreads >>= check . (== 0)+  forM_ sirs $ \(_, sir) -> checkDestroyed sir+  where+    fragileInit :: SingleInitResource -> IO ()+    fragileInit sir = do+      smallDelay+      -- 25% of the time, throw an exception from initialiser+      shouldFail <- fmap (== 4) $ randomRIO (1, 4 :: Int)+      if shouldFail+        then throwIO IntentionalFailure+        else initSIR sir++    smallDelay :: IO ()+    smallDelay =+      -- Random delay up to 100 µs+      randomRIO (0, 100) >>= threadDelay++    numThreads :: Int+    numThreads = 50000