packages feed

acid-state 0.16.1.4 → 0.17.0

raw patch · 4 files changed

+192/−6 lines, 4 filesdep ~directorydep ~filepathPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: directory, filepath

API changes (from Hackage documentation)

+ Data.Acid.Local: [localLastCheckpoint] :: LocalState st -> IORef (Maybe EntryId)
- Data.Acid.Local: LocalState :: Core st -> IORef st -> FileLog (Tagged ByteString) -> FileLog (Checkpoint st) -> FileLock -> LocalState st
+ Data.Acid.Local: LocalState :: Core st -> IORef st -> FileLog (Tagged ByteString) -> FileLog (Checkpoint st) -> FileLock -> IORef (Maybe EntryId) -> LocalState st

Files

CHANGELOG.md view
@@ -1,3 +1,36 @@+0.17.0+======++_Jeremy Shaw, 2026-10-29_++- `createCheckpointAndClose` no longer writes a checkpoint when this+  process has already made one durable and no events have been logged+  since, making shutdown fast right after `createCheckpoint` (e.g. via the+  remote `CreateCheckpoint` command)+  ([#181](https://github.com/acid-state/acid-state/pull/181)).+  A checkpoint is still written when the state is freshly created, when+  events follow the last checkpoint, and when closing a state whose only+  checkpoint is the one loaded at open (so SafeCopy migrations are still+  saved).++**Breaking changes and changed behaviour**++- `LocalState` has a new field, `localLastCheckpoint`. Code that builds a+  `LocalState` with positional arguments will no longer compile. Code that+  builds one with record syntax will still compile (with only a+  `-Wmissing-fields` warning), but leaves the field undefined: the next+  `createCheckpoint` will then kill the log writer thread, and later+  updates, checkpoints and closes will hang. Initialise the field with+  `newIORef Nothing`.+- `createCheckpointAndClose` no longer always appends a checkpoint. After+  `createCheckpoint` followed by `createArchive`, the newest+  `checkpoints-*.log` file is left empty and the latest checkpoint is in+  the file before it. acid-state itself restores correctly, but backup,+  replication or monitoring scripts that copy or check only the newest+  checkpoint file must be updated to look at earlier files too.+- Tools that use the checkpoint log's size, modification time or entry+  count to confirm a clean shutdown may no longer see a change at close.+ 0.16.1.4 ========= 
acid-state.cabal view
@@ -1,6 +1,6 @@ Cabal-version:       1.18 Name:                acid-state-Version:             0.16.1.4+Version:             0.17.0 Synopsis:            Add ACID guarantees to any serializable Haskell data structure. Description:         Use regular Haskell data structures as your database and get stronger ACID guarantees than most RDBMS offer. Homepage:            https://github.com/acid-state/acid-state@@ -13,8 +13,8 @@  tested-with:   GHC == 9.14.1-  GHC == 9.12.2-  GHC == 9.10.2+  GHC == 9.12.4+  GHC == 9.10.3   GHC == 9.8.4   GHC == 9.6.7   GHC == 9.4.8@@ -117,13 +117,16 @@   build-depends:       base                      , acid-state                      , deepseq+                     , directory+                     , filepath                      , hspec                      , mtl                      , safecopy                      , template-haskell   build-tool-depends: hspec-discover:hspec-discover   ghc-options:         -threaded -rtsopts -with-rtsopts=-N-  other-modules:       Data.Acid.TemplateHaskellSpec+  other-modules:       Data.Acid.CheckpointSpec+                       Data.Acid.TemplateHaskellSpec   default-language:    Haskell2010  test-suite state-machine
src/Data/Acid/Local.hs view
@@ -73,6 +73,11 @@                  , localEvents      :: FileLog (Tagged ByteString)                  , localCheckpoints :: FileLog (Checkpoint st)                  , localLock        :: FileLock+                 , localLastCheckpoint :: IORef (Maybe EntryId)+                   -- ^ Event cutoff of the most recent checkpoint made durable+                   --   by this process, if any.  Used by+                   --   'createCheckpointAndClose' to avoid writing a checkpoint+                   --   identical to one already on disk.                  }  newtype StateIsLocked = StateIsLocked FilePath deriving (Show)@@ -193,19 +198,35 @@          withCoreState (localCore acidState) $ \st ->            do eventId <- askCurrentEntryId (localEvents acidState)               pushAction (localEvents acidState) $-                pushEntry (localCheckpoints acidState) (Checkpoint eventId st) (putMVar mvar ())+                pushEntry (localCheckpoints acidState) (Checkpoint eventId st) $+                  do recordCheckpoint acidState eventId+                     putMVar mvar ()          takeMVar mvar +-- | Note that a checkpoint with the given event cutoff is durable.+--   Checkpoints may complete out of order, so keep the largest cutoff.+recordCheckpoint :: LocalState st -> EntryId -> IO ()+recordCheckpoint acidState eventId+    = atomicModifyIORef' (localLastCheckpoint acidState) $ \old ->+        (Just (maybe eventId (max eventId) old), ())+ -- | Save a snapshot to disk and close the AcidState as a single atomic --   action. This is useful when you want to make sure that no events --   are saved to disk after a checkpoint.+--+--   If this process has already made a checkpoint durable and no events+--   have been logged since, that checkpoint already reflects the current+--   state, so no new checkpoint is written. createCheckpointAndClose :: (IsAcidic st, Typeable st) => AcidState st -> IO () createCheckpointAndClose abstract_state     = do mvar <- newEmptyMVar          closeCore' (localCore acidState) $ \st ->            do eventId <- askCurrentEntryId (localEvents acidState)+              lastCheckpoint <- readIORef (localLastCheckpoint acidState)               pushAction (localEvents acidState) $-                pushEntry (localCheckpoints acidState) (Checkpoint eventId st) (putMVar mvar ())+                if lastCheckpoint == Just eventId+                  then putMVar mvar ()+                  else pushEntry (localCheckpoints acidState) (Checkpoint eventId st) (putMVar mvar ())          takeMVar mvar          closeFileLog (localEvents acidState)          closeFileLog (localCheckpoints acidState)@@ -408,12 +429,17 @@       checkpointsLog <- openFileLog checkpointsLogKey       stateCopy <- newIORef undefined       withCoreState core (writeIORef stateCopy)+      -- The checkpoint loaded above is deliberately not recorded here: it+      -- may have been written with an older SafeCopy version, and closing+      -- with createCheckpointAndClose is how such data gets migrated.+      lastCheckpoint <- newIORef Nothing        return $ toAcidState LocalState { localCore = core                                       , localCopy = stateCopy                                       , localEvents = eventsLog                                       , localCheckpoints = checkpointsLog                                       , localLock = lock+                                      , localLastCheckpoint = lastCheckpoint                                       }     maybeLockFile path = do       createDirectoryIfMissing True (takeDirectory path)
+ test/Data/Acid/CheckpointSpec.hs view
@@ -0,0 +1,124 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}++-- | Tests that 'createCheckpointAndClose' does not write a checkpoint+-- identical to one this process has already made durable.+module Data.Acid.CheckpointSpec where++import Test.Hspec++import Control.Exception (bracket_)+import Control.Monad.Reader (ask)+import Control.Monad.State (modify)+import Data.SafeCopy (base, deriveSafeCopy)+import System.Directory (getTemporaryDirectory, removePathForcibly)+import System.FilePath ((</>))++import Data.Acid+import Data.Acid.Local (Checkpoint, createCheckpointAndClose,+                        defaultSerialisationLayer, mkCheckpointsLogKey)+import Data.Acid.Log (FileLog, askCurrentEntryId, closeFileLog, openFileLog)++newtype Counter = Counter Int++$(deriveSafeCopy 0 'base ''Counter)++increment :: Update Counter ()+increment = modify (\(Counter n) -> Counter (n + 1))++current :: Query Counter Int+current = do Counter n <- ask+             return n++$(makeAcidic ''Counter ['increment, 'current])++spec :: Spec+spec = around withStateDir $+    describe "createCheckpointAndClose" $ do+        it "writes a checkpoint when events were logged" $ \dir -> do+            n <- closeAfter dir $ \acid -> update acid Increment+            n `shouldBe` 1+            valueIn dir `shouldReturn` 1++        it "writes a checkpoint for a fresh state with no events" $ \dir -> do+            n <- closeAfter dir $ \_ -> return ()+            n `shouldBe` 1+            -- The initial value must have been saved, so a different+            -- initial value at reopen is ignored.+            reopened <- openLocalStateFrom dir (Counter 42)+            query reopened Current `shouldReturn` 0+            closeAcidState reopened++        it "rewrites the checkpoint it loaded, even with no new events" $ \dir -> do+            _ <- closeAfter dir $ \acid -> update acid Increment+            n <- closeAfter dir $ \_ -> return ()+            -- The loaded checkpoint may need a SafeCopy migration, so+            -- closing must still write one.+            n `shouldBe` 1+            valueIn dir `shouldReturn` 1++        it "skips the checkpoint right after createCheckpoint" $ \dir -> do+            n <- closeAfter dir $ \acid -> do+                update acid Increment+                createCheckpoint acid+            -- Only the explicit checkpoint; the close wrote nothing.+            n `shouldBe` 1+            valueIn dir `shouldReturn` 1++        it "skips the checkpoint after repeated checkpoints with no events" $ \dir -> do+            n <- closeAfter dir $ \acid -> do+                update acid Increment+                createCheckpoint acid+                createCheckpoint acid+            n `shouldBe` 2+            valueIn dir `shouldReturn` 1++        it "writes a checkpoint when events follow the last checkpoint" $ \dir -> do+            n <- closeAfter dir $ \acid -> do+                update acid Increment+                createCheckpoint acid+                update acid Increment+            n `shouldBe` 2+            valueIn dir `shouldReturn` 2++        it "keeps the state recoverable across repeated skipped closes" $ \dir -> do+            let cycle_ = closeAfter dir $ \acid -> do+                    update acid Increment+                    createCheckpoint acid+            counts <- sequence [cycle_, cycle_, cycle_]+            counts `shouldBe` [1, 1, 1]+            valueIn dir `shouldReturn` 3++-- | Open the state in @dir@, run the action, then 'createCheckpointAndClose'.+-- Returns the number of checkpoints written while the state was open.+closeAfter :: FilePath -> (AcidState Counter -> IO ()) -> IO Int+closeAfter dir action = do+    countBefore <- checkpointCount dir+    acid <- openLocalStateFrom dir (Counter 0)+    action acid+    createCheckpointAndClose acid+    countAfter <- checkpointCount dir+    return (countAfter - countBefore)++valueIn :: FilePath -> IO Int+valueIn dir = do+    acid <- openLocalStateFrom dir (Counter 0)+    n <- query acid Current+    closeAcidState acid+    return n++-- | Number of entries in the checkpoint log.  Only valid while the state+-- is closed.+checkpointCount :: FilePath -> IO Int+checkpointCount dir = do+    fLog <- openFileLog (mkCheckpointsLogKey dir defaultSerialisationLayer)+              :: IO (FileLog (Checkpoint Counter))+    n <- askCurrentEntryId fLog+    closeFileLog fLog+    return n++withStateDir :: (FilePath -> IO ()) -> IO ()+withStateDir action = do+    tmp <- getTemporaryDirectory+    let dir = tmp </> "acid-state-checkpoint-spec"+    bracket_ (removePathForcibly dir) (removePathForcibly dir) (action dir)