packages feed

resource-registry (empty) → 0.1.0.0

raw patch · 10 files changed

+2589/−0 lines, 10 filesdep +QuickCheckdep +basedep +bimap

Dependencies added: QuickCheck, base, bimap, containers, generics-sop, io-classes, mtl, nothunks, quickcheck-state-machine, resource-registry, si-timers, strict-mvar, strict-stm, tasty, tasty-quickcheck, tree-diff

Files

+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Revision history of strict-checked-vars++## 0.1.0.0 — 2024-10-22++- First release, extracted from [`ouroboros-consensus`](https://github.com/IntersectMBO/ouroboros-consensus).
+ LICENSE view
@@ -0,0 +1,202 @@++                                 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++   APPENDIX: How to apply the Apache License to your work.++      To apply the Apache License to your work, attach the following+      boilerplate notice, with the fields enclosed by brackets "[]"+      replaced with your own identifying information. (Don't include+      the brackets!)  The text should be enclosed in the appropriate+      comment syntax for the file format. We also recommend that a+      file or class name and description of purpose be included on the+      same "printed page" as the copyright notice for easier+      identification within third-party archives.++   Copyright [yyyy] [name of copyright owner]++   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.
+ NOTICE view
@@ -0,0 +1,13 @@+Copyright 2019-2023 Input Output Global Inc (IOG), INTERSECT 2023-2024.++   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,7 @@+# `resource-registry`++In some cases, the lifetime of a resource is not suitable for CPS and hence+can't use `bracket`-like functions, for example, the resource might be tracked+inside some other data structure. In this case, the container data structure can+be allocated in a resource registry together with the resources so that an+exception will deallocate the resources in a proper order.
+ resource-registry.cabal view
@@ -0,0 +1,95 @@+cabal-version: 3.0+name: resource-registry+version: 0.1.0.0+synopsis: Track allocated resources+description:+  When the scope of a @bracket@ doesn't enclose all uses of the resource, a+  'ResourceRegistry' can be used instead to capture the lifetime of those+  resources.++license: Apache-2.0+license-files:+  LICENSE+  NOTICE++author: IOG Engineering Team+maintainer: operations@iohk.io+copyright:+  2019-2023 Input Output Global Inc (IOG)+  2023-2024 INTERSECT++category: Control+build-type: Simple+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+extra-doc-files:+  CHANGELOG.md+  README.md++source-repository head+  type: git+  location: https://github.com/IntersectMBO/io-classes-extra+  subdir: resource-registry++source-repository this+  type: git+  location: https://github.com/IntersectMBO/io-classes-extra+  subdir: resource-registry+  tag: resource-registry-0.1.0.0++common warnings+  ghc-options:+    -Wall+    -Wcompat+    -Wincomplete-uni-patterns+    -Wincomplete-record-updates+    -Wpartial-fields+    -Widentities+    -Wredundant-constraints+    -Wmissing-export-lists+    -Wunused-packages+    -Wno-unticked-promoted-constructors++library+  import: warnings+  exposed-modules: Control.ResourceRegistry+  build-depends:+    base >=4.14 && <4.21,+    bimap ^>=0.5,+    containers >=0.6 && <0.8,+    io-classes ^>=1.5,+    mtl >=2.2 && <2.4,+    nothunks ^>=0.2,+    strict-stm ^>=1.5,++  hs-source-dirs: src+  default-language: Haskell2010+  default-extensions: ImportQualifiedPost++test-suite resource-registry-test+  import: warnings+  default-language: Haskell2010+  default-extensions: ImportQualifiedPost+  type: exitcode-stdio-1.0+  hs-source-dirs: test+  main-is: Main.hs+  other-modules:+    Test.Util.QSM+    Test.Util.SOP+    Test.Util.ToExpr++  build-depends:+    QuickCheck,+    base,+    containers,+    generics-sop,+    io-classes,+    mtl,+    quickcheck-state-machine:no-vendored-treediff,+    resource-registry,+    si-timers,+    strict-mvar,+    strict-stm,+    tasty,+    tasty-quickcheck,+    tree-diff,
+ src/Control/ResourceRegistry.hs view
@@ -0,0 +1,1497 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingVia #-}+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE QuantifiedConstraints #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE UndecidableInstances #-}++{-# OPTIONS_GHC -Wno-orphans            #-}++-- | Note on terminology: when thread A forks thread B, we will say that thread A+-- is the \"parent\" and thread B is the \"child\". No further relationship+-- between the two threads is implied by this terminology. In particular, note+-- that the child may outlive the parent. We will use \"fork\" and \"spawn\"+-- interchangeably.+--+-- = Motivation+--+-- Whenever we allocate resources, we must keep track of them so that we can+-- deallocate them when they are no longer required. The most important tool we+-- have to achieve this is 'bracket':+--+-- > bracket allocateResource releaseResource $ \r ->+-- >   .. use r ..+--+-- Often 'bracket' comes in the guise of a with-style combinator+--+-- > withResource $ \r ->+-- >   .. use r ..+--+-- Where this pattern is applicable, it should be used and there is no need to+-- use the 'ResourceRegistry'. However, 'bracket' introduces strict lexical+-- scoping: the resource is available inside the scope of the bracket, and+-- will be deallocated once we leave that scope. That pattern is sometimes+-- hard to use.+--+-- For example, suppose we have this interface to an SQL server+--+-- > query :: Query -> IO QueryHandle+-- > close :: QueryHandle -> IO ()+-- > next  :: QueryHandle -> IO Row+--+-- and suppose furthermore that we are writing a simple webserver that allows a+-- client to send multiple SQL queries, get rows from any open query, and close+-- queries when no longer required:+--+-- > server :: IO ()+-- > server = go Map.empty+-- >   where+-- >     go :: Map QueryId QueryHandle -> IO ()+-- >     go handles = getRequest >>= \case+-- >         New q -> do+-- >           h   <- query q                        -- allocate+-- >           qId <- generateQueryId+-- >           sendResponse qId+-- >           go $ Map.insert qId h handles+-- >         Close qId -> do+-- >           close (handles ! qId)                 -- release+-- >           go $ Map.delete qId handles+-- >         Next qId -> do+-- >           sendResponse =<< next (handles ! qId)+-- >           go handles+--+-- The server opens and closes query handles in response to client requests.+-- Restructuring this code to use 'bracket' would be awkward, but as it stands+-- this code does not ensure that resources get deallocated; for example, if+-- the server thread is killed ('killThread'), resources will be leaked.+--+-- Another, perhaps simpler, example is spawning threads. Threads too should+-- be considered to be resources that we should keep track of and deallocate+-- when they are no longer required, primarily because when we deallocate+-- (terminate) those threads they too will have a chance to deallocate /their/+-- resources. As for other resources, we have a with-style combinator for this+--+-- > withAsync $ \thread -> ..+--+-- Lexical scoping of threads is often inconvenient, however, more so than for+-- regular resources. The temptation is therefore to simply fork a thread and+-- forget about it, but if we are serious about resource deallocation this is+-- not an acceptable solution.+--+-- = The resource registry+--+-- The resource registry is essentially a piece of state tracking which+-- resources have been allocated. The registry itself is allocated with a+-- with-style combinator 'withRegistry', and when we leave that scope any+-- resources not yet deallocated will be released at that point. Typically+-- the registry is only used as a fall-back, ensuring that resources will+-- deallocated even in the presence of exceptions. For example, here's how+-- we might rewrite the above server example using a registry:+--+-- > server' :: IO ()+-- > server' =+-- >     withRegistry $ \registry -> go registry Map.empty+-- >   where+-- >     go :: ResourceRegistry IO+-- >        -> Map QueryId (ResourceKey, QueryHandle)+-- >        -> IO ()+-- >     go registry handles = getRequest >>= \case+-- >         New q -> do+-- >           (key, h) <- allocate registry (query q) close  -- allocate+-- >           qId      <- generateQueryId+-- >           sendResponse qId+-- >           go registry $ Map.insert qId (key, h) handles+-- >         Close qId -> do+-- >           release registry (fst (handles ! qId))         -- release+-- >           go registry $ Map.delete qId handles+-- >         Next qId -> do+-- >           sendResponse =<< next (snd (handles ! qId))+-- >           go registry handles+--+-- We allocate the query with the help of the registry, providing the registry+-- with the means to deallocate the query should that be required. We can /and+-- should/ still manually release resources also: in this particular example,+-- the (lexical) scope of the registry is the entire server thread, so delaying+-- releasing queries until we exit that scope will probably mean we hold on to+-- resources for too long. The registry is only there as a fall-back.+--+-- = Spawning threads+--+-- We already observed in the introduction that insisting on lexical scoping+-- for threads is often inconvenient, and that simply using+-- 'Control.Monad.Class.MonadFork.forkIO' is no solution as it means we might+-- leak resources. There is however another problem with+-- 'Control.Monad.Class.MonadFork.forkIO'. Consider this snippet:+--+-- > withRegistry $ \registry ->+-- >   r <- allocate registry allocateResource releaseResource+-- >   forkIO $ .. use r ..+--+-- It is easy to see that this code is problematic: we allocate a resource @r@,+-- then spawn a thread that uses @r@, and finally leave the scope of+-- 'withRegistry', thereby deallocating @r@ -- leaving the thread to run with+-- a now deallocated resource.+--+-- It is /only/ safe for threads to use a given registry, and/or its registered+-- resources, if the lifetime of those threads is tied to the lifetime of the+-- registry. There would be no problem with the example above if the thread+-- would be terminated when we exit the scope of 'withRegistry'.+--+-- The 'forkThread' combinator provided by the registry therefore does two+-- things: it allocates the thread as a resource in the registry, so that it can+-- kill the thread when releasing all resources in the registry. It also records+-- the thread ID in a set of known threads. Whenever the registry is accessed+-- from a thread /not/ in this set, the registry throws a runtime exception,+-- since such a thread might outlive the registry and hence its contents. The+-- intention is that this guards against dangerous patterns like the one above.+--+-- = Linking+--+-- When thread A spawns thread B using 'withAsync', the lifetime of B is tied+-- to the lifetime of A:+--+-- > withAsync .. $ \threadB -> ..+--+-- After all, when A exits the scope of the 'withAsync', thread B will be+-- killed. The reverse is however not true: thread B can terminate before+-- thread A. It is often useful for thread A to be able to declare a dependency+-- on thread B: if B somehow fails, that is, terminates with an exception, we+-- want that exception to be rethrown in thread A as well. A can achieve this+-- by /linking/ to B:+--+-- > withAsync .. $ \threadB -> do+-- >   link threadB+-- >   ..+--+-- Linking a parent to a child is however of limited value if the lifetime of+-- the child is not limited by the lifetime of the parent. For example, if A+-- does+--+-- > threadB <- async $ ..+-- > link threadB+--+-- and A terminates before B does, any exception thrown by B might be send to a+-- thread that no longer exists. This is particularly problematic when we start+-- chaining threads: if A spawns-and-links-to B which spawns-and-links-to C, and+-- C throws an exception, perhaps the intention is that this gets rethrown to B,+-- and then rethrown to A, terminating all three threads; however, if B has+-- terminated before the exception is thrown, C will throw the exception to a+-- non-existent thread and A is never notified.+--+-- For this reason, the registry's 'linkToRegistry' combinator does not link the+-- specified thread to the thread calling 'linkToRegistry', but rather to the+-- thread that created the registry. After all, the lifetime of threads spawned+-- with 'forkThread' can certainly exceed the lifetime of their parent threads,+-- but the lifetime of /all/ threads spawned using the registry will be limited+-- by the scope of that registry, and hence the lifetime of the thread that+-- created it. So, when we call 'linkToRegistry', the exception will be thrown+-- the thread that created the registry, which (if not caught) will cause that+-- that to exit the scope of 'withRegistry', thereby terminating all threads in+-- that registry.+--+-- = Combining the registry and with-style allocation+--+-- It is perfectly possible (indeed, advisable) to use 'bracket' and+-- bracket-like allocation functions alongside the registry, but note that the+-- usual caveats with 'bracket' and forking threads still applies. In+-- particular, spawning threads inside the 'bracket' that make use of the+-- bracketed resource is problematic; this is of course true whether or not a+-- registry is used.+--+-- In principle this also includes 'withAsync'; however, since 'withAsync'+-- results in a thread that is not known to the registry, such a thread will not+-- be able to use the registry (the registry would throw an unknown thread+-- exception, as described above). For this purpose we provide 'withThread';+-- 'withThread' (as opposed to 'forkThread') should be used when a parent thread+-- wants to handle exceptions in the child thread; see 'withThread' for+-- detailed discussion.+--+-- It is /also/ fine to includes nested calls to 'withRegistry'. Since the+-- lifetime of such a registry (and all resources within) is tied to the thread+-- calling 'withRegistry', which itself is tied to the "parent registry" in+-- which it was created, this creates a hierarchy of registries. It is of course+-- essential for compositionality that we should be able to create local+-- registries, but even if we do have easy access to a parent regisry, creating+-- a local one where possibly is useful as it limits the scope of the resources+-- created within, and hence their maximum lifetimes.++module Control.ResourceRegistry (+    -- * The resource registry proper+    Context+  , ResourceId+  , ResourceRegistry+    -- * Exceptions+  , RegistryClosedException (..)+  , ResourceRegistryThreadException+    -- * Creating and releasing the registry itself+  , bracketWithPrivateRegistry+  , registryThread+  , withRegistry+    -- * Allocating and releasing regular resources+  , ResourceKey+  , allocate+  , allocateEither+  , release+  , releaseAll+  , unsafeRelease+  , unsafeReleaseAll+    -- * Threads+  , Thread+  , cancelThread+  , forkLinkedThread+  , forkThread+  , linkToRegistry+  , threadId+  , waitAnyThread+  , waitThread+  , withThread+    -- * Temporary registry+  , TempRegistryException (..)+  , WithTempRegistry+  , allocateTemp+  , modifyWithTempRegistry+  , runInnerWithTempRegistry+  , runWithTempRegistry+    -- * Unsafe combinators primarily for testing+  , closeRegistry+  , countResources+  , unsafeNewRegistry+  ) where++import Control.Applicative ((<|>))+import Control.Concurrent.Class.MonadSTM.Strict+import Control.Exception (asyncExceptionFromException)+import Control.Monad+import Control.Monad.Class.MonadAsync+import Control.Monad.Class.MonadFork+import Control.Monad.Class.MonadThrow+import Control.Monad.Reader+import Control.Monad.State.Strict+import Data.Bifunctor+import Data.Bimap (Bimap)+import Data.Bimap qualified as Bimap+import Data.Either (partitionEithers)+import Data.Map.Strict (Map)+import Data.Map.Strict qualified as Map+import Data.Maybe (catMaybes, listToMaybe)+import Data.Set (Set)+import Data.Set qualified as Set+import Data.Void+import Data.Word (Word64)+import GHC.Generics (Generic)+import GHC.Stack (CallStack, HasCallStack)+import GHC.Stack qualified as GHC+import NoThunks.Class hiding (Context)++-- | Tracks resources during their lifetime.+data ResourceRegistry m = ResourceRegistry {+      -- | Context in which the registry was created+      registryContext :: !(Context m)++      -- | Registry state+    , registryState   :: !(StrictTVar m (RegistryState m))+    }+  deriving (Generic)++deriving instance (forall a. NoThunks a => NoThunks (StrictTVar m a))+               => NoThunks (ResourceRegistry m)++{-------------------------------------------------------------------------------+  Internal: registry state+-------------------------------------------------------------------------------}++-- | The age of a resource+--+-- Age here is represented by an meaningless number. The one and only property+-- that matters is that the age of resource A that was successfully allocated+-- before resource B was (in the same registry) will be greater than the age of+-- resource B.+--+-- For the current implementation, that property will be true unless the+-- registry lives long enough to have contained 2^64 separately allocated+-- resources.+--+-- This data is not exposed by the 'ResourceRegistry' interface.+newtype Age = Age Word64+  deriving stock   (Show)+  deriving newtype (Eq, Ord)+  deriving NoThunks via InspectHeapNamed "Age" Age++-- | The age of the first resource successfully allocated in a fresh registry+ageOfFirstResource :: Age+ageOfFirstResource = Age maxBound++-- | Map the age of the latest resource to be successfully allocated to the age+-- of the next resource to be successfully allocated in the same registry+nextYoungerAge :: Age -> Age+nextYoungerAge (Age n) = Age (n - 1)++-- | Internal registry state+data RegistryState m = RegistryState {+      -- | Forked threads+      registryThreads   :: !(KnownThreads m)++      -- | Currently allocated resources+      --+      -- INVARIANT: We record exactly the ages of currently allocated resources,+      -- @'Bimap.keys' . 'registryAges' = 'Map.keys' . 'registryResources'@.+    , registryResources :: !(Map ResourceId (Resource m))++      -- | Next available resource key+    , registryNextKey   :: !ResourceId++      -- | The age of each currently allocated resource+      --+      -- We use a 'Bimap' so we can maintain the keys in sorted order by age,+      -- which is necessary when closing the registry.+    , registryAges      :: !(Bimap ResourceId Age)++      -- | The age of the next resource+    , registryNextAge   :: !Age++      -- | Does the registry still accept new allocations?+      --+      -- See 'RegistryClosedException' for discussion.+    , registryStatus    :: !RegistryStatus+    }+  deriving (Generic, NoThunks)++-- | The currently allocated keys in youngest-to-oldest order+getYoungestToOldest :: RegistryState m -> [ResourceId]+getYoungestToOldest = map snd . Bimap.toAscListR . registryAges++-- | Threads known to the registry+--+-- This is the set of threads spawned using 'forkThread'. The lifetimes of all+-- of these threads are limited by the lifetime of the registry.+--+-- Does not include the thread ID of the thread that created the registry. After+-- all, this thread may well outlive the registry (though the registry cannot+-- outlive it).+--+-- Invariant (informal): the set of registered threads is a subset of the+-- registered resources ('registryResources'). (This invariant is temporarily+-- broken when we start a new thread in 'forkThread' but will be re-established+-- before that thread starts execution proper.)+newtype KnownThreads m = KnownThreads (Set (ThreadId m))+  deriving NoThunks via InspectHeapNamed "KnownThreads" (KnownThreads m)++-- | Status of the registry (open or closed)+data RegistryStatus =+    RegistryOpen++    -- | We record the 'CallStack' to the call to 'close+  | RegistryClosed !PrettyCallStack+  deriving (Generic, NoThunks)++-- | Resource key+--+-- Resource keys are tied to a particular registry.+data ResourceKey m = ResourceKey !(ResourceRegistry m) !ResourceId+  deriving Generic++deriving instance NoThunks (ResourceRegistry m)+               => NoThunks (ResourceKey m)++-- | Return the 'ResourceId' of a 'ResourceKey'.+resourceKeyId :: ResourceKey m -> ResourceId+resourceKeyId (ResourceKey _rr rid) = rid++-- | Resource ID+--+-- This uniquifying data is not exposed by the 'ResourceRegistry' interface.+newtype ResourceId = ResourceId Int+  deriving stock   (Show, Eq, Ord)+  deriving newtype (Enum, NoThunks)++-- | Information about a resource+data Resource m = Resource {+      -- | Context in which the resource was created+      resourceContext :: !(Context m)++      -- | Deallocate the resource+    , resourceRelease :: !(Release m)+    }+  deriving (Generic, NoThunks)++-- | Release the resource, return 'True' when the resource was actually+-- released, return 'False' when the resource was already released.+--+-- If unsure, returning 'True' is always fine.+newtype Release m = Release (m Bool)+  deriving NoThunks via OnlyCheckWhnfNamed "Release" (Release m)++releaseResource :: Resource m -> m Bool+releaseResource Resource{resourceRelease = Release f} = f++instance Show (Release m) where+  show _ = "<<release>>"++{-------------------------------------------------------------------------------+  Internal: pure functions on the registry state+-------------------------------------------------------------------------------}++modifyKnownThreads ::+     (Set (ThreadId m) -> Set (ThreadId m))+  -> KnownThreads m+  -> KnownThreads m+modifyKnownThreads f (KnownThreads ts) = KnownThreads (f ts)++-- | Auxiliary for functions that should be disallowed when registry is closed+unlessClosed ::+     State (RegistryState m) a+  -> State (RegistryState m) (Either PrettyCallStack a)+unlessClosed f = do+    status <- gets registryStatus+    case status of+      RegistryClosed closed -> return $ Left closed+      RegistryOpen          -> Right <$> f++-- | Allocate key for new resource+allocKey :: State (RegistryState m) (Either PrettyCallStack ResourceId)+allocKey = unlessClosed $ do+    nextKey <- gets registryNextKey+    modify $ \st -> st {registryNextKey = succ nextKey}+    return nextKey++-- | Insert new resource+insertResource ::+     ResourceId+  -> Resource m+  -> State (RegistryState m) (Either PrettyCallStack ())+insertResource key r = unlessClosed $ do+    modify $ \st -> st {+        registryResources = Map.insert key r (registryResources st)+      , registryAges      = Bimap.insert+                              key+                              (registryNextAge st)+                              (registryAges st)+      , registryNextAge   = nextYoungerAge (registryNextAge st)+      }++-- | Remove resource from the registry (if it exists)+removeResource :: ResourceId -> State (RegistryState m) (Maybe (Resource m))+removeResource key = state $ \st ->+    let (mbResource, resources') = Map.updateLookupWithKey+                                     (\_ _ -> Nothing)+                                     key+                                     (registryResources st)++        st' = st {+            registryResources = resources'+          , registryAges      = Bimap.delete key (registryAges st)+          }+    in  (mbResource, st')++-- | Insert thread into the set of known threads+insertThread :: MonadThread m => ThreadId m -> State (RegistryState m) ()+insertThread tid =+    modify $ \st -> st {+        registryThreads = modifyKnownThreads (Set.insert tid) $+                            registryThreads st+      }++-- | Remove thread from set of known threads+removeThread :: MonadThread m => ThreadId m -> State (RegistryState m) ()+removeThread tid =+    modify $ \st -> st {+        registryThreads = modifyKnownThreads (Set.delete tid) $+                            registryThreads st+      }++-- | Close the registry+--+-- Returns the keys currently allocated if the registry is not already closed.+--+-- POSTCONDITION: They are returned in youngest-to-oldest order.+close ::+     PrettyCallStack+  -> State (RegistryState m) (Either PrettyCallStack [ResourceId])+close closeCallStack = unlessClosed $ do+    modify $ \st -> st {registryStatus = RegistryClosed closeCallStack}+    gets getYoungestToOldest++-- | Convenience function for updating the registry state+updateState ::+     forall m a.+     MonadSTM m+  => ResourceRegistry m+  -> State (RegistryState m) a+  -> m a+updateState rr f =+    atomically $ stateTVar (registryState rr) (runState f)++-- | Attempt to allocate a resource in a registry which is closed+--+-- When calling 'closeRegistry' (typically, leaving the scope of+-- 'withRegistry'), all resources in the registry must be released. If a+-- concurrent thread is still allocating resources, we end up with a race+-- between the thread trying to allocate new resources and the registry trying+-- to free them all. To avoid this, before releasing anything, the registry will+-- record itself as closed. Any attempt by a concurrent thread to allocate a new+-- resource will then result in a 'RegistryClosedException'.+--+-- It is probably not particularly useful for threads to try and catch this+-- exception (apart from in a generic handler that does local resource cleanup).+-- The thread will anyway soon receive a 'Control.Exception.ThreadKilled'+-- exception.+data RegistryClosedException =+    forall m. MonadThread m => RegistryClosedException {+        -- | The context in which the registry was created+        registryClosedRegistryContext :: !(Context m)++        -- | Callstack to the call to 'closeRegistry'+        --+        -- Note that 'closeRegistry' can only be called from the same thread+        -- that created the registry.+      , registryClosedCloseCallStack  :: !PrettyCallStack++        -- | Context of the call resulting in the exception+      , registryClosedAllocContext    :: !(Context m)+      }++deriving instance Show RegistryClosedException+instance Exception RegistryClosedException++{-------------------------------------------------------------------------------+  Creating and releasing the registry itself+-------------------------------------------------------------------------------}++-- | Create a new registry+--+-- You are strongly encouraged to use 'withRegistry' instead.+-- Exported primarily for the benefit of tests.+unsafeNewRegistry ::+     (MonadSTM m, MonadThread m, HasCallStack)+  => m (ResourceRegistry m)+unsafeNewRegistry = do+    context  <- captureContext+    stateVar <- newTVarIO initState+    return ResourceRegistry {+          registryContext = context+        , registryState   = stateVar+        }+  where+    initState :: RegistryState m+    initState = RegistryState {+          registryThreads   = KnownThreads Set.empty+        , registryResources = Map.empty+        , registryNextKey   = ResourceId 1+        , registryAges      = Bimap.empty+        , registryNextAge   = ageOfFirstResource+        , registryStatus    = RegistryOpen+        }++-- | Close the registry+--+-- This can only be called from the same thread that created the registry.+-- This is a no-op if the registry is already closed.+--+-- This entire function runs with exceptions masked, so that we are not+-- interrupted while we release all resources.+--+-- Resources will be allocated from young to old, so that resources allocated+-- later can safely refer to resources created earlier.+--+-- The release functions are run in the scope of an exception handler, so that+-- if releasing one resource throws an exception, we still attempt to release+-- the other resources. Should we catch an exception whilst we close the+-- registry, we will rethrow it after having attempted to release all resources.+-- If there is more than one, we will pick a random one to rethrow, though we+-- will prioritize asynchronous exceptions over other exceptions. This may be+-- important for exception handlers that catch all-except-asynchronous+-- exceptions.+closeRegistry ::+     (MonadMask m, MonadThread m, MonadSTM m, HasCallStack)+  => ResourceRegistry m+  -> m ()+closeRegistry rr = mask_ $ do+    context <- captureContext+    unless (contextThreadId context == contextThreadId (registryContext rr)) $+      throwIO $ ResourceRegistryClosedFromWrongThread {+          resourceRegistryCreatedIn = registryContext rr+        , resourceRegistryUsedIn    = context+        }++    -- Close the registry so that we cannot allocate any further resources+    alreadyClosed <- updateState rr $ close (contextCallStack context)+    case alreadyClosed of+      Left _ ->+        return ()+      Right keys -> do+        -- At this point we have not /removed/ any elements from the map,+        -- allowing concurrent threads to do their own cleanup of resources+        -- (this may for instance be important if a thread deallocates its+        -- resources in a particular order -- note that cancelling a thread+        -- is a synchronous operation, so we will wait for it to finish+        -- releasing its resources.)+        -- /If/ a concurrent thread does some cleanup, then some of the calls+        -- to 'release' that we do here might be no-ops.+       void $ releaseResources rr keys release++-- | Helper for 'closeRegistry', 'releaseAll', and 'unsafeReleaseAll': release+-- the resources allocated with the given 'ResourceId's.+--+-- Returns the contexts of the resources that were actually released.+releaseResources ::+     MonadCatch m+  => ResourceRegistry m+  -> [ResourceId]+     -- ^ PRECONDITION: The currently allocated keys,+     -- youngest-to-oldest+  -> (ResourceKey m -> m (Maybe (Context m)))+     -- ^ How to release the resource, e.g., 'release' or+     -- 'unsafeRelease'.+  ->  m [Context m]+releaseResources rr sortedKeys releaser = do+    (exs, mbContexts) <- fmap partitionEithers $+      forM sortedKeys $ try . releaser . ResourceKey rr++    case prioritize exs of+      Nothing -> return (catMaybes mbContexts)+      Just e  -> throwIO e+  where+    prioritize :: [SomeException] -> Maybe SomeException+    prioritize =+          (\(asyncEx, otherEx) -> listToMaybe asyncEx <|> listToMaybe otherEx)+        . first catMaybes+        . unzip+        . map (\e -> (asyncExceptionFromException e, e))++-- | Create a new registry+--+-- See documentation of 'ResourceRegistry' for a detailed discussion.+withRegistry ::+     (MonadSTM m, MonadMask m, MonadThread m, HasCallStack)+  => (ResourceRegistry m -> m a)+  -> m a+withRegistry = bracket unsafeNewRegistry closeRegistry++-- | Create a new private registry for use by a bracketed resource+--+-- Use this combinator as a more specific and easier-to-maintain alternative to+-- the following.+--+-- > 'withRegistry' $ \rr ->+-- >   'bracket' (newFoo rr) closeFoo $ \foo ->+-- >     (... rr does not occur in this scope ...)+--+-- NB The scoped body can use `withRegistry` if it also needs its own, separate+-- registry.+--+-- Use this combinator to emphasize that the registry is private to (ie only+-- used by and/or via) the bracketed resource and that it thus has nearly the+-- same lifetime. This combinator ensures the following specific invariants+-- regarding lifetimes and order of releases.+--+-- o The registry itself is older than the bracketed resource.+--+-- o The only registered resources older than the bracketed resource were+--   allocated in the registry by the function that allocated the bracketed+--   resource.+--+-- o Because of the older resources, the bracketed resource is itself also+--   registered in the registry; that's the only way we can be sure to release+--   all resources in the right order.+--+-- NB Because the registry is private to the resource, the @a@ type could save+-- the handle to @registry@ and safely close the registry if the scoped body+-- calls @closeA@ before the bracket ends. Though we have not used the type+-- system to guarantee that the interface of the @a@ type cannot leak the+-- registry to the body, this combinator does its part to keep the registry+-- private to the bracketed resource.+--+-- See documentation of 'ResourceRegistry' for a more general discussion.+bracketWithPrivateRegistry ::+     (MonadSTM m, MonadMask m, MonadThread m, HasCallStack)+  => (ResourceRegistry m -> m a)+  -> (a -> m ())  -- ^ Release the resource+  -> (a -> m r)+  -> m r+bracketWithPrivateRegistry newA closeA body =+    withRegistry $ \registry -> do+      (_key, a) <- allocate registry (\_key -> newA registry) closeA+      body a++{-------------------------------------------------------------------------------+  Temporary registry+-------------------------------------------------------------------------------}++-- | Run an action with a temporary resource registry.+--+-- When allocating resources that are meant to end up in some final state,+-- e.g., stored in a 'Control.Monad.Class.MonadSTM.TVar', after which they are+-- guaranteed to be released correctly, it is possible that an exception is+-- thrown after allocating such a resource, but before it was stored in the+-- final state. In that case, the resource would be leaked.+-- 'runWithTempRegistry' solves that problem.+--+-- When no exception is thrown before the end of 'runWithTempRegistry', the+-- user must have transferred all the resources it allocated to their final+-- state. This means that these resources don't have to be released by the+-- temporary registry anymore, the final state is now in charge of releasing+-- them.+--+-- In case an exception is thrown before the end of 'runWithTempRegistry',+-- /all/ resources allocated in the temporary registry will be released.+--+-- Resources must be allocated using 'allocateTemp'.+--+-- To make sure that the user doesn't forget to transfer a resource to the+-- final state @st@, the user must pass a function to 'allocateTemp' that+-- checks whether a given @st@ contains the resource, i.e., whether the+-- resource was successfully transferred to its final destination.+--+-- When no exception is thrown before the end of 'runWithTempRegistry', we+-- check whether all allocated resources have been transferred to the final+-- state @st@. If there's a resource that hasn't been transferred to the final+-- state /and/ that hasn't be released or closed before (see the release+-- function passed to 'allocateTemp'), a 'TempRegistryRemainingResource'+-- exception will be thrown.+--+-- For that reason, 'WithTempRegistry' is parameterised over the final state+-- type @st@ and the given 'WithTempRegistry' action must return the final+-- state.+--+-- NOTE: we explicitly don't let 'runWithTempRegistry' return the final state,+-- because the state /must/ have been stored somewhere safely, transferring+-- the resources, before the temporary registry is closed.+runWithTempRegistry ::+     (MonadSTM m, MonadMask m, MonadThread m, HasCallStack)+  => WithTempRegistry st m (a, st)+  -> m a+runWithTempRegistry m = withRegistry $ \rr -> do+    varTransferredTo <- newTVarIO mempty+    let tempRegistry = TempRegistry {+            tempResourceRegistry = rr+          , tempTransferredTo    = varTransferredTo+          }+    (a, st) <- runReaderT (unWithTempRegistry m) tempRegistry+    -- We won't reach this point if an exception is thrown, so we won't check+    -- for remaining resources in that case.+    --+    -- No need to mask here, whether we throw the async exception or+    -- 'TempRegistryRemainingResource' doesn't matter.+    transferredTo <- readTVarIO varTransferredTo+    untrackTransferredTo rr transferredTo st++    context <- captureContext+    remainingResources <- releaseAllHelper rr context release++    whenJust (listToMaybe remainingResources) $ \remainingResource ->+      throwIO $ TempRegistryRemainingResource {+          tempRegistryContext  = registryContext rr+        , tempRegistryResource = remainingResource+        }+    return a++    where+        whenJust (Just x) f = f x+        whenJust Nothing _  = pure ()++-- | Embed a self-contained 'WithTempRegistry' computation into a larger one.+--+-- The internal 'WithTempRegistry' is effectively passed to+-- 'runWithTempRegistry'. It therefore must have no dangling resources, for+-- example. This is the meaning of /self-contained/ above.+--+-- The key difference beyond 'runWithTempRegistry' is that the resulting+-- composite resource is also guaranteed to be registered in the outer+-- 'WithTempRegistry' computation's registry once the inner registry is closed.+-- Combined with the following assumption, this establishes the invariant that+-- all resources are (transitively) in a temporary registry.+--+-- As the resource might require some implementation details to be closed, the+-- function to close it will also be provided by the inner computation.+--+-- ASSUMPTION: closing @res@ closes every resource contained in @innerSt@+--+-- NOTE: In the current implementation, there will be a brief moment where the+-- inner registry still contains the inner computation's resources and also the+-- outer registry simultaneously contains the new composite resource. If an+-- async exception is received at that time, then the inner resources will be+-- closed and then the composite resource will be closed. This means there's a+-- risk of /double freeing/, which can be harmless if anticipated.+runInnerWithTempRegistry ::+     forall innerSt st m res a.+     (MonadSTM m, MonadMask m, MonadThread m)+  => WithTempRegistry innerSt m (a, innerSt, res)+     -- ^ The embedded computation; see ASSUMPTION above+  -> (res -> m Bool)+     -- ^ How to free; same as for 'allocateTemp'+  -> (st -> res -> Bool)+     -- ^ How to check; same as for 'allocateTemp'+  -> WithTempRegistry st m a+runInnerWithTempRegistry inner free isTransferred = do+    outerTR <- WithTempRegistry ask++    lift $ runWithTempRegistry $ do+      (a, innerSt, res) <- inner++      -- Allocate in the outer layer.+      _ <-   withFixedTempRegistry outerTR+           $ allocateTemp (return res) free isTransferred++      -- TODO This point here is where an async exception could cause both the+      -- inner resources to be closed and the outer resource to be closed later.+      --+      -- If we want to do better than that, we'll need a variant of+      -- 'runWithTempRegistry' that lets us perform some action with async+      -- exceptions masked "at the same time" it closes its registry.++      -- Note that everything in `inner` allocated via `allocateTemp` must+      -- either be closed or else present in `innerSt` by this point --+      -- `runWithTempRegistry` would have thrown if not.+      pure (a, innerSt)+  where+    withFixedTempRegistry ::+           TempRegistry     st      m+        -> WithTempRegistry st      m res+        -> WithTempRegistry innerSt m res+    withFixedTempRegistry env (WithTempRegistry (ReaderT f)) =+      WithTempRegistry $ ReaderT $ \_ -> f env++-- | When 'runWithTempRegistry' exits successfully while there are still+-- resources remaining in the temporary registry that haven't been transferred+-- to the final state.+data TempRegistryException =+    forall m. MonadThread m => TempRegistryRemainingResource {+        -- | The context in which the temporary registry was created.+        tempRegistryContext  :: !(Context m)++        -- | The context in which the resource was allocated that was not+        -- transferred to the final state.+      , tempRegistryResource :: !(Context m)+      }++deriving instance Show TempRegistryException+instance Exception TempRegistryException++-- | Given a final state, return the 'ResourceId's of the resources that have+-- been /transferred to/ that state.+newtype TransferredTo st = TransferredTo {+      runTransferredTo :: st -> Set ResourceId+    }+  deriving newtype (Semigroup, Monoid)+  deriving NoThunks via OnlyCheckWhnfNamed "TransferredTo" (TransferredTo st)++-- | The environment used to run a 'WithTempRegistry' action.+data TempRegistry st m = TempRegistry {+      tempResourceRegistry :: !(ResourceRegistry m)+    , tempTransferredTo    :: !(StrictTVar m (TransferredTo st))+      -- ^ Used as a @Writer@.+    }++-- | An action with a temporary registry in scope, see 'runWithTempRegistry'+-- for more details.+--+-- The most important function to run in this monad is 'allocateTemp'.+newtype WithTempRegistry st m a = WithTempRegistry {+      unWithTempRegistry :: ReaderT (TempRegistry st m) m a+    }+  deriving newtype ( Functor+                   , Applicative+                   , Monad+                   , MonadThrow+                   , MonadCatch+                   , MonadMask+                   )++instance MonadTrans (WithTempRegistry st) where+  lift = WithTempRegistry . lift++instance MonadState s m => MonadState s (WithTempRegistry st m) where+  state = WithTempRegistry . state++-- | Untrack all the resources from the registry that have been transferred to+-- the given state.+--+-- Untracking a resource means removing it from the registry without releasing+-- it.+--+-- NOTE: does not check that it's called by the same thread that allocated the+-- resources, as it's an internal function only used in 'runWithTempRegistry'.+untrackTransferredTo ::+     MonadSTM m+  => ResourceRegistry m+  -> TransferredTo st+  -> st+  -> m ()+untrackTransferredTo rr transferredTo st =+    updateState rr $ mapM_ removeResource rids+  where+    rids = runTransferredTo transferredTo st++-- | Allocate a resource in a temporary registry until it has been transferred+-- to the final state @st@. See 'runWithTempRegistry' for more details.+allocateTemp ::+     (MonadSTM m, MonadMask m, MonadThread m, HasCallStack)+  => m a+     -- ^ Allocate the resource+  -> (a -> m Bool)+     -- ^ Release the resource, return 'True' when the resource was actually+     -- released, return 'False' when the resource was already released.+     --+     -- Note that it is safe to always return 'True' when unsure.+  -> (st -> a -> Bool)+     -- ^ Check whether the resource is in the given state+  -> WithTempRegistry st m a+allocateTemp alloc free isTransferred = WithTempRegistry $ do+    TempRegistry rr varTransferredTo <- ask+    (key, a) <- lift (mustBeRight <$>+      allocateEither rr (fmap Right . const alloc) free)+    lift $ atomically $ modifyTVar varTransferredTo $ mappend $+      TransferredTo $ \st ->+        if isTransferred st a+        then Set.singleton (resourceKeyId key)+        else Set.empty+    return a++-- | Higher level API on top of 'runWithTempRegistry': modify the given @st@,+-- allocating resources in the process that will be transferred to the+-- returned @st@.+modifyWithTempRegistry ::+     forall m st a.+     (MonadSTM m, MonadMask m, MonadThread m)+  => m st                                 -- ^ Get the state+  -> (st -> ExitCase st -> m ())          -- ^ Store the new state+  -> StateT st (WithTempRegistry st m) a  -- ^ Modify the state+  -> m a+modifyWithTempRegistry getSt putSt modSt = runWithTempRegistry $+    fst <$> generalBracket (lift getSt) transfer mutate+  where+    transfer :: st -> ExitCase (a, st) -> WithTempRegistry st m ()+    transfer initSt ec = lift $ putSt initSt (snd <$> ec)++    mutate :: st -> WithTempRegistry st m (a, st)+    mutate = runStateT modSt++{-------------------------------------------------------------------------------+  Simple queries on the registry+-------------------------------------------------------------------------------}++-- | The thread that created the registry+registryThread :: ResourceRegistry m -> ThreadId m+registryThread = contextThreadId . registryContext++-- | Number of currently allocated resources+--+-- Primarily for the benefit of testing.+countResources :: MonadSTM m => ResourceRegistry m -> m Int+countResources rr = atomically $ aux <$> readTVar (registryState rr)+  where+    aux :: RegistryState m -> Int+    aux = Map.size . registryResources++{-------------------------------------------------------------------------------+  Allocating resources+-------------------------------------------------------------------------------}++-- | Allocate new resource+--+-- The allocation function will be run with asynchronous exceptions masked. This+-- means that the resource allocation must either be fast or else interruptible;+-- see "Dealing with Asynchronous Exceptions during Resource Acquisition"+-- <http://www.well-typed.com/blog/97/> for details.+allocate ::+     forall m a.+     (MonadSTM m, MonadMask m, MonadThread m, HasCallStack)+  => ResourceRegistry m+  -> (ResourceId -> m a)+  -> (a -> m ())  -- ^ Release the resource+  -> m (ResourceKey m, a)+allocate rr alloc free = mustBeRight <$>+    allocateEither rr (fmap Right . alloc) (\a -> free a >> return True)++-- | Generalization of 'allocate' for allocation functions that may fail+allocateEither ::+     forall m e a.+     (MonadSTM m, MonadMask m, MonadThread m, HasCallStack)+  => ResourceRegistry m+  -> (ResourceId -> m (Either e a))+  -> (a -> m Bool)+     -- ^ Release the resource, return 'True' when the resource+     -- hasn't been released or closed before.+  -> m (Either e (ResourceKey m, a))+allocateEither rr alloc free = do+    context <- captureContext+    ensureKnownThread rr context+    -- We check if the registry has been closed when we allocate the key, so+    -- that we can avoid needlessly allocating the resource.+    mKey <- updateState rr allocKey+    case mKey of+      Left closed ->+        throwRegistryClosed rr context closed+      Right key -> mask_ $ do+        ma <- alloc key+        case ma of+          Left  e -> return $ Left e+          Right a -> do+            -- TODO: Might want to have an exception handler around this call to+            -- 'updateState' just in case /that/ throws an exception.+            inserted <- updateState rr $+                 insertResource key (mkResource context a)+            case inserted of+              Left closed -> do+                -- Despite the earlier check, it's possible that the registry+                -- got closed after we allocated a new key but before we got a+                -- chance to register the resource. In this case, we must+                -- deallocate the resource again before throwing the exception.+                void $ free a+                throwRegistryClosed rr context closed+              Right () ->+                return $ Right (ResourceKey rr key, a)+  where+    mkResource :: Context m -> a -> Resource m+    mkResource context a = Resource {+          resourceContext = context+        , resourceRelease = Release $ free a+        }++throwRegistryClosed ::+     (MonadThrow m, MonadThread m)+  => ResourceRegistry m+  -> Context m+  -> PrettyCallStack+  -> m x+throwRegistryClosed rr context closed = throwIO RegistryClosedException {+      registryClosedRegistryContext = registryContext rr+    , registryClosedCloseCallStack  = closed+    , registryClosedAllocContext    = context+    }++-- | Release resource+--+-- This deallocates the resource and removes it from the registry. It will be+-- the responsibility of the caller to make sure that the resource is no longer+-- used in any thread.+--+-- The deallocation function is run with exceptions masked, so that we are+-- guaranteed not to remove the resource from the registry without releasing it.+--+-- Releasing an already released resource is a no-op.+--+-- When the resource has not been released before, its context is returned.+release ::+     (MonadMask m, MonadSTM m, MonadThread m, HasCallStack)+  => ResourceKey m+  -> m (Maybe (Context m))+release key@(ResourceKey rr _) = do+    context <- captureContext+    ensureKnownThread rr context+    unsafeRelease key++-- | Unsafe version of 'release'+--+-- The only difference between 'release' and 'unsafeRelease' is that the latter+-- does not insist that it is called from a thread that is known to the+-- registry. This is dangerous, because it implies that there is a thread with+-- access to a resource which may be deallocated before that thread is+-- terminated. Of course, we can't detect all such situations (when the thread+-- merely uses a resource but does not allocate or release we can't tell), but+-- normally when we /do/ detect this we throw an exception.+--+-- This function should only be used if the above situation can be ruled out+-- or handled by other means.+unsafeRelease ::+     (MonadMask m, MonadSTM m)+  => ResourceKey m+  -> m (Maybe (Context m))+unsafeRelease (ResourceKey rr rid) = do+    mask_ $ do+      mResource <- updateState rr $ removeResource rid+      case mResource of+        Nothing       -> return Nothing+        Just resource -> do+          actuallyReleased <- releaseResource resource+          return $+            if actuallyReleased+            then Just (resourceContext resource)+            else Nothing++-- | Release all resources in the 'ResourceRegistry' without closing.+--+-- See 'closeRegistry' for more details.+releaseAll ::+     (MonadMask m, MonadSTM m, MonadThread m, HasCallStack)+  => ResourceRegistry m+  -> m ()+releaseAll rr = do+    context <- captureContext+    unless (contextThreadId context == contextThreadId (registryContext rr)) $+      throwIO $ ResourceRegistryClosedFromWrongThread {+          resourceRegistryCreatedIn = registryContext rr+        , resourceRegistryUsedIn    = context+        }+    void $ releaseAllHelper rr context release++-- | This is to 'releaseAll' what 'unsafeRelease' is to 'release': we do not+-- insist that this funciton is called from a thread that is known to the+-- registry. See 'unsafeRelease' for why this is dangerous.+unsafeReleaseAll ::+     (MonadMask m, MonadSTM m, MonadThread m, HasCallStack)+  => ResourceRegistry m+  -> m ()+unsafeReleaseAll rr = do+    context <- captureContext+    void $ releaseAllHelper rr context unsafeRelease++-- | Internal helper used by 'releaseAll' and 'unsafeReleaseAll'.+releaseAllHelper ::+     (MonadMask m, MonadSTM m, MonadThread m)+  => ResourceRegistry m+  -> Context m+  -> (ResourceKey m -> m (Maybe (Context m)))+     -- ^ How to release a resource+  -> m [Context m]+releaseAllHelper rr context releaser = mask_ $ do+    mKeys <- updateState rr $ unlessClosed $ gets getYoungestToOldest+    case mKeys of+      Left closed -> throwRegistryClosed rr context closed+      Right keys  -> releaseResources rr keys releaser++{-------------------------------------------------------------------------------+  Threads+-------------------------------------------------------------------------------}++-- | Thread+--+-- The internals of this type are not exported.+data Thread m a = MonadThread m => Thread {+      -- | The underlying @async@ thread id+      threadId         :: !(ThreadId m)+    , threadResourceId :: !ResourceId+    , threadAsync      :: !(Async m a)+    , threadRegistry   :: !(ResourceRegistry m)+    }+  deriving NoThunks via OnlyCheckWhnfNamed "Thread" (Thread m a)++-- | 'Eq' instance for 'Thread' compares 'threadId' only.+instance MonadThread m => Eq (Thread m a) where+  Thread{threadId = a} == Thread{threadId = b} = a == b++-- | Cancel a thread+--+-- This is a synchronous operation: the thread will have terminated when this+-- function returns.+--+-- Uses 'uninterruptibleCancel' because that's what 'withAsync' does.+cancelThread :: MonadAsync m => Thread m a -> m ()+cancelThread = uninterruptibleCancel . threadAsync++-- | Wait for thread to terminate and return its result.+--+-- If the thread throws an exception, this will rethrow that exception.+--+-- NOTE: If A waits on B, and B is linked to the registry, and B throws an+-- exception, then A might /either/ receive the exception thrown by B /or/+-- the 'Control.Exception.ThreadKilled' exception thrown by the registry.+waitThread :: MonadAsync m => Thread m a -> m a+waitThread = wait . threadAsync++-- | Lift 'waitAny' to 'Thread'+waitAnyThread :: forall m a. MonadAsync m => [Thread m a] -> m a+waitAnyThread ts = snd <$> waitAny (map threadAsync ts)++-- | Fork a new thread+forkThread ::+     forall m a.+     (MonadMask m, MonadAsync m, HasCallStack)+  => ResourceRegistry m+  -> String  -- ^ Label for the thread+  -> m a+  -> m (Thread m a)+forkThread rr label body = snd <$>+    allocate rr (\key -> mkThread key <$> async (body' key)) cancelThread+  where+    mkThread :: ResourceId -> Async m a -> Thread m a+    mkThread rid child = Thread {+          threadId         = asyncThreadId child+        , threadResourceId = rid+        , threadAsync      = child+        , threadRegistry   = rr+        }++    body' :: ResourceId -> m a+    body' rid = do+        me <- myThreadId+        labelThread me label+        (registerThread me >> body) `finally` unregisterThread me rid++    -- Register the thread+    --+    -- We must add the thread to the list of known threads before the thread+    -- will start to use the registry.+    registerThread :: ThreadId m -> m ()+    registerThread tid = updateState rr $ insertThread tid++    -- Unregister the thread+    --+    -- Threads are the only kinds of resources that "deallocate themselves".+    -- We remove the thread from the resources as well as the set of known+    -- threads, so that these datastructures do not grow without bound.+    --+    -- This runs with asynchronous exceptions masked (due to 'finally'),+    -- though for the current implementation of 'unregisterThread' this+    -- makes no difference.+    unregisterThread :: ThreadId m -> ResourceId -> m ()+    unregisterThread tid rid =+        updateState rr $ do+          removeThread tid+          void $ removeResource rid++-- | Bracketed version of 'forkThread'+--+-- The analogue of 'withAsync' for the registry.+--+-- Scoping thread lifetime using 'withThread' is important when a parent+-- thread wants to link to a child thread /and handle any exceptions arising+-- from the link/:+--+-- > let handleLinkException :: ExceptionInLinkedThread -> m ()+-- >     handleLinkException = ..+-- > in handle handleLinkException $+-- >      withThread registry codeInChild $ \child ->+-- >        ..+--+-- instead of+--+-- > handle handleLinkException $ do  -- PROBABLY NOT CORRECT!+-- >   child <- forkThread registry codeInChild+-- >   ..+--+-- where the parent may exit the scope of the exception handler before the child+-- terminates. If the lifetime of the child cannot be limited to the lifetime of+-- the parent, the child should probably be linked to the registry instead and+-- the thread that spawned the registry should handle any exceptions.+--+-- Note that in /principle/ there is no problem in using 'withAsync' alongside a+-- registry. After all, in a pattern like+--+-- > withRegistry $ \registry ->+-- >   ..+-- >   withAsync (.. registry ..) $ \async ->+-- >     ..+--+-- the async will be cancelled when leaving the scope of 'withAsync' and so+-- that reference to the registry, or indeed any of the resources inside the+-- registry, is safe. However, the registry implements a sanity check that the+-- registry is only used from known threads. This is useful: when a thread that+-- is not known to the registry (in other words, whose lifetime is not tied to+-- the lifetime of the registry) spawns a resource in that registry, that+-- resource may well be deallocated before the thread terminates, leading to+-- undefined and hard to debug behaviour (indeed, whether or not this results in+-- problems may well depend on precise timing); an exception that is thrown when+-- /allocating/ the resource is (more) deterministic and easier to debug.+-- Unfortunately, it means that the above pattern is not applicable, as the+-- thread spawned by 'withAsync' is not known to the registry, and so if it were+-- to try to use the registry, the registry would throw an error (even though+-- this pattern is actually safe). This situation is not ideal, but for now we+-- merely provide an alternative to 'withAsync' that /does/ register the thread+-- with the registry.+--+-- NOTE: Threads that are spawned out of the user's control but that must still+-- make use of the registry can use the unsafe API. This should be used with+-- caution, however.+withThread ::+     (MonadMask m, MonadAsync m)+  => ResourceRegistry m+  -> String  -- ^ Label for the thread+  -> m a+  -> (Thread m a -> m b)+  -> m b+withThread rr label body = bracket (forkThread rr label body) cancelThread++-- | Link specified 'Thread' to the (thread that created) the registry+linkToRegistry :: (MonadAsync m, MonadFork m, MonadMask m) => Thread m a -> m ()+linkToRegistry t = linkTo (registryThread $ threadRegistry t) (threadAsync t)++-- | Fork a thread and link to it to the registry.+--+-- This function is just a convenience.+forkLinkedThread ::+     (MonadAsync m, MonadFork m, MonadMask m, HasCallStack)+  => ResourceRegistry m+  -> String  -- ^ Label for the thread+  -> m a+  -> m (Thread m a)+forkLinkedThread rr label body = do+    t <- forkThread rr label body+    -- There is no race condition here between the new thread throwing an+    -- exception and the 'linkToRegistry': if the thread /already/ threw the+    -- exception when we link it, the exception will be raised immediately+    -- (see 'linkTo' for details).+    linkToRegistry t+    return t++{-------------------------------------------------------------------------------+  Check that registry is used from known thread+-------------------------------------------------------------------------------}++ensureKnownThread ::+     forall m.+     (MonadThrow m, MonadThread m, MonadSTM m)+  => ResourceRegistry m+  -> Context m+  -> m ()+ensureKnownThread rr context = do+    isKnown <- checkIsKnown+    unless isKnown $+      throwIO $ ResourceRegistryUsedFromUntrackedThread {+                   resourceRegistryCreatedIn = registryContext rr+                 , resourceRegistryUsedIn    = context+                 }+  where+    checkIsKnown :: m Bool+    checkIsKnown+      | contextThreadId context == contextThreadId (registryContext rr) =+          return True+      | otherwise = atomically $ do+          KnownThreads ts <- registryThreads <$> readTVar (registryState rr)+          return $ contextThreadId context `Set.member` ts++-- | Registry used from untracked threads+--+-- If this exception is raised, it indicates a bug in the caller.+data ResourceRegistryThreadException =+    -- | If the registry is used from an untracked thread, we cannot do proper+    -- reference counting. The following threads are /tracked/: the thread+    -- that spawned the registry and all threads spawned by the registry.+    forall m. MonadThread m => ResourceRegistryUsedFromUntrackedThread {+          -- | Information about the context in which the registry was created+          resourceRegistryCreatedIn :: !(Context m)++          -- | The context in which it was used+        , resourceRegistryUsedIn    :: !(Context m)+        }++    -- | Registry closed from different threat than that created it+  | forall m. MonadThread m => ResourceRegistryClosedFromWrongThread {+          -- | Information about the context in which the registry was created+          resourceRegistryCreatedIn :: !(Context m)++          -- | The context in which it was used+        , resourceRegistryUsedIn    :: !(Context m)+        }++deriving instance Show ResourceRegistryThreadException+instance Exception ResourceRegistryThreadException++{-------------------------------------------------------------------------------+  Auxiliary: context+-------------------------------------------------------------------------------}++-- | The internal context of a resource registry, recording a 'PrettyCallStack'+-- of its creation and the creator's 'ThreadId'+data Context m = MonadThread m => Context {+      -- | CallStack in which it was created+      contextCallStack :: !PrettyCallStack++      -- | Thread that created the registry or resource+    , contextThreadId  :: !(ThreadId m)+    }++-- Existential type; we can't use generics+instance NoThunks (Context m) where+  showTypeOf _ = "Context"+  wNoThunks ctxt (Context cs tid) = allNoThunks+    [ noThunks ctxt cs+    , noThunks ctxt (InspectHeapNamed @"ThreadId" tid)+    ]++deriving instance Show (Context m)++captureContext :: MonadThread m => HasCallStack => m (Context m)+captureContext = Context prettyCallStack <$> myThreadId++{-------------------------------------------------------------------------------+  Misc utilities+-------------------------------------------------------------------------------}++-- | Generalization of 'link' that links an async to an arbitrary thread.+--+-- Non standard (not in 'async' library)+--+linkTo ::+     (MonadAsync m, MonadFork m, MonadMask m)+  => ThreadId m+  -> Async m a+  -> m ()+linkTo tid = linkToOnly tid (not . isCancel)++-- | Generalization of 'linkOnly' that links an async to an arbitrary thread.+--+-- Non standard (not in 'async' library).+--+linkToOnly ::+     forall m a.+     (MonadAsync m, MonadFork m, MonadMask m)+  => ThreadId m+  -> (SomeException -> Bool)+  -> Async m a+  -> m ()+linkToOnly tid shouldThrow a = do+    void $ forkRepeat ("linkToOnly " <> show linkedThreadId) $ do+      r <- waitCatch a+      case r of+        Left e | shouldThrow e -> throwTo tid (exceptionInLinkedThread e)+        _otherwise             -> return ()+  where+    linkedThreadId :: ThreadId m+    linkedThreadId = asyncThreadId a++    exceptionInLinkedThread :: SomeException -> ExceptionInLinkedThread+    exceptionInLinkedThread =+        ExceptionInLinkedThread (show linkedThreadId)++isCancel :: SomeException -> Bool+isCancel e+  | Just AsyncCancelled <- fromException e = True+  | otherwise = False++forkRepeat :: (MonadFork m, MonadMask m) => String -> m a -> m (ThreadId m)+forkRepeat label action =+  mask $ \restore ->+    let go = do r <- tryAll (restore action)+                case r of+                  Left _ -> go+                  _      -> return ()+    in forkIO (labelThisThread label >> go)++tryAll :: MonadCatch m => m a -> m (Either SomeException a)+tryAll = try++mustBeRight :: Either Void a -> a+mustBeRight (Left  v) = absurd v+mustBeRight (Right a) = a++{-------------------------------------------------------------------------------+  Auxiliary: CallStack with different Show instance+-------------------------------------------------------------------------------}++-- | CallStack with 'Show' instance using 'prettyCallStack'+newtype PrettyCallStack = PrettyCallStack CallStack+  deriving newtype (NoThunks)++instance Show PrettyCallStack where+  show (PrettyCallStack cs) = GHC.prettyCallStack cs++-- | Capture a 'PrettyCallStack'+prettyCallStack :: HasCallStack => PrettyCallStack+prettyCallStack = PrettyCallStack GHC.callStack++{-------------------------------------------------------------------------------+  Orphan instance+-------------------------------------------------------------------------------}++instance (NoThunks k, NoThunks v)+      => NoThunks (Bimap k v) where+  wNoThunks ctxt = noThunksInKeysAndValues ctxt . Bimap.toList
+ test/Main.hs view
@@ -0,0 +1,629 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DeriveTraversable #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TypeApplications #-}++-- | Tests for the resource registry+--+-- The tests for the registry are model based. The model records which resources+-- we expect to be alive and which we expect to have been deallocated. The only+-- resources we are modelling here are threads; the commands we then execute are+--+-- * Fork a thread from some other thread+-- * Terminate a thread+-- * Have a thread crash+-- * Collect all live threads+--+-- We then verify that the resource registry behaves like the model, cleaning+-- up resources as threads terminate or crash.+module Main (main) where++import Control.Concurrent.Class.MonadMVar.Strict+import Control.Concurrent.Class.MonadSTM.Strict+import Control.Monad+import Control.Monad.Class.MonadAsync+import Control.Monad.Class.MonadFork+import Control.Monad.Class.MonadThrow+import Control.Monad.Class.MonadTimer.SI+import Control.Monad.Except+#if __GLASGOW_HASKELL__ > 904+import Control.Monad.IO.Class+#endif+import Control.ResourceRegistry+import Data.Foldable+import Data.Function+import Data.Functor.Classes+import Data.Kind+import Data.List (delete, sort)+import Data.Map.Strict (Map)+import Data.Map.Strict qualified as Map+import Data.TreeDiff+import Data.Typeable+import Generics.SOP qualified as SOP+import GHC.Generics (Generic, Generic1)+import Prelude+import Test.QuickCheck hiding (forAll)+import Test.QuickCheck.Monadic hiding (run)+import Test.StateMachine+import Test.StateMachine.Types qualified as QSM+import Test.StateMachine.Types.Rank2 qualified as Rank2+import Test.Tasty hiding (after)+import Test.Tasty.QuickCheck hiding (forAll)+import Test.Util.QSM+import Test.Util.SOP+import Test.Util.ToExpr ()++main :: IO ()+main = defaultMain+    $ testGroup "ResourceRegistry" [+      testProperty "sequential" prop_sequential+    ]++{-------------------------------------------------------------------------------+  Mock implementaton+-------------------------------------------------------------------------------}++-- | Mock thread IDs record thread pedigree+--+-- > [t]           top-level thread t+-- > [t, t']       child t' of top-level thread t+-- > [t, t', t'']  child t'' of thread t', itself a child of t+--+-- NOTE: All thread IDs will be unique. If both threads 1 and 2 spawn a child,+-- they would be @[1,3]@ and @[2,4]@.+type MockThread = [Int]++-- | Threads and their subthreads+--+-- Once created, threads are never removed from this map. Instead, when they are+-- killed their 'alive' status is set to 'False'.+newtype MockThreads = MTs { mockThreadsMap :: Map Int MockState }+  deriving (Show, Generic)++-- | State of a mock thread+data MockState = MS {+      alive :: Bool+    , kids  :: MockThreads+    }+  deriving (Show, Generic)++-- | All known threads, and whether or not they are alive+--+-- TODO: Perhaps it would be better to have an invariant that in 'MockThreads'+-- threads must be recorded as dead if any of their parents are, rather than+-- computing that here.+mockThreads :: MockThreads -> [(MockThread, Bool)]+mockThreads = go [] True+  where+    go :: [Int] -> Bool -> MockThreads -> [(MockThread, Bool)]+    go prefix parentAlive =+        concatMap aux . Map.toList . mockThreadsMap+      where+        aux :: (Int, MockState) -> [(MockThread, Bool)]+        aux (tid, MS{..}) =+            (t, parentAlive && alive) : go t alive' kids+          where+            t :: [Int]+            t = prefix ++ [tid]++            alive' :: Bool+            alive' = parentAlive && alive++mockLiveThreads :: MockThreads -> [MockThread]+mockLiveThreads = map fst . filter snd . mockThreads++alterThreadF :: forall m. MonadError Err m+             => MockThread+             -> (Maybe MockState -> m MockState)+             -> MockThreads -> m MockThreads+alterThreadF [] _ _ =+    error "alterThreadF: invalid thread"+alterThreadF [t] f (MTs m) =+    MTs <$> Map.alterF (fmap Just . f) t m+alterThreadF thread@(t:ts) f (MTs m) =+    MTs <$> Map.alterF (fmap Just . f') t m+  where+    f' :: Maybe MockState -> m MockState+    f' Nothing   = throwError $ ErrInvalidThread (show thread)+    f' (Just ms) = (\kids' -> ms { kids = kids' }) <$>+                     alterThreadF ts f (kids ms)++-- Create thread with the given ID+mockFork :: MockThread -> MockThreads -> Except Err MockThreads+mockFork t = alterThreadF t $ \case+    Just _  -> error "fork: thread already exists (bug in runMock)"+    Nothing -> return newState+  where+    newState :: MockState+    newState = MS {+          alive = True+        , kids  = MTs Map.empty+        }++mockKill :: MockThread -> MockThreads -> Except Err MockThreads+mockKill t = alterThreadF t $ \case+    Nothing -> throwError $ ErrInvalidThread (show t)+    Just st -> return st { alive = False }++data Mock = Mock {+      nextId  :: Int+    , threads :: MockThreads+    , links   :: Map MockThread (Link MockThread)+    }+  deriving (Show, Generic)++emptyMock :: Mock+emptyMock = Mock {+      nextId  = 1+    , threads = MTs Map.empty+    , links   = Map.empty+    }++{-------------------------------------------------------------------------------+  Commands+-------------------------------------------------------------------------------}++-- | Should we link a new thread to its parent?+--+-- If yes, we need some information about that parent.+data Link a = LinkFromParent a | DontLink+  deriving (Show, Functor, Generic)++data Cmd t =+    -- | Fork a new top-level thread+    --+    -- We don't allow linking here, because we don't want an exception in one+    -- of these threads to kill the thread running the tests.+    Fork++    -- | Fork a child thread+  | ForkFrom t (Link ())++    -- | Cause a thread to terminate normally+  | Terminate t++    -- | Cause a thread to terminate abnormally+  | Crash t++    -- | Get all live threads+  | LiveThreads+  deriving (Show, Functor, Foldable, Traversable, Generic)++data Success t =+    Unit ()+  | Spawned t+  | Threads [t]+  deriving (Show, Eq, Functor, Foldable, Traversable)++data Err =+    ErrTimeout+  | ErrInvalidThread String+  deriving (Show, Eq)++newtype Resp t = Resp (Either Err (Success t))+  deriving (Show, Eq, Functor, Foldable, Traversable)++{-------------------------------------------------------------------------------+  "Up-to" comparison for responses++  This is used in 'postcondition'.+-------------------------------------------------------------------------------}++normalize :: Resp MockThread -> Resp MockThread+normalize (Resp r) = Resp $ aux <$> r+  where+    aux :: Success MockThread -> Success MockThread+    aux (Unit ())    = Unit ()+    aux (Spawned t)  = Spawned t+    aux (Threads ts) = Threads (sort ts)++{-------------------------------------------------------------------------------+  Run against the mock implementation+-------------------------------------------------------------------------------}++runMock :: Cmd MockThread -> Mock -> (Resp MockThread, Mock)+runMock cmd m@Mock{..} =+    case runExcept (go cmd) of+      Left  err           -> (Resp (Left err), m)+      Right (success, m') -> (Resp (Right success), m')+  where+    go :: Cmd MockThread -> Except Err (Success MockThread, Mock)+    go Fork                = createThread DontLink           [nextId]+    go (ForkFrom t linked) = createThread (const t <$> linked) (t ++ [nextId])+    go (Terminate t)       = (\x -> (Unit (), m { threads = x })) <$> mockKill t threads+    go (Crash t)           = (\x -> (Unit (), m { threads = x })) <$> killAll  t threads+    go LiveThreads         = return (Threads $ mockLiveThreads threads, m)++    createThread :: Link MockThread -- Thread to link to (if any)+                 -> MockThread -> Except Err (Success MockThread, Mock)+    createThread shouldLink t = do+        threads' <- mockFork t threads+        return (+            Spawned t+          , m { nextId  = succ nextId+              , threads = threads'+              , links   = Map.insert t shouldLink links+              }+          )++    killAll :: MockThread -> MockThreads -> Except Err MockThreads+    killAll t =+        mockKill t >=> killParent (Map.findWithDefault DontLink t links)+      where+        killParent :: Link MockThread -> MockThreads -> Except Err MockThreads+        killParent DontLink            = return+        killParent (LinkFromParent t') = killAll t'++{-------------------------------------------------------------------------------+  Run in IO (possibly simulated)+-------------------------------------------------------------------------------}++data TestThread m = TestThread {+      -- | The underlying 'Thread'+      testThread   :: Thread m ()++      -- | Parent thread this thread is linked to (if any)+    , threadLinked :: Link (TestThread m)++      -- | Send the thread instructions (see 'ThreadInstr')+    , threadComms  :: StrictTQueue m (QueuedInstr m)+    }++-- | Instructions to a thread+--+-- The type argument indicates the result of the instruction+data ThreadInstr m :: Type -> Type where+  -- | Have the thread spawn a child thread+  ThreadFork :: Link () -> ThreadInstr m (TestThread m)++  -- | Have the thread terminate normally+  ThreadTerminate :: ThreadInstr m ()++  -- | Raise an exception in the thread+  ThreadCrash :: ThreadInstr m ()++-- | Instruction along with an MVar for the result+data QueuedInstr m = forall a. QueuedInstr (ThreadInstr m a) (StrictMVar m a)++runInThread :: (MonadMVar m, MonadSTM m) => TestThread m -> ThreadInstr m a -> m a+runInThread TestThread{..} instr = do+    result <- newEmptyMVar+    atomically $ writeTQueue threadComms (QueuedInstr instr result)+    takeMVar result++instance (MonadThread m) => Show (TestThread m) where+  show TestThread{..} = "<Thread " ++ show (threadId testThread) ++ ">"++instance (MonadThread m) => Eq (TestThread m) where+  (==) = (==) `on` (threadId . testThread)++-- | Create a new thread in the given registry+--+-- In order to be able to see which threads are alive, we have threads+-- register and unregister themselves. We do not reuse the registry for this,+-- to avoid circular reasoning in the tests.+newThread :: forall m. (MonadMVar m, MonadMask m, MonadAsync m, MonadFork m)+          => StrictTVar m [TestThread m]+          -> ResourceRegistry m+          -> Link (TestThread m)+          -> m (TestThread m)+newThread alive parentReg = \shouldLink -> do+    comms      <- atomically $ newTQueue+    spawned    <- newEmptyMVar++    thread <- forkThread parentReg "newThread" $+                withRegistry $ \childReg ->+                  threadBody childReg spawned comms+    case shouldLink of+      LinkFromParent _ -> linkToRegistry thread+      DontLink         -> return ()++    let testThread :: TestThread m+        testThread = TestThread {+                         testThread   = thread+                       , threadLinked = shouldLink+                       , threadComms  = comms+                       }++    -- Make sure to register thread before starting it+    atomically $ modifyTVar alive (testThread:)+    putMVar spawned testThread+    return testThread+  where+    threadBody :: ResourceRegistry m+               -> StrictMVar m (TestThread m)+               -> StrictTQueue m (QueuedInstr m)+               -> m ()+    threadBody childReg spawned comms = do+        us <- readMVar spawned+        loop us `finally` (atomically $ modifyTVar alive (delete us))+      where+        loop :: TestThread m -> m ()+        loop us = do+          QueuedInstr instr result <- atomically $ readTQueue comms+          case instr of+            ThreadFork linked -> do+              child <- newThread alive childReg (const us <$> linked)+              putMVar result child+              loop us+            ThreadTerminate -> do+              putMVar result ()+            ThreadCrash -> do+              putMVar result ()+              error "crashing"++runIO :: forall m. (MonadMVar m, MonadTimer m, MonadMask m, MonadAsync m, MonadFork m)+      => StrictTVar m [TestThread m]+      -> ResourceRegistry m+      -> Cmd (TestThread m) -> m (Resp (TestThread m))+runIO alive reg cmd = catchEx $ timeout 1 $+    case cmd of+      Fork ->+        Spawned <$> newThread alive reg DontLink+      ForkFrom thread shouldLink -> do+        Spawned <$> runInThread thread (ThreadFork shouldLink)+      Terminate thread -> do+        runInThread thread ThreadTerminate+        Unit <$> waitForTermination thread+      Crash thread -> do+        runInThread thread ThreadCrash+        Unit <$> waitForTermination thread+      LiveThreads ->+        atomically $ Threads <$> readTVar alive+  where+    catchEx :: m (Maybe (Success a)) -> m (Resp a)+    catchEx = fmap (Resp . maybe (Left ErrTimeout) Right)++    -- For the thread and all of its linked parents to have terminated+    waitForTermination :: TestThread m -> m ()+    waitForTermination t = do+        result <- try $ waitThread (testThread t)+        case (result, threadLinked t) of+          (Left (_ :: SomeException), LinkFromParent t') ->+            waitForTermination t'+          _otherwise ->+            return ()++{-------------------------------------------------------------------------------+  QSM wrappers+-------------------------------------------------------------------------------}++newtype At m f r = At (f (Reference (TestThread m) r))++deriving instance (MonadThread m, Show1 r) => Show (At m Cmd  r)+deriving instance (MonadThread m, Show1 r) => Show (At m Resp r)++{-------------------------------------------------------------------------------+  Relate model to IO+-------------------------------------------------------------------------------}++-- TODO: Use RefEnv?+type Refs m r = [(Reference (TestThread m) r, MockThread)]++(!) :: (Eq k, Show k) => [(k, a)] -> k -> a+env ! r = case lookup r env of+            Just a  -> a+            Nothing -> error $ "Unknown reference: " ++ show r++data Model m r = Model Mock (Refs m r)+  deriving (Show, Generic)++initModel :: Model m r+initModel = Model emptyMock []++{-------------------------------------------------------------------------------+  Events+-------------------------------------------------------------------------------}++toMock :: forall m f r. (Functor f, Eq1 r, Show1 r, MonadThread m)+       => Model m r -> At m f r -> f MockThread+toMock (Model _ hs) (At fr) = (hs !) <$> fr++step :: (Eq1 r, Show1 r, MonadThread m)+     => Model m r -> At m Cmd r -> (Resp MockThread, Mock)+step m@(Model mock _) c = runMock (toMock m c) mock++data Event m r = Event {+      before   :: Model  m     r+    , cmd      :: At     m Cmd r+    , after    :: Model  m     r+    , mockResp :: Resp MockThread+    }++lockstep :: (Eq1 r, Show1 r, MonadThread m)+         => Model m      r+         -> At    m Cmd  r+         -> At    m Resp r+         -> Event m      r+lockstep m@(Model _ hs) c (At resp) = Event {+      before   = m+    , cmd      = c+    , after    = Model mock' (hs <> hs')+    , mockResp = resp'+    }+  where+    (resp', mock') = step m c+    hs' = zip (newHandles resp) (newHandles resp')++    newHandles :: Resp r -> [r]+    newHandles (Resp (Left _))            = []+    newHandles (Resp (Right (Unit ())))   = []+    newHandles (Resp (Right (Spawned t))) = [t]+    newHandles (Resp (Right (Threads _))) = []++{-------------------------------------------------------------------------------+  Generator+-------------------------------------------------------------------------------}++generator :: forall m. Model m Symbolic -> Maybe (Gen (At m Cmd Symbolic))+generator (Model _ hs) = Just $ oneof $ concat [+      withoutHandle+    , if null hs then [] else withHandle (elements (map fst hs))+    ]+  where+    withoutHandle :: [Gen (At m Cmd Symbolic)]+    withoutHandle = [+          fmap At $ return Fork+        , fmap At $ return LiveThreads+        ]++    withHandle :: Gen (Reference (TestThread m) Symbolic)+               -> [Gen (At m Cmd Symbolic)]+    withHandle pickThread = [+          fmap At $ Terminate <$> pickThread+        , fmap At $ Crash     <$> pickThread+        , fmap At $ ForkFrom  <$> pickThread <*> genLink+        ]++    genLink :: Gen (Link ())+    genLink = aux <$> arbitrary+      where+        aux :: Bool -> Link ()+        aux True  = LinkFromParent ()+        aux False = DontLink++shrinker :: Model m Symbolic -> At m Cmd Symbolic -> [At m Cmd Symbolic]+shrinker _ _ = []++{-------------------------------------------------------------------------------+  QSM required instances+-------------------------------------------------------------------------------}++instance SOP.Generic         (Cmd t)+instance SOP.HasDatatypeInfo (Cmd t)++deriving instance Generic1 (At m Cmd)+deriving instance Generic1 (At m Resp)++instance CommandNames (At m Cmd) where+  cmdName  (At cmd) = constrName cmd+  cmdNames _        = constrNames (Proxy @(Cmd ()))++instance Rank2.Foldable    (At m Cmd)+instance Rank2.Functor     (At m Cmd)+instance Rank2.Traversable (At m Cmd)++instance Rank2.Foldable (At m Resp)++instance ToExpr MockState+instance ToExpr MockThreads+instance ToExpr Mock+instance ToExpr (Link MockThread)+instance ToExpr (Model IO Concrete)++instance (MonadThread m) => ToExpr (TestThread m) where+  toExpr = defaultExprViaShow++{-------------------------------------------------------------------------------+  QSM toplevel+-------------------------------------------------------------------------------}++semantics :: (MonadMVar m, MonadMask m, MonadAsync m, MonadFork m, MonadTimer m, Typeable m)+          => StrictTVar m [TestThread m]+          -> ResourceRegistry m+          -> At m Cmd Concrete -> m (At m Resp Concrete)+semantics alive reg (At c) =+    (At . fmap reference) <$>+      runIO alive reg (concrete <$> c)++transition :: (Eq1 r, Show1 r, MonadThread m)+           => Model m r -> At m Cmd r -> At m Resp r -> Model m r+transition m c = after . lockstep m c++precondition :: forall m. (MonadThread m)+             => Model m Symbolic -> At m Cmd Symbolic -> Logic+precondition (Model mock hs) (At c) =+    forAll (toList c) checkRef+  where+    checkRef :: Reference (TestThread m) Symbolic -> Logic+    checkRef r =+        case lookup r hs of+          Nothing -> Bot+          Just r' -> r' `member` mockLiveThreads (threads mock)++postcondition :: (MonadThread m)+              => Model m      Concrete+              -> At    m Cmd  Concrete+              -> At    m Resp Concrete+              -> Logic+postcondition m c r =+    normalize (toMock (after e) r) .== normalize (mockResp e)+  where+    e = lockstep m c r++symbolicResp :: (MonadThread m, Typeable m)+             => Model m     Symbolic+             -> At    m Cmd Symbolic+             -> GenSym (At m Resp Symbolic)+symbolicResp m c = At <$> traverse (const genSym) resp+  where+    (resp, _mock') = step m c++sm :: (MonadMVar m, MonadMask m, MonadAsync m, MonadFork m, MonadTimer m, Typeable m)+   => StrictTVar m [TestThread m]+   -> ResourceRegistry m+   -> StateMachine (Model m) (At m Cmd) m (At m Resp)+sm alive reg = StateMachine {+      initModel     = initModel+    , transition    = transition+    , precondition  = precondition+    , postcondition = postcondition+    , invariant     = Nothing+    , generator     = generator+    , shrinker      = shrinker+    , semantics     = semantics alive reg+    , mock          = symbolicResp+    , cleanup       = noCleanup+    }++prop_sequential :: Property+prop_sequential = forAllCommands (sm unused unused) Nothing prop_sequential'++prop_sequential' :: QSM.Commands (At IO Cmd) (At IO Resp) -> Property+prop_sequential' cmds = monadicIO $ do+    alive <- liftIO $ newTVarIO []+    reg   <- liftIO $ unsafeNewRegistry+    let sm' = sm alive reg+    (hist, _model, res) <- runCommands sm' cmds+    prettyCommands sm' hist+      $ checkCommandNames cmds+      $ res === Ok++unused :: a+unused = error "not used during command generation"++{-------------------------------------------------------------------------------+  For running things from ghci+-------------------------------------------------------------------------------}++_forkCount :: QSM.Commands (At IO Cmd) (At IO Resp)+_forkCount = example (sm unused unused) $ do+    run' $ At $ Fork+    run' $ At $ LiveThreads++_forkKillCount :: QSM.Commands (At IO Cmd) (At IO Resp)+_forkKillCount = example (sm unused unused) $ do+    [t] <- run $ At $ Fork+    run' $ At $ Terminate t+    run' $ At $ LiveThreads++_forkFromKillCount :: QSM.Commands (At IO Cmd) (At IO Resp)+_forkFromKillCount = example (sm unused unused) $ do+    [t] <- run $ At $ Fork+    run' $ At $ ForkFrom t DontLink+    run' $ At $ Terminate t+    run' $ At $ LiveThreads++_invalidForkFrom :: QSM.Commands (At IO Cmd) (At IO Resp)+_invalidForkFrom = example (sm unused unused) $ do+    [t] <- run $ At $ Fork+    run' $ At $ Terminate t+    run' $ At $ ForkFrom t DontLink
+ test/Test/Util/QSM.hs view
@@ -0,0 +1,74 @@+{-# LANGUAGE ScopedTypeVariables #-}++module Test.Util.QSM (+    Example+    -- opaque+  , example+  , run+  , run'+  ) where++import Control.Monad+import Control.Monad.Fail qualified as Fail+import Data.Typeable+import Test.StateMachine.Logic qualified as Logic+import Test.StateMachine.Sequential+import Test.StateMachine.Types+import Test.StateMachine.Types.Rank2 qualified as Rank2++data Example cmd a =+    Done a+  | Run (cmd Symbolic) ([Var] -> Example cmd a)+  | Fail String++instance Functor (Example cmd) where+  fmap = liftM++instance Applicative (Example cmd) where+  pure  = Done+  (<*>) = ap++instance Monad (Example cmd) where+  return         = pure+  Done a   >>= f = f a+  Run c k  >>= f = Run c (k >=> f)+  Fail err >>= _ = Fail err++instance Fail.MonadFail (Example cmd) where+  fail = Fail++-- | Run a command, and capture its references+run :: Typeable a => cmd Symbolic -> Example cmd [Reference a Symbolic]+run cmd = Run cmd (Done . map (Reference . Symbolic))++-- | Run a command, ignoring its references+run' :: cmd Symbolic -> Example cmd ()+run' cmd = Run cmd (\_vars -> Done ())++example :: forall model cmd m resp. (Rank2.Foldable resp, Show (cmd Symbolic))+        => StateMachine model cmd m resp+        -> Example cmd ()+        -> Commands cmd resp+example sm =+    Commands . fst . flip runGenSym newCounter . go (initModel sm)+  where+    go :: model Symbolic -> Example cmd () -> GenSym [Command cmd resp]+    go _ (Done ())   = return []+    go _ (Fail err)  = error $ "example: " ++ err+    go m (Run cmd k) = do+        case Logic.logic (precondition sm m cmd) of+          Logic.VFalse counterexample ->+            error $ "Invalid command " ++ show cmd ++ ": " ++ show counterexample+          Logic.VTrue -> do+            resp <- mock sm m cmd++            let m' :: model Symbolic+                m' = transition sm m cmd resp++                vars :: [Var]+                vars = getUsedVars resp++                cmd' :: Command cmd resp+                cmd' = Command cmd resp vars++            (cmd' :) <$> go m' (k vars)
+ test/Test/Util/SOP.hs view
@@ -0,0 +1,31 @@+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}++module Test.Util.SOP (+    constrName+  , constrNames+  ) where++import Data.Proxy+import Generics.SOP qualified as SOP++constrInfo :: SOP.HasDatatypeInfo a+           => proxy a+           -> SOP.NP SOP.ConstructorInfo (SOP.Code a)+constrInfo = SOP.constructorInfo . SOP.datatypeInfo++constrName :: forall a. SOP.HasDatatypeInfo a => a -> String+constrName a =+    SOP.hcollapse $ SOP.hliftA2 go (constrInfo p) (SOP.unSOP (SOP.from a))+  where+    go :: SOP.ConstructorInfo b -> SOP.NP SOP.I b -> SOP.K String b+    go nfo _ = SOP.K $ SOP.constructorName nfo++    p = Proxy @a++constrNames :: SOP.HasDatatypeInfo a => proxy a -> [String]+constrNames p =+    SOP.hcollapse $ SOP.hmap go (constrInfo p)+  where+    go :: SOP.ConstructorInfo a -> SOP.K String a+    go nfo = SOP.K $ SOP.constructorName nfo
+ test/Test/Util/ToExpr.hs view
@@ -0,0 +1,36 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}++{-# OPTIONS_GHC -Wno-orphans #-}++-- | This module implements QSM's @CanDiff@ typeclass using @tree-diff@'s+-- @ToExpr@.+module Test.Util.ToExpr () where++import Data.TreeDiff as T+import Test.StateMachine qualified as QSM+import Test.StateMachine.Diffing (CanDiff (..))+import Test.StateMachine.Types.References qualified as QSM++instance ToExpr x => CanDiff x where+  type ADiff  x = Edit EditExpr+  type AnExpr x = Expr++  toDiff             = toExpr+  exprDiff         _ = T.exprDiff+  diffToDocCompact _ = ansiWlBgEditExprCompact+  diffToDoc        _ = ansiWlBgEditExpr+  exprToDoc        _ = ansiWlBgExpr++{-------------------------------------------------------------------------------+  QSM's References instances+-------------------------------------------------------------------------------}++instance ToExpr (r k) => ToExpr (QSM.Reference k r)++instance ToExpr a => ToExpr (QSM.Concrete a) where+  toExpr (QSM.Concrete x) = toExpr x++instance ToExpr (QSM.Opaque a) where+  toExpr _ = App "Opaque" []