packages feed

ki 1.0.0.1 → 1.0.0.2

raw patch · 5 files changed

+67/−25 lines, 5 filesdep ~basePVP ok

version bump matches the API change (PVP)

Dependency ranges changed: base

API changes (from Hackage documentation)

Files

CHANGELOG.md view
@@ -1,3 +1,8 @@+## [1.0.0.2] - 2023-01-25++- Bugfix [#20](https://github.com/awkward-squad/ki/pull/20): previously, a child thread could deadlock when attempting+  to propagate an exception to its parent+ ## [1.0.0.1] - 2022-08-14  - Compat: support GHC 9.4.1
README.md view
@@ -1,3 +1,11 @@+| `ki` | `ki-unlifted` |+| --- | --- |+| [![GitHub CI](https://github.com/awkward-squad/ki/workflows/CI/badge.svg)](https://github.com/awkward-squad/ki/actions) | |+| [![Hackage](https://img.shields.io/hackage/v/ki.svg?label=ki&logo=haskell)](https://hackage.haskell.org/package/ki) | [![Hackage](https://img.shields.io/hackage/v/ki-unlifted.svg?label=ki-unlifted&logo=haskell)](https://hackage.haskell.org/package/ki-unlifted) |+| [![Stackage LTS](https://stackage.org/package/ki/badge/lts)](https://www.stackage.org/lts/package/ki) | [![Stackage LTS](https://stackage.org/package/ki-unlifted/badge/lts)](https://www.stackage.org/lts/package/ki-unlifted) |+| [![Stackage Nightly](https://stackage.org/package/ki/badge/nightly)](https://www.stackage.org/nightly/package/ki) | [![Stackage Nightly](https://stackage.org/package/ki-unlifted/badge/nightly)](https://www.stackage.org/nightly/package/ki-unlifted) |+| [![Dependencies](https://img.shields.io/hackage-deps/v/ki)](https://packdeps.haskellers.com/reverse/ki) | [![Dependencies](https://img.shields.io/hackage-deps/v/ki-unlifted)](https://packdeps.haskellers.com/reverse/ki-unlifted) |+ # Overview  `ki` is a lightweight structured-concurrency library inspired by many other projects and blog posts:@@ -24,13 +32,3 @@ # Documentation  [Hackage documentation](https://hackage.haskell.org/package/ki/docs/Ki.html)--# Badges / meta--| `ki` | `ki-unlifted` |-| --- | --- |-| [![GitHub CI](https://github.com/awkward-squad/ki/workflows/CI/badge.svg)](https://github.com/awkward-squad/ki/actions) | |-| [![Hackage](https://img.shields.io/hackage/v/ki.svg?label=ki&logo=haskell)](https://hackage.haskell.org/package/ki) | [![Hackage](https://img.shields.io/hackage/v/ki-unlifted.svg?label=ki-unlifted&logo=haskell)](https://hackage.haskell.org/package/ki-unlifted) |-| [![Stackage LTS](https://stackage.org/package/ki/badge/lts)](https://www.stackage.org/lts/package/ki) | [![Stackage LTS](https://stackage.org/package/ki-unlifted/badge/lts)](https://www.stackage.org/lts/package/ki-unlifted) |-| [![Stackage Nightly](https://stackage.org/package/ki/badge/nightly)](https://www.stackage.org/nightly/package/ki) | [![Stackage Nightly](https://stackage.org/package/ki-unlifted/badge/nightly)](https://www.stackage.org/nightly/package/ki-unlifted) |-| [![Dependencies](https://img.shields.io/hackage-deps/v/ki)](https://packdeps.haskellers.com/reverse/ki) | [![Dependencies](https://img.shields.io/hackage-deps/v/ki-unlifted)](https://packdeps.haskellers.com/reverse/ki-unlifted) |
ki.cabal view
@@ -11,8 +11,8 @@ name: ki stability: experimental synopsis: A lightweight structured concurrency library--- tested-with: GHC == 9.0.2, GHC == 9.2.4, GHC == 9.4.1-version: 1.0.0.1+tested-with: GHC == 9.0.2, GHC == 9.2.5, GHC == 9.4.4+version: 1.0.0.2  description:   A lightweight structured concurrency library.
src/Ki/Internal/Scope.hs view
@@ -41,6 +41,7 @@ import Ki.Internal.Counter import Ki.Internal.Prelude import Ki.Internal.Thread+import GHC.Conc.Sync (readTVarIO)  -- | A scope. --@@ -369,22 +370,54 @@         Nothing -> False         Just _ -> True --- TODO document this+-- We have a non-`ScopeClosing` exception to propagate to our parent.+--+-- If our scope has already begun closing (`startingVar` is -1), then either...+--+--   (A) We already received a `ScopeClosing`, but then ended up trying to propagate an exception anyway, because we+--   threw a synchronous exception (or were hit by a different asynchronous exception) during our teardown procedure.+--+--   or+--+--   (B) We will receive a `ScopeClosing` imminently, because our parent has *just* finished setting `startingVar` to+--   -1, and will proceed to throw ScopeClosing to all of its children.+--+-- If (A), our parent has asynchronous exceptions masked, so we must inform it of our exception via `childExceptionVar`+-- rather than throwTo. If (B), either mechanism would work. And because we don't if we're in case (A) or (B), we just+-- `childExceptionVar`.+--+-- And if our scope has not already begun closing (`startingVar` is not -1), then we ought to throw our exception to it.+-- But that might fail due to either...+--+--   (C) Our parent concurrently closing the scope and sending us a `ScopeClosing`; because it has asynchronous+--   exceptions uninterruptibly masked and we only have asynchronous exception *synchronously* masked, its `throwTo`+--   will return `()`, and ours will throw that `ScopeClosing` asynchronous exception. In this case, since we now know+--   our parent is tearing down and has asynchronous exceptions masked, we again inform it via `childExceptionVar`.+--+--   (D) Some *other* non-`ScopeClosing` asynchronous exception is raised here. This is truly odd: maybe it's a heap+--   overflow exception from the GHC runtime? Maybe some other thread has smuggled our `ThreadId` out and has manually+--   thrown us an exception for some reason? Either way, because we already have an exception that we are trying to+--   propagate, we just scoot these freaky exceptions under the rug.+-- -- Precondition: interruptibly masked propagateException :: Scope -> Int -> SomeException -> UnexceptionalIO ()-propagateException Scope {childExceptionVar, parentThreadId} childId exception =-  loop+propagateException Scope {childExceptionVar, parentThreadId, startingVar} childId exception =+  UnexceptionalIO (readTVarIO startingVar) >>= \case+    -1 -> tryPutChildExceptionVar -- (A) / (B)+    _ -> loop   where     loop :: UnexceptionalIO ()     loop =       unexceptionalTry (throwTo parentThreadId ThreadFailed {childId, exception}) >>= \case-        Left IsScopeClosingException -> unexceptionalTryPutMVar_ childExceptionVar exception-        -- while blocking on notifying the parent of this exception, we got hit by a random async exception from-        -- elsewhere. that's weird and unexpected, but we already have an exception to deliver, so it just gets tossed-        -- to the void...-        Left _ -> loop+        Left IsScopeClosingException -> tryPutChildExceptionVar -- (C)+        Left _ -> loop -- (D)         Right _ -> pure () +    tryPutChildExceptionVar :: UnexceptionalIO ()+    tryPutChildExceptionVar =+      UnexceptionalIO (void (tryPutMVar childExceptionVar exception))++ -- A little promise that this IO action cannot throw an exception. -- -- Yeah it's verbose, and maybe not that necessary, but the code that bothers to use it really does require@@ -410,7 +443,3 @@       catch         (coerce @_ @(a -> IO b) onSuccess <$> action)         (pure . coerce @_ @(SomeException -> IO b) onFailure)--unexceptionalTryPutMVar_ :: MVar a -> a -> UnexceptionalIO ()-unexceptionalTryPutMVar_ var x =-  coerce (void (tryPutMVar var x))
test/Tests.hs view
@@ -1,8 +1,10 @@ module Main (main) where +import Control.Concurrent (newEmptyMVar, putMVar, takeMVar, threadDelay) import Control.Concurrent.STM (atomically) import Control.Exception import Control.Monad+import GHC.IO (unsafeUnmask) import qualified Ki import Test.Tasty import Test.Tasty.HUnit@@ -97,7 +99,15 @@               mask \restore -> do                 thread :: Ki.Thread (Either A ()) <- Ki.forkTry scope (throwIO A2)                 restore (atomically (Ki.awaitAll scope)) `catch` \(_ :: SomeException) -> pure ()-                atomically (Ki.await thread)+                atomically (Ki.await thread),+        testCase "child propagates exceptions thrown during cleanup" do+          (`shouldThrow` A) do+            Ki.scoped \scope -> do+              ready <- newEmptyMVar+              Ki.forkWith_ scope Ki.defaultThreadOptions {Ki.maskingState = MaskedInterruptible} do+                putMVar ready ()+                unsafeUnmask (forever (threadDelay maxBound)) `finally` throwIO A+              takeMVar ready       ]  data A = A