packages feed

atelier-core 0.7.1.0 → 0.7.2.0

raw patch · 4 files changed

+68/−7 lines, 4 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

+ Atelier.Signal: installTerminationHandler :: forall (es :: [Effect]). (Concurrent :> es, IOE :> es) => Eff es ()
+ Atelier.Signal: instance GHC.Exception.Type.Exception Atelier.Signal.Terminated
+ Atelier.Signal: instance GHC.Show.Show Atelier.Signal.Terminated

Files

CHANGELOG.md view
@@ -7,6 +7,12 @@  ## [Unreleased] +## [0.7.2.0] - 2026-09-21++### Added++- `Atelier.Signal`: For properly handling termination of an application.+ ## [0.7.1.0] - 2026-09-17  ### Added
atelier-core.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack  name:           atelier-core-version:        0.7.1.0+version:        0.7.2.0 synopsis:       Foundational Effectful-based effects and utilities description:    Core effects and utilities for effect-based applications, built on Effectful — part of the atelier toolkit. category:       Control@@ -66,6 +66,7 @@       Atelier.Effects.UUID       Atelier.Effects.Yield       Atelier.Exception+      Atelier.Signal       Atelier.Time       Atelier.Types.Base64       Atelier.Types.HttpApiDataReadShow
src/Atelier/Effects/FileWatcher.hs view
@@ -69,11 +69,9 @@ where  import Control.Concurrent (threadDelay)-import Control.Concurrent.STM (retry) import Data.List (nub) import Effectful (Effect, IOE) import Effectful.Concurrent (Concurrent)-import Effectful.Concurrent.STM (atomically) import Effectful.Dispatch.Dynamic (interpretWith, localSeqUnlift, localUnliftIO, reinterpret) import Effectful.State.Static.Shared (evalState, get, put) import Effectful.TH (makeEffect)@@ -82,6 +80,7 @@ import System.FilePath (takeExtension)  import Data.Text qualified as T+import Effectful.Concurrent qualified as Concurrent import System.FSNotify qualified as FSN  import Atelier.Effects.Conc (concStrat)@@ -198,12 +197,32 @@                 for_ dedupedDirs \d ->                     watchTree mgr d (matchesAny absWatches . eventPath) \fsEvent ->                         void $ unliftIO $ callback (eventPath fsEvent) (toFileEvent fsEvent)-                forever $ threadDelay 1_000_000+                -- Park indefinitely to hold the manager open. fsnotify delivers+                -- events on its own threads, so there is nothing to do here, and+                -- waking up is not free: each wake-up ends the RTS idle period+                -- and re-arms idle GC, costing a full major collection. 'forever'+                -- is belt-and-braces in case the delay ever returns early.+                --+                -- Preferred over @atomically retry@: a transaction that retries+                -- without having read a 'TVar' has no wakeup path, so it stays+                -- parked only for as long as GC still sees this thread as+                -- reachable. Should it ever become unreachable the RTS throws+                -- @BlockedIndefinitelyOnSTM@, which would unwind 'withManager'+                -- and silently stop all watching. A delay carries no such+                -- dependency on how callers happen to retain the thread.+                forever $ threadDelay maxBound   -- | Scripted interpreter for testing.--- Delivers all scripted events to the callback in order, then blocks--- indefinitely — matching the blocking semantics of 'runFileWatcherIO'.+--+-- Delivers all scripted events to the callback in order, then parks forever,+-- matching the blocking semantics of 'runFileWatcherIO' — 'WatchFilePaths'+-- returns 'Void', so callers must never be resumed. Parks with a delay rather+-- than @atomically retry@ for the same reason as 'runFileWatcherIO': a retry+-- that read no 'TVar' stays parked only while GC sees the thread as reachable,+-- so whether it survives depends on whether the test happens to hold onto the+-- 'ThreadId'.+-- -- The 'Watch' specification is ignored; the caller controls what events are fed in. runFileWatcherScripted     :: (Concurrent :> es) => [(FilePath, FileEvent)] -> Eff (FileWatcher : es) a -> Eff es a@@ -213,7 +232,7 @@             events' <- get             put []             for_ events' \(path, fileEvent) -> unlift $ callback path fileEvent-            atomically retry+            forever $ Concurrent.threadDelay maxBound   -- Helpers
+ src/Atelier/Signal.hs view
@@ -0,0 +1,35 @@+-- | Termination handler based on Cabal's `Distribution.Client.Signal`.+-- See https://github.com/haskell/cabal/blob/aeb7dbfabad38289cb36da6fdf78acee99b69e3b/cabal-install/src/Distribution/Client/Signal.hs+module Atelier.Signal (installTerminationHandler) where++import Effectful (IOE, withSeqEffToIO)+import Effectful.Concurrent (Concurrent, myThreadId, throwTo)+import Effectful.Exception (asyncExceptionFromException, asyncExceptionToException)+import System.Posix.Signals (Handler (..), installHandler, sigTERM)+import Text.Show (Show (..))+++-- | Terminated is an asynchronous exception, thrown when+-- SIGTERM is received. It's to 'kill' what 'UserInterrupt'+-- is to Ctrl-C.+data Terminated = Terminated+++instance Exception Terminated where+    toException = asyncExceptionToException+    fromException = asyncExceptionFromException+++instance Show Terminated where+    show Terminated = "terminated"+++installTerminationHandler :: (Concurrent :> es, IOE :> es) => Eff es ()+installTerminationHandler = do+    mainThreadId <- myThreadId+    void+        $ withSeqEffToIO \unlift ->+            installHandler+                sigTERM+                (CatchOnce $ unlift $ throwTo mainThreadId Terminated)+                Nothing