acid-state 0.12.4 → 0.16.1.4
raw patch · 170 files changed
Files
- CHANGELOG.md +117/−0
- acid-state.cabal +174/−45
- benchmarks/loading/Benchmark.hs +138/−41
- benchmarks/loading/Benchmark/FileSystem.hs +57/−0
- benchmarks/loading/Benchmark/Model.hs +16/−0
- benchmarks/loading/Benchmark/Prelude.hs +42/−0
- examples/CheckpointCutsEvent.hs +85/−0
- examples/Examples.hs +17/−0
- examples/HelloDatabase.hs +1/−2
- examples/HelloWorld.hs +2/−4
- examples/HelloWorldNoTH.hs +7/−12
- examples/KeyValue.hs +1/−6
- examples/KeyValueNoTH.hs +5/−11
- examples/MonadStateConstraint.hs +44/−0
- examples/ParameterisedState.hs +48/−0
- examples/Proxy.hs +9/−11
- examples/RemoteClient.hs +6/−4
- examples/RemoteCommon.hs +1/−3
- examples/RemoteServer.hs +6/−4
- examples/SlowCheckpoint.hs +27/−7
- examples/StressTest.hs +1/−5
- examples/StressTestNoTH.hs +6/−11
- examples/errors/ChangeState.hs +23/−11
- examples/errors/ChangeVersion.hs +65/−0
- examples/errors/Exceptions.hs +42/−9
- examples/errors/RemoveEvent.hs +22/−8
- repair/Main.hs +10/−0
- src-unix/FileIO.hs +17/−131
- src-win32/FileIO.hs +1/−37
- src/Data/Acid.hs +1/−1
- src/Data/Acid/Abstract.hs +11/−36
- src/Data/Acid/Advanced.hs +5/−0
- src/Data/Acid/Archive.hs +53/−5
- src/Data/Acid/Common.hs +13/−24
- src/Data/Acid/Core.hs +90/−37
- src/Data/Acid/Local.hs +198/−47
- src/Data/Acid/Log.hs +87/−69
- src/Data/Acid/Memory.hs +8/−10
- src/Data/Acid/Memory/Pure.hs +11/−16
- src/Data/Acid/Remote.hs +127/−56
- src/Data/Acid/Repair.hs +142/−0
- src/Data/Acid/TemplateHaskell.hs +298/−121
- test-state/OldStateTest1/checkpoints-0000000000.log +0/−0
- test-state/OldStateTest1/checkpoints.version +1/−0
- test-state/OldStateTest1/events-0000000000.log binary
- test-state/OldStateTest1/events-0000000681.log +0/−0
- test-state/OldStateTest1/events.version +1/−0
- test-state/OldStateTest2/checkpoints-0000000000.log +0/−0
- test-state/OldStateTest2/checkpoints.version +1/−0
- test-state/OldStateTest2/events-0000000000.log binary
- test-state/OldStateTest2/events-0000000003.log binary
- test-state/OldStateTest2/events-0000000007.log binary
- test-state/OldStateTest2/events-0000000012.log binary
- test-state/OldStateTest2/events-0000000014.log binary
- test-state/OldStateTest2/events-0000000017.log binary
- test-state/OldStateTest2/events-0000000019.log binary
- test-state/OldStateTest2/events-0000000031.log binary
- test-state/OldStateTest2/events-0000000042.log binary
- test-state/OldStateTest2/events-0000000044.log binary
- test-state/OldStateTest2/events-0000000046.log binary
- test-state/OldStateTest2/events-0000000057.log binary
- test-state/OldStateTest2/events-0000000059.log binary
- test-state/OldStateTest2/events-0000000060.log binary
- test-state/OldStateTest2/events-0000000069.log binary
- test-state/OldStateTest2/events-0000000078.log binary
- test-state/OldStateTest2/events-0000000079.log binary
- test-state/OldStateTest2/events-0000000082.log binary
- test-state/OldStateTest2/events-0000000098.log binary
- test-state/OldStateTest2/events-0000000100.log binary
- test-state/OldStateTest2/events-0000000102.log binary
- test-state/OldStateTest2/events-0000000104.log binary
- test-state/OldStateTest2/events-0000000112.log binary
- test-state/OldStateTest2/events-0000000117.log binary
- test-state/OldStateTest2/events-0000000122.log binary
- test-state/OldStateTest2/events-0000000126.log binary
- test-state/OldStateTest2/events-0000000127.log binary
- test-state/OldStateTest2/events-0000000131.log binary
- test-state/OldStateTest2/events-0000000141.log binary
- test-state/OldStateTest2/events-0000000143.log binary
- test-state/OldStateTest2/events-0000000149.log +0/−0
- test-state/OldStateTest2/events.version +1/−0
- test-state/OldStateTest3/checkpoints-0000000000.log binary
- test-state/OldStateTest3/checkpoints-0000000001.log binary
- test-state/OldStateTest3/checkpoints-0000000003.log binary
- test-state/OldStateTest3/checkpoints-0000000004.log binary
- test-state/OldStateTest3/checkpoints-0000000005.log binary
- test-state/OldStateTest3/checkpoints-0000000006.log binary
- test-state/OldStateTest3/checkpoints-0000000007.log binary
- test-state/OldStateTest3/checkpoints-0000000008.log binary
- test-state/OldStateTest3/checkpoints-0000000009.log binary
- test-state/OldStateTest3/checkpoints-0000000010.log binary
- test-state/OldStateTest3/checkpoints-0000000011.log binary
- test-state/OldStateTest3/checkpoints-0000000012.log binary
- test-state/OldStateTest3/checkpoints-0000000014.log binary
- test-state/OldStateTest3/checkpoints-0000000016.log binary
- test-state/OldStateTest3/checkpoints-0000000017.log binary
- test-state/OldStateTest3/checkpoints-0000000019.log binary
- test-state/OldStateTest3/checkpoints-0000000020.log binary
- test-state/OldStateTest3/checkpoints-0000000022.log binary
- test-state/OldStateTest3/checkpoints-0000000023.log binary
- test-state/OldStateTest3/checkpoints-0000000024.log binary
- test-state/OldStateTest3/checkpoints-0000000025.log binary
- test-state/OldStateTest3/checkpoints-0000000028.log binary
- test-state/OldStateTest3/checkpoints-0000000029.log binary
- test-state/OldStateTest3/checkpoints-0000000031.log binary
- test-state/OldStateTest3/checkpoints-0000000032.log binary
- test-state/OldStateTest3/checkpoints-0000000033.log binary
- test-state/OldStateTest3/checkpoints-0000000034.log binary
- test-state/OldStateTest3/checkpoints-0000000037.log binary
- test-state/OldStateTest3/checkpoints-0000000040.log binary
- test-state/OldStateTest3/checkpoints-0000000041.log binary
- test-state/OldStateTest3/checkpoints-0000000043.log binary
- test-state/OldStateTest3/checkpoints-0000000044.log binary
- test-state/OldStateTest3/checkpoints-0000000045.log binary
- test-state/OldStateTest3/checkpoints-0000000046.log binary
- test-state/OldStateTest3/checkpoints-0000000047.log +0/−0
- test-state/OldStateTest3/checkpoints.version +1/−0
- test-state/OldStateTest3/events-0000000000.log binary
- test-state/OldStateTest3/events-0000000001.log binary
- test-state/OldStateTest3/events-0000000002.log binary
- test-state/OldStateTest3/events-0000000003.log binary
- test-state/OldStateTest3/events-0000000006.log binary
- test-state/OldStateTest3/events-0000000016.log binary
- test-state/OldStateTest3/events-0000000018.log binary
- test-state/OldStateTest3/events-0000000023.log binary
- test-state/OldStateTest3/events-0000000027.log binary
- test-state/OldStateTest3/events-0000000029.log binary
- test-state/OldStateTest3/events-0000000030.log binary
- test-state/OldStateTest3/events-0000000031.log binary
- test-state/OldStateTest3/events-0000000033.log binary
- test-state/OldStateTest3/events-0000000034.log binary
- test-state/OldStateTest3/events-0000000036.log binary
- test-state/OldStateTest3/events-0000000037.log binary
- test-state/OldStateTest3/events-0000000038.log binary
- test-state/OldStateTest3/events-0000000041.log binary
- test-state/OldStateTest3/events-0000000044.log binary
- test-state/OldStateTest3/events-0000000051.log binary
- test-state/OldStateTest3/events-0000000054.log binary
- test-state/OldStateTest3/events-0000000055.log binary
- test-state/OldStateTest3/events-0000000058.log binary
- test-state/OldStateTest3/events-0000000059.log binary
- test-state/OldStateTest3/events-0000000060.log binary
- test-state/OldStateTest3/events-0000000061.log binary
- test-state/OldStateTest3/events-0000000062.log binary
- test-state/OldStateTest3/events-0000000064.log binary
- test-state/OldStateTest3/events-0000000065.log binary
- test-state/OldStateTest3/events-0000000066.log binary
- test-state/OldStateTest3/events-0000000067.log binary
- test-state/OldStateTest3/events-0000000068.log binary
- test-state/OldStateTest3/events-0000000070.log binary
- test-state/OldStateTest3/events-0000000075.log binary
- test-state/OldStateTest3/events-0000000076.log binary
- test-state/OldStateTest3/events-0000000077.log binary
- test-state/OldStateTest3/events-0000000078.log binary
- test-state/OldStateTest3/events-0000000079.log binary
- test-state/OldStateTest3/events-0000000081.log binary
- test-state/OldStateTest3/events-0000000083.log binary
- test-state/OldStateTest3/events-0000000088.log binary
- test-state/OldStateTest3/events-0000000089.log binary
- test-state/OldStateTest3/events-0000000092.log binary
- test-state/OldStateTest3/events-0000000094.log binary
- test-state/OldStateTest3/events-0000000097.log binary
- test-state/OldStateTest3/events-0000000099.log binary
- test-state/OldStateTest3/events-0000000100.log +0/−0
- test-state/OldStateTest3/events.version +1/−0
- test/Data/Acid/KeyValueStateMachine.hs +169/−0
- test/Data/Acid/StateMachineTest.hs +527/−0
- test/Data/Acid/TemplateHaskellSpec.hs +165/−0
- test/Spec.hs +1/−0
- test/StateMachine.hs +9/−0
@@ -0,0 +1,117 @@+0.16.1.4+=========++_Andreas Abel, 2025-08-27_++- Drop support for GHC 7+- Tested with GHC 8.2 - 9.14.1-alpha1++0.16.1.3+========++_Andreas Abel, 2023-08-03_++- Support template-haskell-2.21+ ([#162](https://github.com/acid-state/acid-state/pull/162))+- Tested with GHC 7.8 - 9.8.1-alpha1++0.16.1.2+========++_Andreas Abel, 2023-04-06_++- Support unix-2.8+- Tested with GHC 7.8 - 9.6.1++0.16.1.1+========++_Andreas Abel, 2022-06-01_++- Adapt to changes in hedgehog-1.1 related to barbies+- Support mtl-2.3+- Tested with GHC 7.8 - 9.2.3++0.16.1+======++_David Fox, 2022-02-18_++ - Support GHC-9.0.2, template-haskell 2.18++0.16.0.1+========++_Jeremy Shaw, 2020-05-19_++0.16.0+======++_Jeremy Shaw, 2019-11-14_++ - support network-3.x and ghc-8.8.1+ - Fix tests+ ([#131](https://github.com/acid-state/acid-state/pull/131))+ - Haddock documentation fixes+ - Update build dependency constraints++0.15.2+======++ - adds acid-state-repair recovery tool+ ([#126](https://github.com/acid-state/acid-state/pull/16))+ - parameterize the underlying serialization library+ ([#96](https://github.com/acid-state/acid-state/pull/96))+ - support safecopy-0.10, which supports GHC.Generics+ ([#128](https://github.com/acid-state/acid-state/pull/128))++0.15.0+======++ - change text of error messages to include module names+ ([#116](https://github.com/acid-state/acid-state/pull/116))+ - depend on filelock library to avoid locking bug+ ([#91](https://github.com/acid-state/acid-state/pull/91))+ - permit events that are polymorphic in the base monad, with a MonadReader/MonadState constraint+ ([#94](https://github.com/acid-state/acid-state/pull/94))+ - fix a minor memory leak+ ([#104](https://github.com/acid-state/acid-state/pull/104))+ - add a test suite and extend examples+ ([#98](https://github.com/acid-state/acid-state/pull/98))+ - improve benchmarks+ ([#113](https://github.com/acid-state/acid-state/pull/113))+ - expose internal modules (subject to change in the future)++0.14.3+======++ - support building on GHC 8.2+ - update links from seize.it to github.com++0.14.2+======++ - createCheckpoint now cuts a new events file+ ([#74](https://github.com/acid-state/acid-state/pull/74))++0.14.1+======++ - fix bug in archiveLog that resulted in logs being moved prematurely+ ([#22](https://github.com/acid-state/acid-state/issues/22))+ - tweaks for GHC 8.0.x, template-haskell 2.11.x+ - fix compilation of benchmarks++0.14.0+======++ - fixes for cereal 0.5 while maintaining cereal 0.4+ compatibility. IMPORTANT: cereal 0.5 / safecopy 0.9 change the+ serialization format of Float/Double. Migration should be performed+ automatically on old data. However, you should be aware that once+ you upgrade to safecopy 0.9 / cereal 0.5, your data will be+ migrated and not readable by older versions of your application+ which are compiled against safecopy 0.8 / cereal 0.4.++ - additional fixes for TH and kinded type variables+ ([#56](https://github.com/acid-state/acid-state/pull/56))
@@ -1,54 +1,89 @@+Cabal-version: 1.18 Name: acid-state-Version: 0.12.4+Version: 0.16.1.4 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: http://acid-state.seize.it/+Homepage: https://github.com/acid-state/acid-state License: PublicDomain Author: David Himmelstrup Maintainer: Lemmih <lemmih@gmail.com> -- Copyright: Category: Database Build-type: Simple-Cabal-version: >=1.10++tested-with:+ GHC == 9.14.1+ GHC == 9.12.2+ GHC == 9.10.2+ GHC == 9.8.4+ GHC == 9.6.7+ GHC == 9.4.8+ GHC == 9.2.8+ GHC == 9.0.2+ GHC == 8.10.7+ GHC == 8.8.4+ GHC == 8.6.5+ GHC == 8.4.4+ GHC == 8.2.2+ -- cabal-3.12 cannot install dependency system-filepath any more in the GHC 8.0 environment+ -- https://github.com/haskell/cabal/issues/10379+ -- GHC == 8.0.2++Extra-doc-files:+ CHANGELOG.md Extra-source-files:- examples/*.hs- examples/errors/*.hs- src-win32/*.hs- src-unix/*.hs+ test-state/OldStateTest1/*.log+ test-state/OldStateTest1/*.version+ test-state/OldStateTest2/*.log+ test-state/OldStateTest2/*.version+ test-state/OldStateTest3/*.log+ test-state/OldStateTest3/*.version Source-repository head type: git location: https://github.com/acid-state/acid-state +flag skip-state-machine-test+ description: If enabled, do not build/run the state-machine test+ default: False+ manual: False+ Library- Exposed-Modules: Data.Acid,- Data.Acid.Local, Data.Acid.Memory,- Data.Acid.Memory.Pure, Data.Acid.Remote,+ Exposed-Modules: Data.Acid+ Data.Acid.Archive+ Data.Acid.Common+ Data.Acid.Local Data.Acid.Memory+ Data.Acid.Memory.Pure Data.Acid.Remote Data.Acid.Advanced+ Data.Acid.Log Data.Acid.CRC+ Data.Acid.Abstract Data.Acid.Core+ Data.Acid.TemplateHaskell+ Data.Acid.Repair - Other-modules: Data.Acid.Log, Data.Acid.Archive,- Data.Acid.CRC, Paths_acid_state,- Data.Acid.TemplateHaskell, Data.Acid.Common, FileIO,- Data.Acid.Abstract, Data.Acid.Core+ Other-modules: Paths_acid_state+ FileIO - Build-depends: array,- base >= 4 && < 5,- bytestring >= 0.10,- cereal >= 0.4.1.0,- containers,- extensible-exceptions,- safecopy >= 0.6,- stm >= 2.4,- directory,- filepath,- mtl,- network,- template-haskell+ -- Lower bounds taken from GHC 8.0 / LTS 7.0+ Build-depends: array >= 0.5.1.1+ , base >= 4.9 && < 5+ , bytestring >= 0.10.8.0+ , cereal >= 0.5.3.0+ , containers >= 0.5.7.1+ , safecopy >= 0.6 && < 0.11+ , stm >= 2.4+ , directory >= 1.2.6.2+ , filelock >= 0.1.0.1+ , filepath >= 1.4.1.0+ , mtl >= 2.2.1+ , network >= 2.6.3.1 && < 3.3+ , network-bsd+ , template-haskell >= 2.11.0.0 && < 2.25+ , th-expand-syns >= 0.4.0.0 if os(windows)- Build-depends: Win32+ Build-depends: Win32 >= 2.3.1.1 else- Build-depends: unix+ Build-depends: unix >= 2.7.2.0 Hs-Source-Dirs: src/ @@ -58,10 +93,98 @@ Hs-Source-Dirs: src-unix/ default-language: Haskell2010- GHC-Options: -fwarn-unused-imports -fwarn-unused-binds+ GHC-Options: -Wall+ -Wcompat+ -Wno-dodgy-imports+ -Wno-missing-signatures+ -Wno-name-shadowing+ -Wno-unused-do-bind+ -Wno-unused-matches +executable acid-state-repair+ hs-source-dirs: repair+ build-depends: acid-state+ , base+ , directory+ main-is: Main.hs+ default-language: Haskell2010 +test-suite specs+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ main-is: Spec.hs+ build-depends: base+ , acid-state+ , deepseq+ , hspec+ , mtl+ , safecopy+ , template-haskell+ build-tool-depends: hspec-discover:hspec-discover+ ghc-options: -threaded -rtsopts -with-rtsopts=-N+ other-modules: Data.Acid.TemplateHaskellSpec+ default-language: Haskell2010++test-suite state-machine+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ main-is: StateMachine.hs+ build-depends: base+ , acid-state+ , containers+ , deepseq >= 1.4.0.0+ , directory+ , hedgehog+ , mtl+ , safecopy+ ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall+ other-modules: Data.Acid.KeyValueStateMachine+ Data.Acid.StateMachineTest+ default-language: Haskell2010+ default-extensions: TypeOperators++ if flag(skip-state-machine-test)+ buildable: False++test-suite examples+ type: exitcode-stdio-1.0+ hs-source-dirs: examples+ examples/errors+ main-is: Examples.hs+ build-depends: base+ , acid-state+ , cereal+ , containers+ , directory+ , mtl+ , network+ , safecopy+ , text+ , time+ ghc-options: -threaded -rtsopts -with-rtsopts=-N+ other-modules: ChangeState+ ChangeVersion+ CheckpointCutsEvent+ Exceptions+ HelloDatabase+ HelloWorld+ HelloWorldNoTH+ KeyValue+ KeyValueNoTH+ MonadStateConstraint+ ParameterisedState+ RemoteClient+ RemoteCommon+ RemoteServer+ RemoveEvent+ SlowCheckpoint+ StressTest+ StressTestNoTH+ if !os(windows)+ other-modules: Proxy+ default-language: Haskell2010+ benchmark loading-benchmark type: exitcode-stdio-1.0@@ -69,37 +192,43 @@ benchmarks/loading main-is: Benchmark.hs+ other-modules:+ Benchmark.FileSystem+ Benchmark.Model+ Benchmark.Prelude build-depends: random, directory, system-fileio == 0.3.*, system-filepath,- criterion >= 0.8 && < 1.1,+ -- perRunEnv was added in criterion-1.2.0.0+ criterion >= 1.2.0.0 && < 1.7, mtl, base, acid-state default-language: Haskell2010 default-extensions:- PatternGuards+ -- keep this list alphabetically sorted!+ ConstraintKinds+ DataKinds+ DefaultSignatures+ DeriveDataTypeable+ EmptyDataDecls+ FlexibleContexts+ FlexibleInstances GADTs- StandaloneDeriving MultiParamTypeClasses- ScopedTypeVariables- FlexibleInstances- TypeFamilies- TypeOperators- FlexibleContexts NoImplicitPrelude- EmptyDataDecls- DataKinds NoMonomorphismRestriction+ OverloadedStrings+ PatternGuards RankNTypes- ConstraintKinds- DefaultSignatures- TupleSections+ ScopedTypeVariables+ StandaloneDeriving TemplateHaskell- OverloadedStrings- DeriveDataTypeable+ TupleSections+ TypeFamilies+ TypeOperators ghc-options: -O2
@@ -1,78 +1,175 @@+{-# LANGUAGE RecordWildCards #-} import Benchmark.Prelude import Criterion.Main import qualified Data.Acid as Acid+import qualified Data.Acid.Memory as Memory import qualified Benchmark.FileSystem as FS import qualified Benchmark.Model as Model; import Benchmark.Model (Model) import qualified System.Random as Random - main :: IO ()-main = do+main = defaultMain [benchmark defaultBenchmarkInterfaces [100,200,300,400]] - workingPath <- do- workingPath <- FS.getTemporaryDirectory- rndStr <- replicateM 16 $ Random.randomRIO ('a', 'z')- return $ workingPath <> "acid-state" <> "benchmarks" <> "loading" <> FS.decodeString rndStr+benchmark :: [BenchmarkInterface] -> [Int] -> Benchmark+benchmark bis sizes = env setupWorkingPath $ \ workingPath ->+ bgroup "" $ map (benchmarksGroup bis workingPath) sizes ++-- | An acid-state interface to be benchmarked.+data BenchmarkInterface = forall m .+ BenchmarkInterface+ { benchName :: String+ -- ^ Name of the interface, for use in constructing benchmarks.+ , benchPersist :: Bool+ -- ^ Does this interface actually persist data to disk? If+ -- it doesn't, some benchmarks are not applicable.+ , benchOpen :: FS.FilePath -> IO (Acid.AcidState m)+ -- ^ Open an acid-state component with the given path. Note+ -- that the type of the state is encapsulated within+ -- 'BenchmarkInterface'.+ , benchUpdate :: Acid.AcidState m -> [[Int]] -> IO ()+ -- ^ Execute an 'insert' update against the acid-state.+ , benchQuery :: Acid.AcidState m -> IO Int+ -- ^ Execute a 'sumUp' query against the state.+ }++memoryBenchmarkInterface :: BenchmarkInterface+memoryBenchmarkInterface =+ BenchmarkInterface { benchName = "Memory"+ , benchPersist = False+ , benchOpen = const $ Memory.openMemoryState mempty+ , benchUpdate = \ inst v -> Acid.update inst (Model.Insert v)+ , benchQuery = \ inst -> Acid.query inst Model.SumUp+ }++localBenchmarkInterface :: BenchmarkInterface+localBenchmarkInterface =+ BenchmarkInterface { benchName = "Local"+ , benchPersist = True+ , benchOpen = \ p -> Acid.openLocalStateFrom (FS.encodeString p) mempty+ , benchUpdate = \ inst v -> Acid.update inst (Model.Insert v)+ , benchQuery = \ inst -> Acid.query inst Model.SumUp+ }++defaultBenchmarkInterfaces :: [BenchmarkInterface]+defaultBenchmarkInterfaces = [memoryBenchmarkInterface, localBenchmarkInterface]+++setupWorkingPath :: IO FS.FilePath+setupWorkingPath = do+ workingPath <- do workingPath <- FS.getTemporaryDirectory+ rndStr <- replicateM 16 $ Random.randomRIO ('a', 'z')+ return $ workingPath <> "acid-state" <> "benchmarks" <> "loading" <> FS.decodeString rndStr putStrLn $ "Working under the following temporary directory: " ++ FS.encodeString workingPath FS.removeTreeIfExists workingPath FS.createTree workingPath+ return workingPath - defaultMain =<< sequence- [- prepareBenchmarksGroup workingPath $ 100,- prepareBenchmarksGroup workingPath $ 200,- prepareBenchmarksGroup workingPath $ 300,- prepareBenchmarksGroup workingPath $ 400- ] +benchmarksGroup :: [BenchmarkInterface] -> FS.FilePath -> Int -> Benchmark+benchmarksGroup bis workingPath size =+ bgroup (show size)+ [ bgroup (benchName bi) $+ initializeBenchmarksGroup bi workingPath' size+ : if benchPersist bi then [openCloseBenchmarksGroup bi workingPath' size] else []+ | bi <- bis+ ]+ where+ workingPath' = workingPath <> FS.decodeString (show size) -prepareBenchmarksGroup :: FS.FilePath -> Int -> IO Benchmark-prepareBenchmarksGroup workingPath size = do+-- | The Initialize benchmarks measure how long it takes to open an+-- empty 'AcidState' component, call 'initialize' to populate it with+-- data, and optionally checkpoint before closing.+initializeBenchmarksGroup :: BenchmarkInterface -> FS.FilePath -> Int -> Benchmark+initializeBenchmarksGroup bi workingPath size =+ bgroup "Initialize"+ [ bench "Without checkpoint" $ perRunEnv (prepareInitialize bi workingPath) $ \ _ ->+ initializeClose bi workingPath size+ , bench "With checkpoint" $ perRunEnv (prepareInitialize bi workingPath) $ \ _ ->+ initializeCheckpointClose bi workingPath size+ ]++prepareInitialize :: BenchmarkInterface -> FS.FilePath -> IO ()+prepareInitialize bi workingPath =+ when (benchPersist bi) $ do FS.removeTreeIfExists workingPath+ FS.createTree workingPath+++-- | The OpenClose benchmarks measure how long it takes to open an+-- existing on-disk 'AcidState' component (either from a checkpoint or+-- from a transaction log), optionally execute a query over the entire+-- state, then close. These benchmarks are not applicable if the+-- interface being benchmarked does not persist data.+openCloseBenchmarksGroup :: BenchmarkInterface -> FS.FilePath -> Int -> Benchmark+openCloseBenchmarksGroup bi workingPath size =+ env (prepareOpenCloseBenchmarksGroup bi workingPath size) $ \ ~(logsInstancePath, checkpointInstancePath) -> bgroup "OpenClose"+ [+ bench "From Logs" $ nfIO $+ openClose bi logsInstancePath+ , bench "From Checkpoint" $ nfIO $+ openClose bi checkpointInstancePath+ , bench "From Logs (with query)" $ nfIO $+ openQueryClose bi logsInstancePath+ , bench "From Checkpoint (with query)" $ nfIO $+ openQueryClose bi checkpointInstancePath+ ]++-- | Set up data on disk for the open/close benchmarks. This+-- initializes an instance, creates a copy of it (for restoring from+-- transaction logs), then checkpoints.+prepareOpenCloseBenchmarksGroup :: BenchmarkInterface -> FS.FilePath -> Int -> IO (FS.FilePath, FS.FilePath)+prepareOpenCloseBenchmarksGroup bi workingPath size = do putStrLn $ "Preparing instances for size " ++ show size let- workingPath' = workingPath <> (FS.decodeString $ show size)- logsInstancePath = workingPath' <> "logs-instance"- checkpointInstancePath = workingPath' <> "checkpoint-instance"+ logsInstancePath = workingPath <> "logs-instance"+ checkpointInstancePath = workingPath <> "checkpoint-instance" FS.createTree logsInstancePath FS.createTree checkpointInstancePath putStrLn "Initializing"- inst <- initialize checkpointInstancePath size+ initialize bi checkpointInstancePath size $ \inst -> do - putStrLn "Copying"- FS.copy checkpointInstancePath logsInstancePath- FS.removeFile $ logsInstancePath <> "open.lock"+ putStrLn "Copying"+ FS.copy checkpointInstancePath logsInstancePath+ FS.removeFile $ logsInstancePath <> "open.lock" - putStrLn "Checkpointing"- Acid.createCheckpoint inst+ putStrLn "Checkpointing"+ Acid.createCheckpoint inst - putStrLn "Closing"- Acid.closeAcidState inst+ putStrLn "Closing"+ Acid.closeAcidState inst - return $ bgroup (show size)- [- bench "From Logs" $ nfIO $- load logsInstancePath >>= Acid.closeAcidState,- bench "From Checkpoint" $ nfIO $- load checkpointInstancePath >>= Acid.closeAcidState- ]+ return (logsInstancePath, checkpointInstancePath) +initialize :: BenchmarkInterface -> FS.FilePath -> Int -> (forall m . Acid.AcidState m -> IO r) -> IO r+initialize BenchmarkInterface{..} p size k = do+ inst <- benchOpen p+ let values = replicate size $ replicate 100 $ replicate 100 1+ mapM_ (benchUpdate inst) values+ k inst -load :: FS.FilePath -> IO (Acid.AcidState Model)-load path = Acid.openLocalStateFrom (FS.encodeString path) mempty+initializeClose :: BenchmarkInterface -> FS.FilePath -> Int -> IO ()+initializeClose bi p size = initialize bi p size Acid.closeAcidState -initialize :: FS.FilePath -> Int -> IO (Acid.AcidState Model)-initialize path size = do- inst <- Acid.openLocalStateFrom (FS.encodeString path) mempty- let values = replicate size $ replicate 100 $ replicate 100 1- mapM_ (Acid.update inst . Model.Insert) values- return inst+initializeCheckpointClose :: BenchmarkInterface -> FS.FilePath -> Int -> IO ()+initializeCheckpointClose bi p size =+ initialize bi p size $ \ inst -> do+ Acid.createCheckpoint inst+ Acid.closeAcidState inst +openClose :: BenchmarkInterface -> FS.FilePath -> IO ()+openClose BenchmarkInterface{..} p = benchOpen p >>= Acid.closeAcidState++openQueryClose :: BenchmarkInterface -> FS.FilePath -> IO Int+openQueryClose BenchmarkInterface{..} p = do+ inst <- benchOpen p+ n <- benchQuery inst+ Acid.closeAcidState inst+ return n
@@ -0,0 +1,57 @@+module Benchmark.FileSystem + ( + copy,+ removeTreeIfExists,+ exists,+ getTemporaryDirectory,+ module Filesystem,+ module Filesystem.Path.CurrentOS+ )+ where++import Benchmark.Prelude hiding (stripPrefix, last)+import Filesystem.Path.CurrentOS+import Filesystem+import qualified System.Directory as Directory+import Debug.Trace+import qualified Data.List as List++++removeTreeIfExists :: FilePath -> IO ()+removeTreeIfExists path = removeTree path `catch` \e -> case e of+ _ | isDoesNotExistError e -> return ()+ | otherwise -> throwIO e++exists :: FilePath -> IO Bool+exists path = do+ isDir <- isDirectory path+ isFile <- isFile path+ return $ isDir || isFile ++getTemporaryDirectory :: IO FilePath+getTemporaryDirectory = + Directory.getTemporaryDirectory >>= return . decodeString++copy :: FilePath -> FilePath -> IO ()+copy from to = do+ isDir <- isDirectory from+ if isDir+ then copyDirectory from to+ else copyFile from to++copyDirectory :: FilePath -> FilePath -> IO ()+copyDirectory path path' = do+ members <- listDirectory path+ let members' = do+ member <- members+ let relative = + fromMaybe (error "Unexpectedly empty member path") $+ last member+ return $ path' <> relative+ sequence_ $ zipWith copy members members'++last :: FilePath -> Maybe FilePath+last p = case splitDirectories p of+ [] -> Nothing+ l -> Just $ List.last l
@@ -0,0 +1,16 @@+module Benchmark.Model where++import Benchmark.Prelude hiding (insert)+import qualified Data.Acid as Acid++++type Model = [[[Int]]]++insert :: [[Int]] -> Acid.Update Model ()+insert = modify . (:)++sumUp :: Acid.Query Model Int+sumUp = sum . map (sum . map sum) <$> ask++Acid.makeAcidic ''Model ['insert, 'sumUp]
@@ -0,0 +1,42 @@+module Benchmark.Prelude + ( + module Prelude,+ module Control.Monad,+ module Control.Applicative,+ module Control.Arrow,+ module Data.Monoid,+ module Data.Foldable,+ module Data.Traversable,+ module Data.Maybe,+ module Data.List,+ module Data.Data,++ -- mtl+ module Control.Monad.State,+ module Control.Monad.Reader,++ -- exceptions+ module Control.Exception,+ module System.IO.Error,+ )+ where++import Prelude hiding (concat, foldr, mapM_, sequence_, foldl1, maximum, minimum, product, sum, all, and, any, concatMap, elem, foldl, foldr1, notElem, or, mapM, sequence, FilePath)+import Control.Monad hiding (mapM_, sequence_, forM_, msum, mapM, sequence, forM)+import Control.Applicative+import Control.Arrow +import Data.Monoid+import Data.Foldable+import Data.Traversable+import Data.Maybe+import Data.List hiding (concat, foldr, foldl1, maximum, minimum, product, sum, all, and, any, concatMap, elem, foldl, foldr1, notElem, or, find, maximumBy, minimumBy, mapAccumL, mapAccumR, foldl')+import Data.Data++-- mtl+import Control.Monad.State hiding (mapM_, sequence_, forM_, msum, mapM, sequence, forM)+import Control.Monad.Reader hiding (mapM_, sequence_, forM_, msum, mapM, sequence, forM)++-- exceptions+import Control.Exception+import System.IO.Error+
@@ -0,0 +1,85 @@+{-+This example is mostly just to test that this bug is fixed:++https://github.com/acid-state/acid-state/issues/73++At the end of a run, the checkpoint file should contain a single+checkpoint and the event file should be empty. The old checkpoints and+events should be in the Archive directory.++In the Acrhive directory, each checkpoint file should contain one+checkpoint, and each event file should contain 10 events.++If you comment out the 'createArchive' line below, then the checkpoint+files should contain 10 checkpoints each.++-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+module CheckpointCutsEvent (main) where++-- import Control.Concurrent+import Control.Applicative+import Control.Monad+import Control.Monad.State ( get, put )+import Data.Acid+import Data.List ( sort )+import Data.SafeCopy+import System.Directory+import System.Environment++------------------------------------------------------+-- The Haskell structure that we want to encapsulate++newtype Counter = Counter { unCounter :: Integer }+ deriving (Show)++$(deriveSafeCopy 0 'base ''Counter)++incCounter :: Update Counter Integer+incCounter =+ do (Counter c) <- get+ let c' = succ c+ put (Counter c')+ return c'++$(makeAcidic ''Counter ['incCounter])+++main :: IO ()+main = do+ putStrLn "CheckpointCutsEvent test"+ exists <- doesDirectoryExist fp+ when exists $ removeDirectoryRecursive fp+ acid <- openLocalStateFrom fp (Counter 0)+ replicateM_ 10 $ do is <- replicateM 10 (update acid IncCounter)+ print is+ createCheckpoint acid+ createArchive acid+ closeAcidState acid+ checkDirectoryContents fp expected_state+ checkDirectoryContents (fp ++ "/Archive") expected_archive+ s <- readFile (fp ++ "/events-0000000100.log")+ unless (s == "") $ error "non-empty events file"+ putStrLn "CheckpointCutsEvent done"+ where+ fp = "state/CheckpointCutsEvent"++ expected_state = [".","..","Archive","checkpoints-0000000009.log","checkpoints-0000000010.log"+ ,"checkpoints.version","events-0000000100.log","events.version","open.lock"]+ expected_archive = [".","..","checkpoints-0000000000.log","checkpoints-0000000001.log"+ ,"checkpoints-0000000002.log","checkpoints-0000000003.log","checkpoints-0000000004.log"+ ,"checkpoints-0000000005.log","checkpoints-0000000006.log","checkpoints-0000000007.log"+ ,"checkpoints-0000000008.log","events-0000000000.log","events-0000000010.log"+ ,"events-0000000020.log","events-0000000030.log","events-0000000040.log"+ ,"events-0000000050.log","events-0000000060.log","events-0000000070.log"+ ,"events-0000000080.log","events-0000000090.log"]+++checkDirectoryContents :: FilePath -> [FilePath] -> IO ()+checkDirectoryContents fp expected_fs = do+ putStrLn $ "Checking contents of " ++ fp+ fs <- sort <$> getDirectoryContents fp+ unless (fs == expected_fs) $ error $ "bad contents of " ++ fp ++ ": expected "+ ++ show expected_fs ++ " but got " ++ show fs
@@ -0,0 +1,17 @@+module Main (main) where++import qualified ChangeState+import qualified ChangeVersion+import qualified CheckpointCutsEvent+import qualified Exceptions+import qualified RemoveEvent+import qualified SlowCheckpoint++main :: IO ()+main = do+ ChangeState.test+ ChangeVersion.test+ CheckpointCutsEvent.main+ Exceptions.test+ RemoveEvent.test+ SlowCheckpoint.main
@@ -1,8 +1,7 @@-{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} -module Main (main) where+module HelloDatabase (main) where import Data.Acid
@@ -1,21 +1,19 @@-{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} -module Main (main) where+module HelloWorld (main) where import Control.Monad.Reader import Control.Monad.State import Data.Acid import Data.SafeCopy-import Data.Typeable import System.Environment ------------------------------------------------------ -- The Haskell structure that we want to encapsulate data HelloWorldState = HelloWorldState String- deriving (Show, Typeable)+ deriving (Show) $(deriveSafeCopy 0 'base ''HelloWorldState)
@@ -1,24 +1,22 @@-{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TypeFamilies #-} -module Main (main) where+module HelloWorldNoTH (main) where import Data.Acid import Data.Acid.Advanced -import Control.Monad.Reader-import Control.Monad.State+import Control.Monad+import Control.Monad.Reader (ask)+import Control.Monad.State (put) import Data.SafeCopy import System.Environment -import Data.Typeable- ------------------------------------------------------ -- The Haskell structure that we want to encapsulate data HelloWorldState = HelloWorldState String- deriving (Show, Typeable)+ deriving (Show) instance SafeCopy HelloWorldState where putCopy (HelloWorldState state) = contain $ safePut state@@ -56,8 +54,6 @@ data WriteState = WriteState String data QueryState = QueryState --deriving instance Typeable WriteState instance SafeCopy WriteState where putCopy (WriteState st) = contain $ safePut st getCopy = contain $ liftM WriteState safeGet@@ -66,7 +62,6 @@ type MethodState WriteState = HelloWorldState instance UpdateEvent WriteState -deriving instance Typeable QueryState instance SafeCopy QueryState where putCopy QueryState = contain $ return () getCopy = contain $ return QueryState@@ -77,6 +72,6 @@ instance IsAcidic HelloWorldState where- acidEvents = [ UpdateEvent (\(WriteState newState) -> writeState newState)- , QueryEvent (\QueryState -> queryState)+ acidEvents = [ UpdateEvent (\(WriteState newState) -> writeState newState) safeCopyMethodSerialiser+ , QueryEvent (\QueryState -> queryState) safeCopyMethodSerialiser ]
@@ -1,8 +1,7 @@-{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} -module Main (main) where+module KeyValue (main) where import Data.Acid import Data.Acid.Remote@@ -11,13 +10,10 @@ import Control.Monad.Reader import Control.Monad.State import Data.SafeCopy-import Network import System.Environment import System.Exit import System.IO -import Data.Typeable- import qualified Data.Map as Map ------------------------------------------------------@@ -27,7 +23,6 @@ type Value = String data KeyValue = KeyValue !(Map.Map Key Value)- deriving (Typeable) $(deriveSafeCopy 0 'base ''KeyValue)
@@ -1,21 +1,19 @@-{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TypeFamilies #-} -module Main (main) where+module KeyValueNoTH (main) where import Data.Acid import Data.Acid.Advanced import Control.Applicative-import Control.Monad.Reader+import Control.Monad+import Control.Monad.Reader (ask) import qualified Control.Monad.State as State import Data.SafeCopy import System.Environment import System.IO -import Data.Typeable- import qualified Data.Map as Map ------------------------------------------------------@@ -25,7 +23,6 @@ type Value = String data KeyValue = KeyValue !(Map.Map Key Value)- deriving (Typeable) instance SafeCopy KeyValue where putCopy (KeyValue state) = contain $ safePut state@@ -73,8 +70,6 @@ data InsertKey = InsertKey Key Value data LookupKey = LookupKey Key --deriving instance Typeable InsertKey instance SafeCopy InsertKey where putCopy (InsertKey key value) = contain $ safePut key >> safePut value getCopy = contain $ InsertKey <$> safeGet <*> safeGet@@ -83,7 +78,6 @@ type MethodState InsertKey = KeyValue instance UpdateEvent InsertKey -deriving instance Typeable LookupKey instance SafeCopy LookupKey where putCopy (LookupKey key) = contain $ safePut key getCopy = contain $ LookupKey <$> safeGet@@ -93,6 +87,6 @@ instance QueryEvent LookupKey instance IsAcidic KeyValue where- acidEvents = [ UpdateEvent (\(InsertKey key value) -> insertKey key value)- , QueryEvent (\(LookupKey key) -> lookupKey key)+ acidEvents = [ UpdateEvent (\(InsertKey key value) -> insertKey key value) safeCopyMethodSerialiser+ , QueryEvent (\(LookupKey key) -> lookupKey key) safeCopyMethodSerialiser ]
@@ -0,0 +1,44 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}++module MonadStateConstraint (main) where++import Control.Monad.Reader+import Control.Monad.State+import Data.Acid+import Data.SafeCopy+import System.Environment++------------------------------------------------------+-- The Haskell structure that we want to encapsulate++data HelloWorldState = HelloWorldState String+ deriving (Show)++$(deriveSafeCopy 0 'base ''HelloWorldState)++------------------------------------------------------+-- The transaction we will execute over the state.++writeState :: MonadState HelloWorldState m => String -> m ()+writeState newValue+ = put (HelloWorldState newValue)++queryState :: MonadReader HelloWorldState m => m String+queryState = do HelloWorldState string <- ask+ return string++$(makeAcidic ''HelloWorldState ['writeState, 'queryState])++------------------------------------------------------+-- This is how AcidState is used:++main :: IO ()+main = do acid <- openLocalState (HelloWorldState "Hello world")+ args <- getArgs+ if null args+ then do string <- query acid QueryState+ putStrLn $ "The state is: " ++ string+ else do update acid (WriteState (unwords args))+ putStrLn "The state has been modified!"
@@ -0,0 +1,48 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE StandaloneDeriving #-}++module ParameterisedState (main) where++import Control.Monad.State.Strict++import Data.Acid+import qualified Data.Map as Map+import Data.SafeCopy (SafeCopy(..), deriveSafeCopy, base)+import Data.Serialize+import Data.Typeable+import GHC.Generics++data Entry k = Entry+ { key :: !k+ , val :: !Int+ }+ deriving (Eq, Ord, Generic)++instance Serialize k => Serialize (Entry k)++$(deriveSafeCopy 0 'base ''Entry)++newtype Store k = Store { store :: Map.Map k (Entry k) }+ deriving (Eq, Generic)++instance (Ord k, Serialize k, SafeCopy k, Typeable k) => SafeCopy (Store k)+instance (Ord k, Serialize k) => Serialize (Store k)++insertStore+ :: (Ord k, Serialize k)+ => Entry k+ -> Update (Store k) k+insertStore item = do+ modify $ \(Store s) -> Store $ Map.insert (key item) item s+ return (key item)++makeAcidic ''Store [ 'insertStore ]+++main :: IO ()+main = do st <- openLocalState (Store Map.empty :: Store String)+ k <- update st (InsertStore (Entry "A" 42))+ putStrLn k
@@ -1,27 +1,23 @@-{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} -module Main (main) where+module Proxy (main) where import Data.Acid import Data.Acid.Advanced (scheduleUpdate) import Data.Acid.Remote -import Control.Monad.Reader-import Control.Monad.State+import Control.Monad+import Control.Monad.Reader (ask)+import Control.Monad.State (get, put) import Data.SafeCopy-import Network import System.Environment import System.IO -import Data.Typeable- ------------------------------------------------------ -- The Haskell structure that we want to encapsulate data ProxyStressState = StressState !Int- deriving (Typeable) $(deriveSafeCopy 0 'base ''ProxyStressState) @@ -45,17 +41,19 @@ openLocal = openLocalState (StressState 0) openRemote :: String -> IO (AcidState ProxyStressState)-openRemote socket = openRemoteState skipAuthenticationPerform "localhost" (UnixSocket socket)+openRemote socket = openRemoteState skipAuthenticationPerform "localhost" port +port = 6303+ main :: IO () main = do args <- getArgs case args of ["server", socket] -> do acid <- openLocal- acidServer skipAuthenticationCheck (UnixSocket socket) acid+ acidServer skipAuthenticationCheck port acid ["proxy", from, to] -> do acid <- openRemote from- acidServer skipAuthenticationCheck (UnixSocket to) acid+ acidServer skipAuthenticationCheck port acid ["query", socket] -> do acid <- openRemote socket n <- query acid QueryState
@@ -1,10 +1,10 @@-module Main (main) where+module RemoteClient (main) where -import Control.Monad.Reader+import Control.Monad import Data.Acid import Data.Acid.Advanced import Data.Acid.Remote-import Network+import Network.Socket (SockAddr(..)) import RemoteCommon import System.Environment import System.IO@@ -13,7 +13,9 @@ -- This is how AcidState is used: open :: IO (AcidState StressState)-open = openRemoteState skipAuthenticationPerform "localhost" (PortNumber 8080)+open = openRemoteState skipAuthenticationPerform "localhost" 8080+-- on Unixy systems we could use a Unix Domain Socket+-- open = openRemoteStateSockAddr skipAuthenticationPerform (SockAddrUnix "remote.socket") main :: IO () main = do args <- getArgs
@@ -1,4 +1,3 @@-{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} @@ -8,12 +7,11 @@ import Control.Monad.State import Data.Acid import Data.SafeCopy-import Data.Typeable ------------------------------------------------------ -- The Haskell structure that we want to encapsulate -data StressState = StressState !Int deriving (Typeable)+data StressState = StressState !Int $(deriveSafeCopy 0 'base ''StressState)
@@ -1,11 +1,13 @@-module Main where+module RemoteServer where import Control.Exception (bracket) import Data.Acid (closeAcidState, openLocalState)-import Data.Acid.Remote (acidServer, skipAuthenticationCheck)-import Network (PortID (..))+import Data.Acid.Remote (acidServer, acidServerSockAddr, skipAuthenticationCheck) import RemoteCommon (StressState (..))+import Network.Socket (SockAddr(..)) main :: IO () main = bracket (openLocalState $ StressState 0)- closeAcidState $ acidServer skipAuthenticationCheck (PortNumber 8080)+ closeAcidState $ acidServer skipAuthenticationCheck 8080+-- on Unixy systems we could use a Unix Domain Socket+-- closeAcidState $ acidServerSockAddr skipAuthenticationCheck (SockAddrUnix "remote.socket")
@@ -1,15 +1,20 @@-{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} -module Main (main) where+-- With optimizations enabled, serializing the checkpoint can happen too quickly+{-# OPTIONS_GHC -O0 #-} +module SlowCheckpoint (main) where+ import Data.Acid import Control.Concurrent-import Control.Monad.State+import Control.Monad+import Control.Monad.Reader (ask)+import Control.Monad.State (get, put) import Data.SafeCopy import Data.Time+import System.Directory import System.IO ------------------------------------------------------@@ -36,13 +41,20 @@ put $ SlowCheckpoint slow (tick+1) return tick -$(makeAcidic ''SlowCheckpoint ['setComputationallyHeavyData, 'tick])+askTick :: Query SlowCheckpoint Int+askTick = do SlowCheckpoint _ tick <- ask+ return tick +$(makeAcidic ''SlowCheckpoint ['setComputationallyHeavyData, 'tick, 'askTick])+ ------------------------------------------------------ -- This is how AcidState is used: main :: IO ()-main = do acid <- openLocalStateFrom "state/SlowCheckpoint" (SlowCheckpoint 0 0)+main = do putStrLn "SlowCheckpoint test"+ exists <- doesDirectoryExist fp+ when exists $ removeDirectoryRecursive fp+ acid <- openLocalStateFrom fp (SlowCheckpoint 0 0) putStrLn "This example illustrates that the state is still accessible while" putStrLn "a checkpoint is being serialized. This is an important property when" putStrLn "the size of a checkpoint reaches several hundred megabytes."@@ -51,12 +63,20 @@ putStrLn "" doTick acid update acid SetComputationallyHeavyData- forkIO $ do putStrLn "Seriazing checkpoint..."+ forkIO $ do putStrLn "Serializing checkpoint..." t <- timeIt $ createCheckpoint acid- putStrLn $ "Checkpoint created in: " ++ show t+ n <- query acid AskTick+ putStrLn $ "Checkpoint created in: " ++ show t ++ " (saw " ++ show n ++ " ticks)"+ when (n < threshold) $ error $ "Not enough ticks! Expected at least " ++ show threshold replicateM_ 20 $ do doTick acid threadDelay (10^5)+ putStrLn "SlowCheckpoint done"+ where+ fp = "state/SlowCheckpoint"++ -- We must see at least this many ticks for the test to be considered a success+ threshold = 5 doTick acid = do tick <- update acid Tick
@@ -1,8 +1,7 @@-{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} -module Main (main) where+module StressTest (main) where import Data.Acid import Data.Acid.Advanced (groupUpdates)@@ -13,13 +12,10 @@ import System.Environment import System.IO -import Data.Typeable- ------------------------------------------------------ -- The Haskell structure that we want to encapsulate data StressState = StressState !Int- deriving (Typeable) $(deriveSafeCopy 0 'base ''StressState)
@@ -1,25 +1,22 @@-{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TypeFamilies #-} -module Main (main) where+module StressTestNoTH (main) where import Data.Acid import Data.Acid.Advanced -import Control.Monad.Reader-import Control.Monad.State+import Control.Monad+import Control.Monad.Reader (ask)+import Control.Monad.State (get, put) import Data.SafeCopy import System.Environment import System.IO -import Data.Typeable- ------------------------------------------------------ -- The Haskell structure that we want to encapsulate data StressState = StressState !Int- deriving (Typeable) instance SafeCopy StressState where putCopy (StressState state) = contain $ safePut state@@ -65,7 +62,6 @@ data PokeState = PokeState data QueryState = QueryState -deriving instance Typeable PokeState instance SafeCopy PokeState where putCopy PokeState = contain $ return () getCopy = contain $ return PokeState@@ -74,7 +70,6 @@ type MethodState PokeState = StressState instance UpdateEvent PokeState -deriving instance Typeable QueryState instance SafeCopy QueryState where putCopy QueryState = contain $ return () getCopy = contain $ return QueryState@@ -84,6 +79,6 @@ instance QueryEvent QueryState instance IsAcidic StressState where- acidEvents = [ UpdateEvent (\PokeState -> pokeState)- , QueryEvent (\QueryState -> queryState)+ acidEvents = [ UpdateEvent (\PokeState -> pokeState) safeCopyMethodSerialiser+ , QueryEvent (\QueryState -> queryState) safeCopyMethodSerialiser ]
@@ -1,19 +1,16 @@-{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} -module Main (main) where+module ChangeState (main, test) where import Data.Acid -import Control.Monad.State+import Control.Exception+import Control.Monad import Data.SafeCopy+import System.Directory import System.Environment--import Data.Typeable--import Control.Exception-import Prelude hiding (catch)+import Data.List (isSuffixOf) import qualified Data.Text as Text @@ -43,9 +40,24 @@ putStrLn "without telling AcidState how to migrate from the old version to the new." putStrLn "Hopefully this program will fail with a readable error message." putStrLn ""- firstAcid <- openLocalStateFrom "state/ChangeState" (FirstState "first state")+ firstAcid <- openLocalStateFrom fp (FirstState "first state") createCheckpoint firstAcid closeAcidState firstAcid- secondAcid <- openLocalStateFrom "state/ChangeState" (SecondState (Text.pack "This initial value shouldn't be used"))+ secondAcid <- openLocalStateFrom fp (SecondState (Text.pack "This initial value shouldn't be used")) closeAcidState secondAcid- putStrLn "If you see this message then something has gone wrong!"+ error "If you see this message then something has gone wrong!"++test :: IO ()+test = do+ putStrLn "ChangeState test"+ exists <- doesDirectoryExist fp+ when exists $ removeDirectoryRecursive fp+ handle hdl main+ putStrLn "ChangeState done"+ where+ hdl (ErrorCall msg)+ | "Could not parse saved checkpoint due to the following error: too few bytes\nFrom:\tChangeState.SecondState:\n\tdemandInput\n\n" `isSuffixOf` msg+ = putStrLn $ "Caught error: " ++ msg+ hdl e = throwIO e++fp = "state/ChangeState"
@@ -0,0 +1,65 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}++module ChangeVersion (main, test) where++import Data.Acid++import Control.Exception+import Control.Monad+import Data.SafeCopy+import System.Directory+import System.Environment++------------------------------------------------------+-- The Haskell structure that we want to encapsulate++data FirstState = FirstState String+ deriving (Show)++data SecondState = SecondState Int+ deriving (Show)++$(deriveSafeCopy 0 'base ''FirstState)+$(deriveSafeCopy 1 'base ''SecondState)+-- The version number difference is important here, as safecopy has no+-- way to reliably notice if we change a type and fail to update its+-- migration history. In some cases the serialised data will fail to+-- parse (see the "ChangeState" example), but it depends on the types+-- involved, and this will not always be the case.++------------------------------------------------------+-- The transaction we will execute over the state.++$(makeAcidic ''FirstState [])+$(makeAcidic ''SecondState [])++------------------------------------------------------+-- This is how AcidState is used:++main :: IO ()+main = do putStrLn "This example simulates what happens when you modify your safecopy"+ putStrLn "version without specifying how to migrate from the old version to the new."+ putStrLn "Hopefully this program will fail with a readable error message."+ putStrLn ""+ firstAcid <- openLocalStateFrom fp (FirstState "first state")+ createCheckpoint firstAcid+ closeAcidState firstAcid+ secondAcid <- openLocalStateFrom fp (SecondState 42)+ closeAcidState secondAcid+ error "If you see this message then something has gone wrong!"++test :: IO ()+test = do+ putStrLn "ChangeVersion test"+ exists <- doesDirectoryExist fp+ when exists $ removeDirectoryRecursive fp+ handle hdl main+ putStrLn "ChangeVersion done"+ where+ hdl (ErrorCall msg)+ | msg == "Could not parse saved checkpoint due to the following error: Failed reading: safecopy: ChangeVersion.SecondState: Cannot find getter associated with this version number: Version {unVersion = 0}\nEmpty call stack\n"+ = putStrLn $ "Caught error: " ++ msg+ hdl e = throwIO e++fp = "state/ChangeVersion"
@@ -1,18 +1,18 @@-{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE CPP #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} -module Main (main) where+module Exceptions (main, test) where import Data.Acid import Data.Acid.Local (createCheckpointAndClose) -import Control.Monad.State+import Control.Monad+import Control.Monad.State ( get, put ) import Data.SafeCopy+import System.Directory import System.Environment -import Data.Typeable- import Control.Exception import Prelude hiding (catch) @@ -20,7 +20,7 @@ -- The Haskell structure that we want to encapsulate data MyState = MyState Integer- deriving (Show, Typeable)+ deriving (Show) $(deriveSafeCopy 0 'base ''MyState) @@ -28,7 +28,7 @@ -- The transaction we will execute over the state. failEvent :: Update MyState ()-failEvent = fail "fail!"+failEvent = pure $ error "fail!" errorEvent :: Update MyState () errorEvent = error "error!"@@ -70,7 +70,40 @@ ["6"] -> update acid (StateNestedError2 (error "nested state error (2)")) _ -> do putStrLn "Call with [123456] to test error scenarios." putStrLn "If the tick doesn't get stuck, everything is fine."- n <- update acid Tick- putStrLn $ "Tick: " ++ show n+ do_tick acid `catch` \e -> do putStrLn $ "Caught exception: " ++ show (e:: SomeException) createCheckpointAndClose acid++do_tick :: AcidState MyState -> IO ()+do_tick acid = do n <- update acid Tick+ putStrLn $ "Tick: " ++ show n++test :: IO ()+test = do putStrLn "Exceptions test"+ exists <- doesDirectoryExist fp+ when exists $ removeDirectoryRecursive fp+ acid <- openLocalStateFrom fp (MyState 0)+ do_tick acid+ handle hdl $ update acid (undefined :: FailEvent)+ do_tick acid+ handle hdl $ update acid FailEvent+ do_tick acid+ handle hdl $ update acid ErrorEvent+ do_tick acid+ handle hdl $ update acid StateError+ do_tick acid+ -- We can't currently cope with an error being thrown during serialization of the state (see #38)+ -- handle hdl $ update acid StateNestedError1+ do_tick acid+ handle hdl $ update acid (StateNestedError2 (error "nested state error (2)"))+ do_tick acid+ n <- update acid Tick+ unless (n == expected_n) $ error $ "Wrong tick value, expected " ++ show expected_n+ createCheckpointAndClose acid+ putStrLn "Exceptions done"+ where+ fp = "state/Exceptions"++ hdl e = putStrLn $ "Caught exception: " ++ show (e:: SomeException)++ expected_n = 7
@@ -1,16 +1,15 @@-{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} -module Main (main) where+module RemoveEvent (main, test) where import Data.Acid -import Control.Monad.State+import Control.Monad import Data.SafeCopy+import System.Directory import System.Environment--import Data.Typeable+import Data.List (isSuffixOf) import Control.Exception import Prelude hiding (catch)@@ -44,10 +43,25 @@ putStrLn "that is required to replay the journal." putStrLn "Hopefully this program will fail with a readable error message." putStrLn ""- firstAcid <- openLocalStateFrom "state/RemoveEvent" FirstState+ firstAcid <- openLocalStateFrom fp FirstState update firstAcid FirstEvent closeAcidState firstAcid - secondAcid <- openLocalStateFrom "state/RemoveEvent" SecondState+ secondAcid <- openLocalStateFrom fp SecondState closeAcidState secondAcid- putStrLn "If you see this message then something has gone wrong!"+ error "If you see this message then something has gone wrong!"++test :: IO ()+test = do+ putStrLn "RemoveEvent test"+ exists <- doesDirectoryExist fp+ when exists $ removeDirectoryRecursive fp+ handle hdl main+ putStrLn "RemoveEvent done"+ where+ hdl (ErrorCall msg)+ | "This method is required but not available: \"RemoveEvent.FirstEvent\". Did you perhaps remove it before creating a checkpoint?" `isSuffixOf` msg+ = putStrLn $ "Caught error: " ++ msg+ hdl e = throwIO e++fp = "state/RemoveEvent"
@@ -0,0 +1,10 @@+module Main where++import Data.Acid.Repair+import System.Directory++main :: IO ()+main = do+ directory <- getCurrentDirectory+ repairEvents directory+ repairCheckpoints directory
@@ -1,36 +1,30 @@+{-# LANGUAGE CPP #-} {-# LANGUAGE ForeignFunctionInterface #-}-module FileIO(FHandle,open,write,flush,close,obtainPrefixLock,releasePrefixLock,PrefixLock) where-import System.Posix(Fd(Fd),- openFd,- fdWriteBuf,- fdToHandle,- closeFd,- OpenMode(WriteOnly,ReadWrite),- exclusive, trunc,- defaultFileFlags,- stdFileMode- )++module FileIO(FHandle,open,write,flush,close) where++import System.Posix+ ( Fd(Fd), openFd, fdWriteBuf, closeFd+ , OpenMode(WriteOnly)+#if MIN_VERSION_unix(2,8,0)+ , OpenFileFlags(creat)+#endif+ , defaultFileFlags+ , stdFileMode+ ) import Data.Word(Word8,Word32) import Foreign(Ptr) import Foreign.C(CInt(..))-import System.IO -import Data.Maybe (listToMaybe)-import qualified System.IO.Error as SE-import System.Posix.Process (getProcessID)-import System.Posix.Signals (nullSignal, signalProcess)-import System.Posix.Types (ProcessID)-import Control.Exception.Extensible as E-import System.Directory ( createDirectoryIfMissing, removeFile)-import System.FilePath--newtype PrefixLock = PrefixLock FilePath- data FHandle = FHandle Fd -- should handle opening flags correctly open :: FilePath -> IO FHandle+#if !MIN_VERSION_unix(2,8,0) open filename = fmap FHandle $ openFd filename WriteOnly (Just stdFileMode) defaultFileFlags+#else+open filename = fmap FHandle $ openFd filename WriteOnly defaultFileFlags{ creat = Just stdFileMode }+#endif write :: FHandle -> Ptr Word8 -> Word32 -> IO Word32 write (FHandle fd) data' length = fmap fromIntegral $ fdWriteBuf fd data' $ fromIntegral length@@ -43,111 +37,3 @@ close :: FHandle -> IO () close (FHandle fd) = closeFd fd---- Unix needs to use a special open call to open files for exclusive writing---openExclusively :: FilePath -> IO Handle---openExclusively fp =--- fdToHandle =<< openFd fp ReadWrite (Just 0o600) flags--- where flags = defaultFileFlags {exclusive = True, trunc = True}-----obtainPrefixLock :: FilePath -> IO PrefixLock-obtainPrefixLock prefix = do- checkLock fp >> takeLock fp- where fp = prefix ++ ".lock"---- |Read the lock and break it if the process is dead.-checkLock :: FilePath -> IO ()-checkLock fp = readLock fp >>= maybeBreakLock fp---- |Read the lock and return the process id if possible.-readLock :: FilePath -> IO (Maybe ProcessID)-readLock fp = try (readFile fp) >>=- return . either (checkReadFileError fp) (fmap (fromInteger . read) . listToMaybe . lines)---- |Is this a permission error? If so we don't have permission to--- remove the lock file, abort.-checkReadFileError :: [Char] -> IOError -> Maybe ProcessID-checkReadFileError fp e | SE.isPermissionError e = throw (userError ("Could not read lock file: " ++ show fp))- | SE.isDoesNotExistError e = Nothing- | True = throw e--maybeBreakLock :: FilePath -> Maybe ProcessID -> IO ()-maybeBreakLock fp Nothing =- -- The lock file exists, but there's no PID in it. At this point,- -- we will break the lock, because the other process either died- -- or will give up when it failed to read its pid back from this- -- file.- breakLock fp-maybeBreakLock fp (Just pid) = do- -- The lock file exists and there is a PID in it. We can break the- -- lock if that process has died.- -- getProcessStatus only works on the children of the calling process.- -- exists <- try (getProcessStatus False True pid) >>= either checkException (return . isJust)- exists <- doesProcessExist pid- case exists of- True -> throw (lockedBy fp pid)- False -> breakLock fp--doesProcessExist :: ProcessID -> IO Bool-doesProcessExist pid =- -- Implementation 1- -- doesDirectoryExist ("/proc/" ++ show pid)- -- Implementation 2- try (signalProcess nullSignal pid) >>= return . either checkException (const True)- where checkException e | SE.isDoesNotExistError e = False- | True = throw e---- |We have determined the locking process is gone, try to remove the--- lock.-breakLock :: FilePath -> IO ()-breakLock fp = try (removeFile fp) >>= either checkBreakError (const (return ()))---- |An exception when we tried to break a lock, if it says the lock--- file has already disappeared we are still good to go.-checkBreakError :: IOError -> IO ()-checkBreakError e | SE.isDoesNotExistError e = return ()- | True = throw e---- |Try to create lock by opening the file with the O_EXCL flag and--- writing our PID into it. Verify by reading the pid back out and--- matching, maybe some other process slipped in before we were done--- and broke our lock.-takeLock :: FilePath -> IO PrefixLock-takeLock fp = do- createDirectoryIfMissing True (takeDirectory fp)- h <- openFd fp ReadWrite (Just 0o600) (defaultFileFlags {exclusive = True, trunc = True}) >>= fdToHandle- pid <- getProcessID- hPutStrLn h (show pid) >> hClose h- -- Read back our own lock and make sure its still ours- readLock fp >>= maybe (throw (cantLock fp pid))- (\ pid' -> if pid /= pid'- then throw (stolenLock fp pid pid')- else return (PrefixLock fp))---- |An exception saying the data is locked by another process.-lockedBy :: (Show a) => FilePath -> a -> SomeException-lockedBy fp pid = SomeException (SE.mkIOError SE.alreadyInUseErrorType ("Locked by " ++ show pid) Nothing (Just fp))---- |An exception saying we don't have permission to create lock.-cantLock :: FilePath -> ProcessID -> SomeException-cantLock fp pid = SomeException (SE.mkIOError SE.alreadyInUseErrorType ("Process " ++ show pid ++ " could not create a lock") Nothing (Just fp))---- |An exception saying another process broke our lock before we--- finished creating it.-stolenLock :: FilePath -> ProcessID -> ProcessID -> SomeException-stolenLock fp pid pid' = SomeException (SE.mkIOError SE.alreadyInUseErrorType ("Process " ++ show pid ++ "'s lock was stolen by process " ++ show pid') Nothing (Just fp))---- |Relinquish the lock by removing it and then verifying the removal.-releasePrefixLock :: PrefixLock -> IO ()-releasePrefixLock (PrefixLock fp) =- dropLock >>= either checkDrop return- where- dropLock = try (removeFile fp)- checkDrop e | SE.isDoesNotExistError e = return ()- | True = throw e---
@@ -1,4 +1,4 @@-module FileIO(FHandle,open,write,flush,close,obtainPrefixLock,releasePrefixLock,PrefixLock) where+module FileIO(FHandle,open,write,flush,close) where import System.Win32(HANDLE, createFile, gENERIC_WRITE,@@ -11,21 +11,9 @@ import Data.Word(Word8,Word32) import Foreign(Ptr) import System.IO-import System.Directory(createDirectoryIfMissing,removeFile)-import Control.Exception.Extensible(try,throw)-import Control.Exception(SomeException,IOException)-import qualified Control.Exception as E -type PrefixLock = (FilePath, Handle)- data FHandle = FHandle HANDLE -tryE :: IO a -> IO (Either SomeException a)-tryE = try--catchIO :: IO a -> (IOException -> IO a) -> IO a-catchIO = E.catch- open :: FilePath -> IO FHandle open filename = fmap FHandle $ createFile filename gENERIC_WRITE fILE_SHARE_NONE Nothing cREATE_ALWAYS fILE_ATTRIBUTE_NORMAL Nothing@@ -38,27 +26,3 @@ close :: FHandle -> IO () close (FHandle handle) = closeHandle handle---- Windows opens files for exclusive writing by default-openExclusively :: FilePath -> IO Handle-openExclusively fp = openFile fp ReadWriteMode--obtainPrefixLock :: FilePath -> IO PrefixLock-obtainPrefixLock prefix = do- createDirectoryIfMissing True prefix- -- catchIO obtainLock onError- catchIO obtainLock onError- where fp = prefix ++ ".lock"- obtainLock = do- h <- openExclusively fp- return (fp, h)- onError e = do- putStrLn "There may already be an instance of this application running, which could result in a loss of data."- putStrLn ("Please make sure there is no other application attempting to access '" ++ prefix ++ "'")- throw e--releasePrefixLock :: PrefixLock -> IO ()-releasePrefixLock (fp, h) = do- tryE $ hClose h- tryE $ removeFile fp- return ()
@@ -9,7 +9,7 @@ AcidState container using a transaction log on disk. To see how it all fits together, have a look at these example- <http://mirror.seize.it/acid-state/examples/>.+ <https://github.com/acid-state/acid-state/tree/master/examples>. -}
@@ -18,38 +18,29 @@ import Data.ByteString.Lazy ( ByteString ) import Control.Monad ( void ) import Control.Monad.Trans ( MonadIO(liftIO) )-#if __GLASGOW_HASKELL__ >= 707 import Data.Typeable ( Typeable, gcast, typeOf )-#else-import Data.Typeable ( Typeable1, gcast1, typeOf1 )-#endif data AnyState st where-#if __GLASGOW_HASKELL__ >= 707 AnyState :: Typeable sub_st => sub_st st -> AnyState st-#else- AnyState :: Typeable1 sub_st => sub_st st -> AnyState st-#endif -- Haddock doesn't get the types right on its own. {-| State container offering full ACID (Atomicity, Consistency, Isolation and Durability) guarantees. - [@Atomicity@] State changes are all-or-nothing. This is what you'd expect of any state- variable in Haskell and AcidState doesn't change that.-+ [@Atomicity@] State changes are all-or-nothing. This is what you'd expect+ of any state variable in Haskell and AcidState doesn't+ change that. [@Consistency@] No event or set of events will break your data invariants.-- [@Isolation@] Transactions cannot interfere with each other even when issued in parallel.-- [@Durability@] Successful transaction are guaranteed to survive unexpected system shutdowns- (both those caused by hardware and software).+ [@Isolation@] Transactions cannot interfere with each other even when+ issued in parallel.+ [@Durability@] Successful transaction are guaranteed to survive unexpected+ system shutdowns (both those caused by hardware and software). -} data AcidState st = AcidState { _scheduleUpdate :: forall event. (UpdateEvent event, EventState event ~ st) => event -> IO (MVar (EventResult event)) , scheduleColdUpdate :: Tagged ByteString -> IO (MVar ByteString)- , _query :: (QueryEvent event, EventState event ~ st) => event -> IO (EventResult event)+ , _query :: forall event. (QueryEvent event, EventState event ~ st) => event -> IO (EventResult event) , queryCold :: Tagged ByteString -> IO ByteString , -- | Take a snapshot of the state and save it to disk. Creating checkpoints@@ -84,7 +75,7 @@ -- scheduleUpdate acid EventB -- @ scheduleUpdate :: UpdateEvent event => AcidState (EventState event) -> event -> IO (MVar (EventResult event))-scheduleUpdate = _scheduleUpdate -- Redirection to make Haddock happy.+scheduleUpdate acid = _scheduleUpdate acid -- Redirection to make Haddock happy. -- | Schedule multiple Update events and wait for them to be durable, but -- throw away their results. This is useful for importing existing@@ -111,20 +102,15 @@ -- | Issue a Query event and wait for its result. Events may be issued in parallel. query :: QueryEvent event => AcidState (EventState event) -> event -> IO (EventResult event)-query = _query -- Redirection to make Haddock happy.+query acid = _query acid -- Redirection to make Haddock happy. -- | Same as 'query' but lifted into any monad capable of doing IO. query' :: (QueryEvent event, MonadIO m) => AcidState (EventState event) -> event -> m (EventResult event) query' acidState event = liftIO (query acidState event) -#if __GLASGOW_HASKELL__ >= 707 mkAnyState :: Typeable sub_st => sub_st st -> AnyState st-#else-mkAnyState :: Typeable1 sub_st => sub_st st -> AnyState st-#endif mkAnyState = AnyState -#if __GLASGOW_HASKELL__ >= 707 downcast :: (Typeable sub, Typeable st) => AcidState st -> sub st downcast AcidState{acidSubState = AnyState sub} = r@@ -133,15 +119,4 @@ Just (Just x) -> x _ -> error $- "Data.Acid: Invalid subtype cast: " ++ show (typeOf sub) ++ " -> " ++ show (typeOf r)-#else-downcast :: Typeable1 sub => AcidState st -> sub st-downcast AcidState{acidSubState = AnyState sub}- = r- where- r = case gcast1 (Just sub) of- Just (Just x) -> x- _ ->- error $- "Data.Acid: Invalid subtype cast: " ++ show (typeOf1 sub) ++ " -> " ++ show (typeOf1 r)-#endif+ "Data.Acid.Abstract: Invalid subtype cast: " ++ show (typeOf sub) ++ " -> " ++ show (typeOf r)
@@ -17,9 +17,14 @@ , Method(..) , IsAcidic(..) , Event(..)++ , safeCopySerialiser+ , safeCopyMethodSerialiser+ , defaultArchiver ) where import Data.Acid.Abstract+import Data.Acid.Archive import Data.Acid.Core import Data.Acid.Common
@@ -12,38 +12,86 @@ , readEntries , entriesToList , entriesToListNoFail+ , Archiver(..)+ , defaultArchiver ) where import Data.Acid.CRC import qualified Data.ByteString as Strict import qualified Data.ByteString.Lazy as Lazy+import Data.ByteString.Builder import Data.Monoid-import Data.Serialize.Builder import Data.Serialize.Get hiding (Result (..)) import qualified Data.Serialize.Get as Serialize +-- | A bytestring that represents an entry in an archive. type Entry = Lazy.ByteString++-- | Result of unpacking an archive. This is essentially a list of+-- 'Entry', but may terminate in 'Fail' if the archive format is+-- incorrect. data Entries = Done | Next Entry Entries | Fail String deriving (Show) +-- | Convert 'Entries' to a normal list, calling 'error' if there was+-- a failure in unpacking the archive. entriesToList :: Entries -> [Entry] entriesToList Done = [] entriesToList (Next entry next) = entry : entriesToList next-entriesToList (Fail msg) = error msg+entriesToList (Fail msg) = error $ "Data.Acid.Archive: " <> msg +-- | Convert 'Entries' to a normal list, silently ignoring a failure+-- to unpack the archive and instead returning a truncated list. entriesToListNoFail :: Entries -> [Entry] entriesToListNoFail Done = [] entriesToListNoFail (Next entry next) = entry : entriesToListNoFail next entriesToListNoFail Fail{} = [] ++-- | Interface for the lowest level of the serialisation layer, which+-- handles packing lists of 'Entry' elements (essentially just+-- bytestrings) into a single bytestring, perhaps with error-checking.+--+-- Any @'Archiver'{'archiveWrite', 'archiveRead'}@ must satisfy the+-- round-trip property:+--+-- > forall xs . entriesToList (archiveRead (archiveWrite xs)) == xs+--+-- Moreover, 'archiveWrite' must be a monoid homomorphism, so that+-- concatenating archives is equivalent to concatenating the lists of+-- entries that they represent:+--+-- > archiveWrite [] == empty+-- > forall xs ys . archiveWrite xs <> archiveWrite ys == archiveWrite (xs ++ ys)+data Archiver+ = Archiver+ { archiveWrite :: [Entry] -> Lazy.ByteString+ -- ^ Pack a list of entries into a bytestring.++ , archiveRead :: Lazy.ByteString -> Entries+ -- ^ Unpack a bytestring as a list of 'Entries', including the+ -- possibility of failure if the format is invalid.+ }++-- | Standard (and historically the only) implementation of the+-- 'Archiver' interface. This represents each entry in the following+-- format:+--+-- > | entry length | crc16 | entry |+-- > | 8 bytes | 2 bytes | n bytes |+defaultArchiver :: Archiver+defaultArchiver = Archiver packEntries readEntries++ putEntry :: Entry -> Builder putEntry content- = putWord64le contentLength `mappend`- putWord16le contentHash `mappend`- fromLazyByteString content+ = word64LE contentLength !<>+ word16LE contentHash !<>+ lazyByteString content where contentLength = fromIntegral $ Lazy.length content contentHash = crc16 content+ a !<> b = let c = a <> b in c `seq` c putEntries :: [Entry] -> Builder putEntries = mconcat . map putEntry
@@ -1,3 +1,4 @@+{-# LANGUAGE CPP #-} {-# LANGUAGE GeneralizedNewtypeDeriving, GADTs #-} ----------------------------------------------------------------------------- -- |@@ -13,27 +14,15 @@ import Data.Acid.Core -import Control.Monad.State-import Control.Monad.Reader-import Data.ByteString.Lazy ( ByteString )-import Data.SafeCopy-import Data.Serialize ( Get, runGet, runGetLazy )-import Control.Applicative-import qualified Data.ByteString as Strict---- Silly fix for bug in cereal-0.3.3.0's version of runGetLazy.-runGetLazyFix :: Get a- -> ByteString- -> Either String a-runGetLazyFix getter inp- = case runGet getter Strict.empty of- Left _msg -> runGetLazy getter inp- Right val -> Right val+import Control.Monad+import Control.Monad.State (MonadState, get, State)+import Control.Monad.Reader (MonadReader, Reader, runReader) -class (SafeCopy st) => IsAcidic st where+class IsAcidic st where acidEvents :: [Event st] -- ^ List of events capable of updating or querying the state. + -- | Context monad for Update events. newtype Update st a = Update { unUpdate :: State st a } deriving (Monad, Functor, MonadState st)@@ -72,9 +61,10 @@ -- QueryEvents are executed in a MonadReader context and obviously do not have -- to be serialized to disk. data Event st where- UpdateEvent :: UpdateEvent ev => (ev -> Update (EventState ev) (EventResult ev)) -> Event (EventState ev)- QueryEvent :: QueryEvent ev => (ev -> Query (EventState ev) (EventResult ev)) -> Event (EventState ev)+ UpdateEvent :: UpdateEvent ev => (ev -> Update (EventState ev) (EventResult ev)) -> MethodSerialiser ev -> Event (EventState ev)+ QueryEvent :: QueryEvent ev => (ev -> Query (EventState ev) (EventResult ev)) -> MethodSerialiser ev -> Event (EventState ev) + -- | All UpdateEvents are also Methods. class Method ev => UpdateEvent ev -- | All QueryEvents are also Methods.@@ -84,8 +74,7 @@ eventsToMethods :: [Event st] -> [MethodContainer st] eventsToMethods = map worker where worker :: Event st -> MethodContainer st- worker (UpdateEvent fn) = Method (unUpdate . fn)- worker (QueryEvent fn) = Method (\ev -> do st <- get- return (runReader (unQuery $ fn ev) st)- )-+ worker (UpdateEvent fn ms) = Method (unUpdate . fn) ms+ worker (QueryEvent fn ms) = Method (\ev -> do st <- get+ return (runReader (unQuery $ fn ev) st)+ ) ms
@@ -1,5 +1,6 @@-{-# LANGUAGE CPP, GADTs, DeriveDataTypeable, TypeFamilies,- FlexibleContexts, BangPatterns #-}+{-# LANGUAGE CPP, GADTs, TypeFamilies,+ FlexibleContexts, BangPatterns,+ DefaultSignatures, ScopedTypeVariables #-} ----------------------------------------------------------------------------- -- | -- Module : Data.Acid.Core@@ -28,11 +29,21 @@ , modifyCoreState_ , withCoreState , lookupHotMethod+ , lookupHotMethodAndSerialiser , lookupColdMethod , runHotMethod , runColdMethod , MethodMap , mkMethodMap++ , Serialiser(..)+ , safeCopySerialiser+ , MethodSerialiser(..)+ , safeCopyMethodSerialiser+ , encodeMethod+ , decodeMethod+ , encodeResult+ , decodeResult ) where import Control.Concurrent ( MVar, newMVar, withMVar@@ -40,19 +51,18 @@ import Control.Monad ( liftM ) import Control.Monad.State ( State, runState ) import qualified Data.Map as Map+#if !MIN_VERSION_base(4,11,0)+import Data.Monoid ((<>))+#endif import Data.ByteString.Lazy as Lazy ( ByteString ) import Data.ByteString.Lazy.Char8 as Lazy ( pack, unpack ) import Data.Serialize ( runPutLazy, runGetLazy ) import Data.SafeCopy ( SafeCopy, safeGet, safePut ) -import Data.Typeable ( Typeable, TypeRep, typeOf )+import Data.Typeable ( Typeable, TypeRep, typeRepTyCon, typeOf, tyConModule ) import Unsafe.Coerce ( unsafeCoerce ) -#if MIN_VERSION_base(4,4,0)--import Data.Typeable.Internal ( TypeRep (..), tyConModule )- -- in base >= 4.4 the Show instance for TypeRep no longer provides a -- fully qualified name. But we have old data around that expects the -- FQN. So we will recreate the old naming system for newer versions@@ -60,30 +70,67 @@ -- end-of-life anyway. showQualifiedTypeRep :: TypeRep -> String showQualifiedTypeRep tr = tyConModule con ++ "." ++ show tr- where con = extractTypeRepCon tr-#if MIN_VERSION_base(4,8,0)- extractTypeRepCon (TypeRep _ c _ _) = c-#else- extractTypeRepCon (TypeRep _ c _) = c-#endif+ where con = typeRepTyCon tr -#else+-- | Interface for (de)serialising values of type @a@.+--+-- A @'Serialiser' { 'serialiserEncode', 'serialiserDecode' }@ must+-- satisfy the round-trip property:+--+-- > forall x . serialiserDecode (serialiserEncode x) == Right x+data Serialiser a =+ Serialiser+ { serialiserEncode :: a -> Lazy.ByteString+ -- ^ Serialise a value to a bytestring.+ , serialiserDecode :: Lazy.ByteString -> Either String a+ -- ^ Deserialise a value, generating a string error message+ -- on failure.+ } -showQualifiedTypeRep :: TypeRep -> String-showQualifiedTypeRep tr = show tr+-- | Default implementation of 'Serialiser' interface using 'SafeCopy'.+safeCopySerialiser :: SafeCopy a => Serialiser a+safeCopySerialiser = Serialiser (runPutLazy . safePut) (runGetLazy safeGet) -#endif +-- | Interface for (de)serialising a method, namely 'Serialiser's for+-- its arguments type and its result type.+data MethodSerialiser method =+ MethodSerialiser+ { methodSerialiser :: Serialiser method+ , resultSerialiser :: Serialiser (MethodResult method)+ }++-- | Default implementation of 'MethodSerialiser' interface using 'SafeCopy'.+safeCopyMethodSerialiser :: (SafeCopy method, SafeCopy (MethodResult method)) => MethodSerialiser method+safeCopyMethodSerialiser = MethodSerialiser safeCopySerialiser safeCopySerialiser++-- | Encode the arguments of a method using the given serialisation strategy.+encodeMethod :: MethodSerialiser method -> method -> ByteString+encodeMethod ms = serialiserEncode (methodSerialiser ms)++-- | Decode the arguments of a method using the given serialisation strategy.+decodeMethod :: MethodSerialiser method -> ByteString -> Either String method+decodeMethod ms = serialiserDecode (methodSerialiser ms)++-- | Encode the result of a method using the given serialisation strategy.+encodeResult :: MethodSerialiser method -> MethodResult method -> ByteString+encodeResult ms = serialiserEncode (resultSerialiser ms)++-- | Decode the result of a method using the given serialisation strategy.+decodeResult :: MethodSerialiser method -> ByteString -> Either String (MethodResult method)+decodeResult ms = serialiserDecode (resultSerialiser ms)++ -- | The basic Method class. Each Method has an indexed result type -- and a unique tag.-class ( Typeable ev, SafeCopy ev- , Typeable (MethodResult ev), SafeCopy (MethodResult ev)) =>- Method ev where+class Method ev where type MethodResult ev type MethodState ev methodTag :: ev -> Tag+ default methodTag :: Typeable ev => ev -> Tag methodTag ev = Lazy.pack (showQualifiedTypeRep (typeOf ev)) + -- | The control structure at the very center of acid-state. -- This module provides access to a mutable state through -- methods. No efforts towards durability, checkpointing or@@ -118,7 +165,7 @@ = modifyMVar_ (coreState core) $ \st -> do action st return errorMsg- where errorMsg = error "Access failure: Core closed."+ where errorMsg = error "Data.Acid.Core: Access failure: Core closed." -- | Modify the state component. The resulting state is ensured to be in -- WHNF.@@ -155,18 +202,18 @@ lookupColdMethod core (storedMethodTag, methodContent) = case Map.lookup storedMethodTag (coreMethods core) of Nothing -> missingMethod storedMethodTag- Just (Method method)- -> liftM (runPutLazy . safePut) (method (lazyDecode methodContent))+ Just (Method method ms)+ -> liftM (encodeResult ms) (method (lazyDecode ms methodContent)) -lazyDecode :: SafeCopy a => Lazy.ByteString -> a-lazyDecode inp- = case runGetLazy safeGet inp of- Left msg -> error msg+lazyDecode :: MethodSerialiser method -> Lazy.ByteString -> method+lazyDecode ms inp+ = case decodeMethod ms inp of+ Left msg -> error $ "Data.Acid.Core: " <> msg Right val -> val missingMethod :: Tag -> a missingMethod tag- = error msg+ = error $ "Data.Acid.Core: " <> msg where msg = "This method is required but not available: " ++ show (Lazy.unpack tag) ++ ". Did you perhaps remove it before creating a checkpoint?" @@ -180,21 +227,29 @@ -- | Find the state action that corresponds to an in-memory method. lookupHotMethod :: Method method => MethodMap (MethodState method) -> method -> State (MethodState method) (MethodResult method)-lookupHotMethod methodMap method+lookupHotMethod methodMap method = fst (lookupHotMethodAndSerialiser methodMap method)++-- | Find the state action and serialiser that correspond to an+-- in-memory method.+lookupHotMethodAndSerialiser :: Method method => MethodMap (MethodState method) -> method+ -> (State (MethodState method) (MethodResult method), MethodSerialiser method)+lookupHotMethodAndSerialiser methodMap method = case Map.lookup (methodTag method) methodMap of Nothing -> missingMethod (methodTag method)- Just (Method methodHandler)+ Just (Method methodHandler ms) -> -- If the methodTag doesn't index the right methodHandler then we're in deep -- trouble. Luckly, it would take deliberate malevolence for that to happen.- unsafeCoerce methodHandler method+ (unsafeCoerce methodHandler method, unsafeCoerce ms) -- | Method tags must be unique and are most commonly generated automatically. type Tag = Lazy.ByteString type Tagged a = (Tag, a) +type MethodBody method = method -> State (MethodState method) (MethodResult method)+ -- | Method container structure that hides the exact type of the method. data MethodContainer st where- Method :: (Method method) => (method -> State (MethodState method) (MethodResult method)) -> MethodContainer (MethodState method)+ Method :: (Method method) => MethodBody method -> MethodSerialiser method -> MethodContainer (MethodState method) -- | Collection of Methods indexed by a Tag. type MethodMap st = Map.Map Tag (MethodContainer st)@@ -206,8 +261,6 @@ where -- A little bit of ugliness is required to access the methodTags. methodType :: MethodContainer st -> Tag methodType m = case m of- Method fn -> let ev :: (ev -> State st res) -> ev- ev _ = undefined- in methodTag (ev fn)--+ Method fn _ -> let ev :: (ev -> State st res) -> ev+ ev _ = undefined+ in methodTag (ev fn)
@@ -1,4 +1,4 @@-{-# LANGUAGE DeriveDataTypeable, BangPatterns, CPP #-}+{-# LANGUAGE BangPatterns, CPP #-} ----------------------------------------------------------------------------- -- | -- Module : Data.Acid.Local@@ -15,33 +15,43 @@ module Data.Acid.Local ( openLocalState , openLocalStateFrom+ , openLocalStateWithSerialiser , prepareLocalState , prepareLocalStateFrom+ , prepareLocalStateWithSerialiser+ , defaultStateDirectory+ , scheduleLocalUpdate'+ , scheduleLocalColdUpdate' , createCheckpointAndClose+ , LocalState(..)+ , Checkpoint(..)+ , SerialisationLayer(..)+ , defaultSerialisationLayer+ , mkEventsLogKey+ , mkCheckpointsLogKey ) where +import Data.Acid.Archive import Data.Acid.Log as Log import Data.Acid.Core import Data.Acid.Common import Data.Acid.Abstract import Control.Concurrent ( newEmptyMVar, putMVar, takeMVar, MVar )-import Control.Exception ( onException, evaluate )+import Control.Exception ( onException, evaluate, Exception, throwIO ) import Control.Monad.State ( runState ) import Control.Monad ( join )-import Control.Applicative ( (<$>), (<*>) ) import Data.ByteString.Lazy ( ByteString ) import qualified Data.ByteString.Lazy as Lazy ( length ) - import Data.Serialize ( runPutLazy, runGetLazy ) import Data.SafeCopy ( SafeCopy(..), safeGet, safePut , primitive, contain ) import Data.Typeable ( Typeable, typeOf ) import Data.IORef-import System.FilePath ( (</>) )--import FileIO ( obtainPrefixLock, releasePrefixLock, PrefixLock )+import System.FilePath ( (</>), takeDirectory )+import System.FileLock+import System.Directory ( createDirectoryIfMissing ) {-| State container offering full ACID (Atomicity, Consistency, Isolation and Durability)@@ -61,11 +71,14 @@ = LocalState { localCore :: Core st , localCopy :: IORef st , localEvents :: FileLog (Tagged ByteString)- , localCheckpoints :: FileLog Checkpoint- , localLock :: PrefixLock- } deriving (Typeable)+ , localCheckpoints :: FileLog (Checkpoint st)+ , localLock :: FileLock+ } +newtype StateIsLocked = StateIsLocked FilePath deriving (Show) +instance Exception StateIsLocked+ -- | Issue an Update event and return immediately. The event is not durable -- before the MVar has been filled but the order of events is honored. -- The behavior in case of exceptions is exactly the same as for 'update'.@@ -79,7 +92,7 @@ scheduleLocalUpdate :: UpdateEvent event => LocalState (EventState event) -> event -> IO (MVar (EventResult event)) scheduleLocalUpdate acidState event = do mvar <- newEmptyMVar- let encoded = runPutLazy (safePut event)+ let encoded = encodeMethod ms event -- It is important that we encode the event now so that we can catch -- any exceptions (see nestedStateError in examples/errors/Exceptions.hs)@@ -93,8 +106,34 @@ putMVar mvar result return st' return mvar- where hotMethod = lookupHotMethod (coreMethods (localCore acidState)) event+ where (hotMethod, ms) = lookupHotMethodAndSerialiser (coreMethods (localCore acidState)) event +-- | Same as scheduleLocalUpdate but does not immediately change the localCopy+-- and return the result mvar - returns an IO action to do this instead. Take+-- care to run actions of multiple Updates in the correct order as otherwise+-- Queries will operate on outdated state.+scheduleLocalUpdate' :: UpdateEvent event => LocalState (EventState event) -> event -> MVar (EventResult event) -> IO (IO ())+scheduleLocalUpdate' acidState event mvar+ = do+ let encoded = encodeMethod ms event++ -- It is important that we encode the event now so that we can catch+ -- any exceptions (see nestedStateError in examples/errors/Exceptions.hs)+ evaluate (Lazy.length encoded)++ act <- modifyCoreState (localCore acidState) $ \st ->+ do let !(result, !st') = runState hotMethod st+ -- Schedule the log entry. Very important that it happens when 'localCore' is locked+ -- to ensure that events are logged in the same order that they are executed.+ pushEntry (localEvents acidState) (methodTag event, encoded) $ return ()+ let action = do writeIORef (localCopy acidState) st'+ putMVar mvar result+ return (st', action)+ -- this is the action to update state for queries and release the+ -- result into the supplied mvar+ return act+ where (hotMethod, ms) = lookupHotMethodAndSerialiser (coreMethods (localCore acidState)) event+ scheduleLocalColdUpdate :: LocalState st -> Tagged ByteString -> IO (MVar ByteString) scheduleLocalColdUpdate acidState event = do mvar <- newEmptyMVar@@ -108,6 +147,23 @@ return mvar where coldMethod = lookupColdMethod (localCore acidState) event +-- | Same as scheduleLocalColdUpdate but does not immediately change the+-- localCopy and return the result mvar - returns an IO action to do this+-- instead. Take care to run actions of multiple Updates in the correct order as+-- otherwise Queries will operate on outdated state.+scheduleLocalColdUpdate' :: LocalState st -> Tagged ByteString -> MVar ByteString -> IO (IO ())+scheduleLocalColdUpdate' acidState event mvar+ = do act <- modifyCoreState (localCore acidState) $ \st ->+ do let !(result, !st') = runState coldMethod st+ -- Schedule the log entry. Very important that it happens when 'localCore' is locked+ -- to ensure that events are logged in the same order that they are executed.+ pushEntry (localEvents acidState) event $ return ()+ let action = do writeIORef (localCopy acidState) st'+ putMVar mvar result+ return (st', action)+ return act+ where coldMethod = lookupColdMethod (localCore acidState) event+ -- | Issue a Query event and wait for its result. Events may be issued in parallel. localQuery :: QueryEvent event => LocalState (EventState event) -> event -> IO (EventResult event) localQuery acidState event@@ -130,128 +186,218 @@ -- with this call. -- -- This call will not return until the operation has succeeded.-createLocalCheckpoint :: SafeCopy st => LocalState st -> IO ()+createLocalCheckpoint :: IsAcidic st => LocalState st -> IO () createLocalCheckpoint acidState- = do mvar <- newEmptyMVar+ = do cutFileLog (localEvents acidState)+ mvar <- newEmptyMVar withCoreState (localCore acidState) $ \st -> do eventId <- askCurrentEntryId (localEvents acidState) pushAction (localEvents acidState) $- do let encoded = runPutLazy (safePut st)- pushEntry (localCheckpoints acidState) (Checkpoint eventId encoded) (putMVar mvar ())+ pushEntry (localCheckpoints acidState) (Checkpoint eventId st) (putMVar mvar ()) takeMVar mvar -- | 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.-createCheckpointAndClose :: (SafeCopy st, Typeable st) => AcidState st -> IO ()+createCheckpointAndClose :: (IsAcidic st, Typeable st) => AcidState st -> IO () createCheckpointAndClose abstract_state = do mvar <- newEmptyMVar closeCore' (localCore acidState) $ \st -> do eventId <- askCurrentEntryId (localEvents acidState) pushAction (localEvents acidState) $- pushEntry (localCheckpoints acidState) (Checkpoint eventId (runPutLazy (safePut st))) (putMVar mvar ())+ pushEntry (localCheckpoints acidState) (Checkpoint eventId st) (putMVar mvar ()) takeMVar mvar closeFileLog (localEvents acidState) closeFileLog (localCheckpoints acidState)- releasePrefixLock (localLock acidState)+ unlockFile (localLock acidState) where acidState = downcast abstract_state -data Checkpoint = Checkpoint EntryId ByteString+data Checkpoint s = Checkpoint EntryId s -instance SafeCopy Checkpoint where+-- | Previous versions of @acid-state@ had+--+-- > data Checkpoint = Checkpoint EntryId ByteString+--+-- where the 'ByteString' is the @safecopy@-serialization of the+-- original checkpoint data. Thus we give a 'SafeCopy' instance that+-- is backwards-compatible with this by making nested calls to+-- 'safePut' and 'safeGet'.+--+-- Note that if the inner data cannot be deserialised, 'getCopy' will+-- not report an error immediately but will return a 'Checkpoint'+-- whose payload is an error thunk. This means consumers can skip+-- deserialising intermediate checkpoint data when they care only+-- about the last checkpoint in a file. However, they must be sure to+-- force the returned data promptly.+instance SafeCopy s => SafeCopy (Checkpoint s) where kind = primitive putCopy (Checkpoint eventEntryId content) = contain $ do safePut eventEntryId- safePut content- getCopy = contain $ Checkpoint <$> safeGet <*> safeGet+ safePut (runPutLazy (safePut content))+ getCopy = contain $ Checkpoint <$> safeGet <*> (fromNested <$> safeGet)+ where+ fromNested b = case runGetLazy safeGet b of+ Left msg -> checkpointRestoreError msg+ Right v -> v+ errorTypeName s = "Checkpoint " ++ errorTypeName s -- | Create an AcidState given an initial value. -- -- This will create or resume a log found in the \"state\/[typeOf state]\/\" directory.-openLocalState :: (Typeable st, IsAcidic st)+openLocalState :: (Typeable st, IsAcidic st, SafeCopy st) => st -- ^ Initial state value. This value is only used if no checkpoint is -- found. -> IO (AcidState st) openLocalState initialState =- openLocalStateFrom ("state" </> show (typeOf initialState)) initialState+ openLocalStateFrom (defaultStateDirectory initialState) initialState -- | Create an AcidState given an initial value. -- -- This will create or resume a log found in the \"state\/[typeOf state]\/\" directory. -- The most recent checkpoint will be loaded immediately but the AcidState will not be opened -- until the returned function is executed.-prepareLocalState :: (Typeable st, IsAcidic st)+prepareLocalState :: (Typeable st, IsAcidic st, SafeCopy st) => st -- ^ Initial state value. This value is only used if no checkpoint is -- found. -> IO (IO (AcidState st)) prepareLocalState initialState =- prepareLocalStateFrom ("state" </> show (typeOf initialState)) initialState+ prepareLocalStateFrom (defaultStateDirectory initialState) initialState +-- | Directory to load the state from unless otherwise specified,+-- namely \"state\/[typeOf state]\/\".+defaultStateDirectory :: Typeable st => st -> FilePath+defaultStateDirectory initialState = "state" </> show (typeOf initialState) -- | Create an AcidState given a log directory and an initial value. -- -- This will create or resume a log found in @directory@. -- Running two AcidState's from the same directory is an error -- but will not result in dataloss.-openLocalStateFrom :: (IsAcidic st)+openLocalStateFrom :: (IsAcidic st, SafeCopy st) => FilePath -- ^ Location of the checkpoint and transaction files. -> st -- ^ Initial state value. This value is only used if no checkpoint is -- found. -> IO (AcidState st) openLocalStateFrom directory initialState =- join $ resumeLocalStateFrom directory initialState False+ openLocalStateWithSerialiser directory initialState defaultSerialisationLayer --- | Create an AcidState given an initial value.+-- | Create an AcidState given a log directory, an initial value and a serialisation layer. -- -- This will create or resume a log found in @directory@.+-- Running two AcidState's from the same directory is an error+-- but will not result in dataloss.+openLocalStateWithSerialiser :: (IsAcidic st)+ => FilePath -- ^ Location of the checkpoint and transaction files.+ -> st -- ^ Initial state value. This value is only used if no checkpoint is+ -- found.+ -> SerialisationLayer st -- ^ Serialisation layer to use for checkpoints, events and archives.+ -> IO (AcidState st)+openLocalStateWithSerialiser directory initialState serialisationLayer =+ join $ resumeLocalStateFrom directory initialState False serialisationLayer++-- | Create an AcidState given a log directory and an initial value.+--+-- This will create or resume a log found in @directory@. -- The most recent checkpoint will be loaded immediately but the AcidState will not be opened -- until the returned function is executed.-prepareLocalStateFrom :: (IsAcidic st)+prepareLocalStateFrom :: (IsAcidic st, SafeCopy st) => FilePath -- ^ Location of the checkpoint and transaction files. -> st -- ^ Initial state value. This value is only used if no checkpoint is -- found. -> IO (IO (AcidState st)) prepareLocalStateFrom directory initialState =- resumeLocalStateFrom directory initialState True+ prepareLocalStateWithSerialiser directory initialState defaultSerialisationLayer +-- | Create an AcidState given a log directory, an initial value and a serialisation layer.+--+-- This will create or resume a log found in @directory@.+-- The most recent checkpoint will be loaded immediately but the AcidState will not be opened+-- until the returned function is executed.+prepareLocalStateWithSerialiser :: (IsAcidic st)+ => FilePath -- ^ Location of the checkpoint and transaction files.+ -> st -- ^ Initial state value. This value is only used if no checkpoint is+ -- found.+ -> SerialisationLayer st -- ^ Serialisation layer to use for checkpoints, events and archives.+ -> IO (IO (AcidState st))+prepareLocalStateWithSerialiser directory initialState serialisationLayer =+ resumeLocalStateFrom directory initialState True serialisationLayer +data SerialisationLayer st =+ SerialisationLayer+ { checkpointSerialiser :: Serialiser (Checkpoint st)+ -- ^ Serialisation strategy for checkpoints.+ --+ -- Use 'safeCopySerialiser' for the backwards-compatible+ -- implementation using "Data.SafeCopy".++ , eventSerialiser :: Serialiser (Tagged ByteString)+ -- ^ Serialisation strategy for events.+ --+ -- Use 'safeCopySerialiser' for the backwards-compatible+ -- implementation using "Data.SafeCopy".++ , archiver :: Archiver+ -- ^ Serialisation strategy for archive log files.+ --+ -- Use 'defaultArchiver' for the backwards-compatible+ -- implementation using "Data.Serialize".+ }++-- | Standard (and historically the only) serialisation layer, using+-- 'safeCopySerialiser' and 'defaultArchiver'.+defaultSerialisationLayer :: SafeCopy st => SerialisationLayer st+defaultSerialisationLayer = SerialisationLayer safeCopySerialiser safeCopySerialiser defaultArchiver++mkEventsLogKey :: FilePath -> SerialisationLayer object -> LogKey (Tagged ByteString)+mkEventsLogKey directory serialisationLayer =+ LogKey { logDirectory = directory+ , logPrefix = "events"+ , logSerialiser = eventSerialiser serialisationLayer+ , logArchiver = archiver serialisationLayer }++mkCheckpointsLogKey :: FilePath -> SerialisationLayer object -> LogKey (Checkpoint object)+mkCheckpointsLogKey directory serialisationLayer =+ LogKey { logDirectory = directory+ , logPrefix = "checkpoints"+ , logSerialiser = checkpointSerialiser serialisationLayer+ , logArchiver = archiver serialisationLayer }+ resumeLocalStateFrom :: (IsAcidic st) => FilePath -- ^ Location of the checkpoint and transaction files. -> st -- ^ Initial state value. This value is only used if no checkpoint is -- found. -> Bool -- ^ True => load checkpoint before acquiring the lock.+ -> SerialisationLayer st -- ^ Serialisation layer to use for checkpoints, events and archives. -> IO (IO (AcidState st))-resumeLocalStateFrom directory initialState delayLocking =+resumeLocalStateFrom directory initialState delayLocking serialisationLayer = case delayLocking of True -> do (n, st) <- loadCheckpoint return $ do- lock <- obtainPrefixLock lockFile+ lock <- maybeLockFile lockFile replayEvents lock n st False -> do- lock <- obtainPrefixLock lockFile- (n, st) <- loadCheckpoint `onException` releasePrefixLock lock+ lock <- maybeLockFile lockFile+ (n, st) <- loadCheckpoint `onException` unlockFile lock return $ do replayEvents lock n st where- lockFile = directory </> "open"- eventsLogKey = LogKey { logDirectory = directory- , logPrefix = "events" }- checkpointsLogKey = LogKey { logDirectory = directory- , logPrefix = "checkpoints" }+ lockFile = directory </> "open.lock"+ eventsLogKey = mkEventsLogKey directory serialisationLayer+ checkpointsLogKey = mkCheckpointsLogKey directory serialisationLayer loadCheckpoint = do mbLastCheckpoint <- Log.newestEntry checkpointsLogKey case mbLastCheckpoint of Nothing -> return (0, initialState)- Just (Checkpoint eventCutOff content) -> do- case runGetLazy safeGet content of- Left msg -> checkpointRestoreError msg- Right val -> return (eventCutOff, val)+ Just (Checkpoint eventCutOff !val) ->+ -- N.B. We must be strict in val so that we force any+ -- lurking deserialisation error immediately.+ return (eventCutOff, val) replayEvents lock n st = do core <- mkCore (eventsToMethods acidEvents) st @@ -269,10 +415,16 @@ , localCheckpoints = checkpointsLog , localLock = lock }+ maybeLockFile path = do+ createDirectoryIfMissing True (takeDirectory path)+ maybe (throwIO (StateIsLocked path))+ return =<< tryLockFile path Exclusive + checkpointRestoreError msg = error $ "Could not parse saved checkpoint due to the following error: " ++ msg + -- | Close an AcidState and associated logs. -- Any subsequent usage of the AcidState will throw an exception. closeLocalState :: LocalState st -> IO ()@@ -280,7 +432,7 @@ = do closeCore (localCore acidState) closeFileLog (localEvents acidState) closeFileLog (localCheckpoints acidState)- releasePrefixLock (localLock acidState)+ unlockFile (localLock acidState) createLocalArchive :: LocalState st -> IO () createLocalArchive state@@ -313,4 +465,3 @@ , closeAcidState = closeLocalState local , acidSubState = mkAnyState local }-
@@ -1,9 +1,10 @@--- A log is a stack of entries that supports efficient pushing of--- new entries and fetching of old. It can be considered an--- extendible array of entries.+{-# LANGUAGE CPP #-}++-- | A log is a stack of entries that supports efficient pushing of new entries+-- and fetching of old. It can be considered an extendible array of entries. -- module Data.Acid.Log- ( FileLog+ ( FileLog(..) , LogKey(..) , EntryId , openFileLog@@ -18,9 +19,11 @@ , askCurrentEntryId , cutFileLog , archiveFileLog+ , findLogFiles ) where -import Data.Acid.Archive as Archive+import Data.Acid.Archive (Archiver(..), Entries(..), entriesToList)+import Data.Acid.Core import System.Directory import System.FilePath import System.IO@@ -33,16 +36,16 @@ import qualified Data.ByteString.Lazy as Lazy import qualified Data.ByteString as Strict import qualified Data.ByteString.Unsafe as Strict-import Data.List+import Data.List ( (\\), stripPrefix, sort ) import Data.Maybe-import qualified Data.Serialize.Get as Get-import qualified Data.Serialize.Put as Put-import Data.SafeCopy ( safePut, safeGet, SafeCopy )-+#if !MIN_VERSION_base(4,11,0)+import Data.Monoid ( (<>) )+#endif import Text.Printf ( printf ) import Paths_acid_state ( version ) import Data.Version ( showVersion )+import Control.Exception ( handle, IOException ) type EntryId = Int @@ -58,6 +61,8 @@ = LogKey { logDirectory :: FilePath , logPrefix :: String+ , logSerialiser :: Serialiser object+ , logArchiver :: Archiver } formatLogFile :: String -> EntryId -> String@@ -87,7 +92,8 @@ currentState <- newEmptyMVar queue <- newTVarIO ([], []) nextEntryRef <- newTVarIO 0- tid2 <- forkIO $ fileWriter currentState queue+ tid1 <- myThreadId+ tid2 <- forkIO $ fileWriter (logArchiver identifier) currentState queue tid1 let fLog = FileLog { logIdentifier = identifier , logCurrent = currentState , logNextEntryId = nextEntryRef@@ -98,27 +104,29 @@ handle <- open (logDirectory identifier </> formatLogFile (logPrefix identifier) currentEntryId) putMVar currentState handle else do let (lastFileEntryId, lastFilePath) = maximum logFiles- entries <- readEntities lastFilePath+ entries <- readEntities (logArchiver identifier) lastFilePath let currentEntryId = lastFileEntryId + length entries atomically $ writeTVar nextEntryRef currentEntryId handle <- open (logDirectory identifier </> formatLogFile (logPrefix identifier) currentEntryId) putMVar currentState handle return fLog -fileWriter :: MVar FHandle -> TVar ([Lazy.ByteString], [IO ()]) -> IO ()-fileWriter currentState queue = forever $ do+fileWriter :: Archiver -> MVar FHandle -> TVar ([Lazy.ByteString], [IO ()]) -> ThreadId -> IO ()+fileWriter archiver currentState queue parentTid = forever $ do (entries, actions) <- atomically $ do (entries, actions) <- readTVar queue when (null entries && null actions) retry writeTVar queue ([], []) return (reverse entries, reverse actions)- withMVar currentState $ \fd -> do- let arch = Archive.packEntries entries- writeToDisk fd (repack arch)+ handle (\e -> throwTo parentTid (e :: IOException)) $+ withMVar currentState $ \fd -> do+ let arch = archiveWrite archiver entries+ writeToDisk fd (repack arch) sequence_ actions yield --- Repack a lazy bytestring into larger blocks that can be efficiently written to disk.+-- | Repack a lazy bytestring into larger blocks that can be efficiently written+-- to disk. repack :: Lazy.ByteString -> [Strict.ByteString] repack = worker where@@ -145,16 +153,12 @@ modifyMVar_ (logCurrent fLog) $ \handle -> do close handle _ <- forkIO $ forM_ (logThreads fLog) killThread- return $ error "FileLog has been closed"+ return $ error "Data.Acid.Log: FileLog has been closed" -readEntities :: FilePath -> IO [Lazy.ByteString]-readEntities path = do+readEntities :: Archiver -> FilePath -> IO [Lazy.ByteString]+readEntities archiver path = do archive <- Lazy.readFile path- return $ worker (Archive.readEntries archive)- where- worker Done = []- worker (Next entry next) = entry : worker next- worker (Fail msg) = error msg+ return $ entriesToList (archiveRead archiver archive) ensureLeastEntryId :: FileLog object -> EntryId -> IO () ensureLeastEntryId fLog youngestEntry = do@@ -164,10 +168,10 @@ cutFileLog fLog return () --- Read all durable entries younger than the given EntryId.--- Note that entries written during or after this call won't--- be included in the returned list.-readEntriesFrom :: SafeCopy object => FileLog object -> EntryId -> IO [object]+-- | Read all durable entries younger than the given 'EntryId'. Note that+-- entries written during or after this call won't be included in the returned+-- list.+readEntriesFrom :: FileLog object -> EntryId -> IO [object] readEntriesFrom fLog youngestEntry = do -- Cut the log so we can read written entries without interfering -- with the writing of new entries.@@ -183,15 +187,17 @@ -- cereal-0.3.5.2 and binary-0.7.1.0. The code should revert back -- to lazy bytestrings once the bug has been fixed. archive <- liftM Lazy.fromChunks $ mapM (Strict.readFile . snd) relevant- let entries = entriesToList $ readEntries archive- return $ map decode'+ let entries = entriesToList $ archiveRead (logArchiver identifier) archive+ return $ map (decode' identifier) $ take (entryCap - youngestEntry) -- Take events under the eventCap. $ drop (youngestEntry - firstEntryId) entries -- Drop entries that are too young. where rangeStart (firstEntryId, _path) = firstEntryId+ identifier = logIdentifier fLog --- Obliterate log entries younger than or equal to the EventId. Very unsafe, can't be undone-rollbackTo :: SafeCopy object => LogKey object -> EntryId -> IO ()+-- | Obliterate log entries younger than or equal to the 'EntryId'. Very unsafe,+-- can't be undone+rollbackTo :: LogKey object -> EntryId -> IO () rollbackTo identifier youngestEntry = do logFiles <- findLogFiles identifier let sorted = sort logFiles@@ -201,24 +207,26 @@ | otherwise = do archive <- Strict.readFile path pathHandle <- openFile path WriteMode- let entries = entriesToList $ readEntries (Lazy.fromChunks [archive])+ let entries = entriesToList $ archiveRead (logArchiver identifier) (Lazy.fromChunks [archive]) entriesToKeep = take (youngestEntry - rangeStart + 1) entries- lengthToKeep = Lazy.length (packEntries entriesToKeep)+ lengthToKeep = Lazy.length (archiveWrite (logArchiver identifier) entriesToKeep) hSetFileSize pathHandle (fromIntegral lengthToKeep) hClose pathHandle loop (reverse sorted) --- Obliterate log entries as long as the filterFn returns True.-rollbackWhile :: SafeCopy object => LogKey object -> (object -> Bool) -> IO ()+-- | Obliterate log entries as long as the filter function returns @True@.+rollbackWhile :: LogKey object+ -> (object -> Bool) -- ^ the filter function+ -> IO () rollbackWhile identifier filterFn = do logFiles <- findLogFiles identifier let sorted = sort logFiles loop [] = return () loop ((_rangeStart, path) : xs) = do archive <- Strict.readFile path- let entries = entriesToList $ readEntries (Lazy.fromChunks [archive])- entriesToSkip = takeWhile (filterFn . decode') $ reverse entries- skip_size = Lazy.length (packEntries entriesToSkip)+ let entries = entriesToList $ archiveRead (logArchiver identifier) (Lazy.fromChunks [archive])+ entriesToSkip = takeWhile (filterFn . decode' identifier) $ reverse entries+ skip_size = Lazy.length (archiveWrite (logArchiver identifier) entriesToSkip) orig_size = fromIntegral $ Strict.length archive new_size = orig_size - skip_size if new_size == 0@@ -228,9 +236,15 @@ hClose pathHandle loop (reverse sorted) --- Filter out log files that are outside the min_entry/max_entry range.--- minEntryId <= x < maxEntryId-filterLogFiles :: Maybe EntryId -> Maybe EntryId -> [(EntryId, FilePath)] -> [(EntryId, FilePath)]+-- | Filter out log files that are outside the min_entry/max_entry range.+--+-- minEntryId <= x < maxEntryId+filterLogFiles+ :: Maybe EntryId+ -- ^ minEntryId+ -> Maybe EntryId+ -- ^ maxEntryId+ -> [(EntryId, FilePath)] -> [(EntryId, FilePath)] filterLogFiles minEntryIdMb maxEntryIdMb logFiles = worker logFiles where worker [] = []@@ -252,14 +266,14 @@ Just maxEntryId -> (< maxEntryId) rangeStart (firstEntryId, _path) = firstEntryId --- Move all log files that do not contain entries equal or higher than the given entryId--- into an Archive/ directory.+-- | Move all log files that do not contain entries equal or higher than the+-- given entryId into an @Archive/@ directory. archiveFileLog :: FileLog object -> EntryId -> IO () archiveFileLog fLog entryId = do logFiles <- findLogFiles (logIdentifier fLog) let sorted = sort logFiles- relevant = filterLogFiles Nothing (Just entryId) sorted \\- filterLogFiles (Just (entryId+1)) (Just entryId) sorted+ relevant = filterLogFiles Nothing (Just entryId) sorted+ \\ filterLogFiles (Just entryId) (Just (entryId+1)) sorted createDirectoryIfMissing True archiveDir forM_ relevant $ \(_startEntry, logFilePath) ->@@ -286,12 +300,15 @@ where key = logIdentifier fLog --- Finds the newest entry in the log. Doesn't work on open logs.--- Do not use after the log has been opened.--- Implementation: Search the newest log files first. Once a file--- containing at least one valid entry is found,--- return the last entry in that file.-newestEntry :: SafeCopy object => LogKey object -> IO (Maybe object)+-- | Finds the newest entry in the log. Doesn't work on open logs. Do not use+-- after the log has been opened.+--+-- Implementation:+--+-- - Search the newest log files first.+-- - Once a file containing at least one valid entry is found, return the last+-- entry in that file.+newestEntry :: LogKey object -> IO (Maybe object) newestEntry identifier = do logFiles <- findLogFiles identifier let sorted = reverse $ sort logFiles@@ -304,27 +321,28 @@ -- cereal-0.3.5.2 and binary-0.7.1.0. The code should revert back -- to lazy bytestrings once the bug has been fixed. archive <- fmap Lazy.fromStrict $ Strict.readFile logFile- case Archive.readEntries archive of+ case archiveRead (logArchiver identifier) archive of Done -> worker logFiles- Next entry next -> return $ Just (decode' (lastEntry entry next))- Fail msg -> error msg+ Next entry next -> return $ Just (decode' identifier (lastEntry entry next))+ Fail msg -> error $ "Data.Acid.Log: " <> msg lastEntry entry Done = entry- lastEntry entry (Fail msg) = error msg+ lastEntry entry (Fail msg) = error $ "Data.Acid.Log: " <> msg lastEntry _ (Next entry next) = lastEntry entry next --- Schedule a new log entry. This call does not block--- The given IO action runs once the object is durable. The IO action--- blocks the serialization of events so it should be swift.-pushEntry :: SafeCopy object => FileLog object -> object -> IO () -> IO ()+-- | Schedule a new log entry. This call does not block. The given IO action+-- runs once the object is durable. The IO action blocks the serialization of+-- events so it should be swift.+pushEntry :: FileLog object -> object -> IO () -> IO () pushEntry fLog object finally = atomically $ do tid <- readTVar (logNextEntryId fLog)- writeTVar (logNextEntryId fLog) (tid+1)+ writeTVar (logNextEntryId fLog) $! tid+1 (entries, actions) <- readTVar (logQueue fLog) writeTVar (logQueue fLog) ( encoded : entries, finally : actions ) where- encoded = Lazy.fromChunks [ Strict.copy $ Put.runPut (safePut object) ]+ encoded = Lazy.fromChunks [ Strict.copy $ Lazy.toStrict $+ serialiserEncode (logSerialiser (logIdentifier fLog)) object ] --- The given IO action is executed once all previous entries are durable.+-- | The given IO action is executed once all previous entries are durable. pushAction :: FileLog object -> IO () -> IO () pushAction fLog finally = atomically $ do (entries, actions) <- readTVar (logQueue fLog)@@ -336,8 +354,8 @@ -- FIXME: Check for unused input.-decode' :: SafeCopy object => Lazy.ByteString -> object-decode' inp =- case Get.runGetLazy safeGet inp of- Left msg -> error msg+decode' :: LogKey object -> Lazy.ByteString -> object+decode' s inp =+ case serialiserDecode (logSerialiser s) inp of+ Left msg -> error $ "Data.Acid.Log: " <> msg Right val -> val
@@ -1,4 +1,4 @@-{-# LANGUAGE DeriveDataTypeable, BangPatterns #-}+{-# LANGUAGE BangPatterns #-} ----------------------------------------------------------------------------- -- | -- Module : Data.Acid.Memory@@ -8,6 +8,7 @@ -- Portability : non-portable (uses GHC extensions) -- -- AcidState container without a transaction log. Mostly used for testing.+-- Supports Atomicity, Consistency and Isolation, but not Durability. -- module Data.Acid.Memory@@ -21,12 +22,9 @@ import Control.Concurrent ( newEmptyMVar, putMVar, MVar ) import Control.Monad.State ( runState ) import Data.ByteString.Lazy ( ByteString )-import Data.Typeable ( Typeable ) import Data.IORef ( IORef, newIORef, readIORef, writeIORef ) -import Data.SafeCopy ( SafeCopy(..) ) - {-| State container offering full ACID (Atomicity, Consistency, Isolation and Durability) guarantees. @@ -43,9 +41,10 @@ data MemoryState st = MemoryState { localCore :: Core st , localCopy :: IORef st- } deriving (Typeable)+ } --- | Create an AcidState given an initial value.+-- | Create an 'AcidState' given an initial value. The state is kept only in+-- memory, so it is not durable. openMemoryState :: (IsAcidic st) => st -- ^ Initial state value. -> IO (AcidState st)@@ -55,8 +54,7 @@ return $ toAcidState MemoryState { localCore = core, localCopy = ref } --- | Issue an Update event and return immediately. The event is not durable--- before the MVar has been filled but the order of events is honored.+-- | Issue an Update event and return immediately. The order of events is honored. -- The behavior in case of exceptions is exactly the same as for 'update'. -- -- If EventA is scheduled before EventB, EventA /will/ be executed before EventB:@@ -103,12 +101,12 @@ where coldMethod = lookupColdMethod (localCore acidState) event -- | This is a nop with the memory backend.-createMemoryCheckpoint :: SafeCopy st => MemoryState st -> IO ()+createMemoryCheckpoint :: MemoryState st -> IO () createMemoryCheckpoint acidState = return () -- | This is a nop with the memory backend.-createMemoryArchive :: SafeCopy st => MemoryState st -> IO ()+createMemoryArchive :: MemoryState st -> IO () createMemoryArchive acidState = return ()
@@ -1,4 +1,4 @@-{-# LANGUAGE DeriveDataTypeable, BangPatterns #-}+{-# LANGUAGE BangPatterns #-} ----------------------------------------------------------------------------- -- | -- Module : Data.Acid.Memory.Pure@@ -7,8 +7,13 @@ -- Maintainer : lemmih@gmail.com -- Portability : non-portable (uses GHC extensions) ----- AcidState container without a transaction log. Mostly used for testing. +-- AcidState container without a transaction log. Mostly used for testing. --+-- This module consists of internal implementation details for+-- "Data.Acid.Memory". You should not normally need to import it. Call+-- 'Data.Acid.Memory.openMemoryState' and thereafter use the API from+-- "Data.Acid" instead.+-- module Data.Acid.Memory.Pure ( IsAcidic(..)@@ -35,18 +40,8 @@ import Control.Monad.State import Control.Monad.Reader -{-| State container offering full ACID (Atomicity, Consistency, Isolation and Durability)- guarantees.-- [@Atomicity@] State changes are all-or-nothing. This is what you'd expect of any state- variable in Haskell and AcidState doesn't change that.-- [@Consistency@] No event or set of events will break your data invariants.-- [@Isolation@] Transactions cannot interfere with each other even when issued in parallel.-- [@Durability@] Successful transaction are guaranteed to survive system failure (both- hardware and software).+{-| Pure state value used internally. This is not the same as+ 'Data.Acid.AcidState' from "Data.Acid". -} data AcidState st = AcidState { localMethods :: MethodMap st@@ -80,10 +75,10 @@ -- | Create an AcidState given an initial value. openAcidState :: IsAcidic st- => st -- ^ Initial state value. + => st -- ^ Initial state value. -> AcidState st openAcidState initialState- = AcidState { localMethods = mkMethodMap (eventsToMethods acidEvents) + = AcidState { localMethods = mkMethodMap (eventsToMethods acidEvents) , localState = initialState } -- | Execute the 'Update' monad in a pure environment.
@@ -1,4 +1,4 @@-{-# LANGUAGE CPP, DeriveDataTypeable, RankNTypes, RecordWildCards, ScopedTypeVariables #-}+{-# LANGUAGE CPP, RankNTypes, RecordWildCards, ScopedTypeVariables #-} ----------------------------------------------------------------------------- {- | Module : Data.Acid.Remote@@ -25,7 +25,7 @@ 'openRemoteState' and 'acidServer' communicate over an unencrypted socket. If you need an encrypted connection, see @acid-state-tls@. -On Unix®-like systems you can use 'UnixSocket' to create a socket file for+On Unix®-like systems you can use 'SockAddrUnix' to create a socket file for local communication between the client and server. Access can be controlled by setting the permissions of the parent directory containing the socket file.@@ -33,7 +33,7 @@ It is also possible to perform some simple authentication using 'sharedSecretCheck' and 'sharedSecretPerform'. Keep in mind that secrets will be sent in plain-text if you do not use-@acid-state-tls@. If you are using a 'UnixSocket' additional+@acid-state-tls@. If you are using a 'SockAddrUnix' additional authentication may not be required, so you can use 'skipAuthenticationCheck' and 'skipAuthenticationPerform'. @@ -78,8 +78,10 @@ ( -- * Server/Client acidServer+ , acidServerSockAddr , acidServer' , openRemoteState+ , openRemoteStateSockAddr -- * Authentication , skipAuthenticationCheck , skipAuthenticationPerform@@ -99,7 +101,7 @@ import Control.Concurrent.STM.TQueue import Control.Exception ( AsyncException(ThreadKilled) , Exception(fromException), IOException, Handler(..)- , SomeException, catch, catches, throw )+ , SomeException, catch, catches, throw, bracketOnError ) import Control.Exception ( throwIO, finally ) import Control.Monad ( forever, liftM, join, when ) import Control.Concurrent ( ThreadId, forkIO, threadDelay, killThread, myThreadId )@@ -108,20 +110,21 @@ import Data.Acid.Abstract import Data.Acid.Core import Data.Acid.Common+#if !MIN_VERSION_base(4,11,0)+import Data.Monoid ((<>))+#endif import qualified Data.ByteString as Strict import Data.ByteString.Char8 ( pack ) import qualified Data.ByteString.Lazy as Lazy import Data.IORef ( newIORef, readIORef, writeIORef ) import Data.Serialize-import Data.SafeCopy ( SafeCopy, safeGet, safePut ) import Data.Set ( Set, member )-import Data.Typeable ( Typeable ) import GHC.IO.Exception ( IOErrorType(..) )-import Network ( HostName, PortID(..), connectTo, listenOn, withSocketsDo )-import Network.Socket ( Socket, accept, sClose )-import Network.Socket.ByteString ( recv, sendAll )+import Network.BSD ( PortNumber, getProtocolNumber, getHostByName, hostAddress )+import Network.Socket+import Network.Socket.ByteString as NSB ( recv, sendAll ) import System.Directory ( removeFile )-import System.IO ( Handle, hPrint, hFlush, hClose, stderr )+import System.IO ( Handle, hPrint, hFlush, hClose, stderr, IOMode(..) ) import System.IO.Error ( ioeGetErrorType, isFullError, isDoesNotExistError ) debugStrLn :: String -> IO ()@@ -143,7 +146,7 @@ | AcidStateClosed | SerializeError String | AuthenticationError String- deriving (Eq, Show, Typeable)+ deriving (Eq, Show) instance Exception AcidRemoteException -- | create a 'CommChannel' from a 'Handle'. The 'Handle' should be@@ -163,8 +166,8 @@ socketToCommChannel :: Socket -> CommChannel socketToCommChannel socket = CommChannel { ccPut = sendAll socket- , ccGetSome = recv socket- , ccClose = sClose socket+ , ccGetSome = NSB.recv socket+ , ccClose = close socket } {- | skip server-side authentication checking entirely. -}@@ -207,40 +210,73 @@ then return () else throwIO (AuthenticationError "shared secret authentication failed.") -{- | Accept connections on @port@ and handle requests using the given 'AcidState'.+{- | Accept connections on @sockAddr@ and handle requests using the given 'AcidState'. This call doesn't return. - On Unix®-like systems you can use 'UnixSocket' to communicate- using a socket file. To control access, you can set the permissions of- the parent directory which contains the socket file.-- see also: 'openRemoteState' and 'sharedSecretCheck'.+ see also: 'acidServer', 'openRemoteState' and 'sharedSecretCheck'. -}-acidServer :: SafeCopy st =>- (CommChannel -> IO Bool) -- ^ check authentication, see 'sharedSecretPerform'- -> PortID -- ^ Port to listen on+acidServerSockAddr :: (CommChannel -> IO Bool) -- ^ check authentication, see 'sharedSecretPerform'+ -> SockAddr -- ^ SockAddr to listen on -> AcidState st -- ^ state to serve -> IO ()-acidServer checkAuth port acidState- = withSocketsDo $- do listenSocket <- listenOn port+acidServerSockAddr checkAuth sockAddr acidState+ = do listenSocket <- listenOn sockAddr (acidServer' checkAuth listenSocket acidState) `finally` (cleanup listenSocket) where cleanup socket =- do sClose socket- case port of+ do close socket #if !defined(mingw32_HOST_OS) && !defined(cygwin32_HOST_OS) && !defined(_WIN32)- UnixSocket path -> removeFile path+ case sockAddr of+ (SockAddrUnix path) -> removeFile path+ _ -> pure () #endif- _ -> return () ++{- | Accept connections on @port@ and handle requests using the given 'AcidState'.+ This call doesn't return.++ see also: 'acidServerSockAddr', 'openRemoteState' and 'sharedSecretCheck'.+ -}+acidServer :: (CommChannel -> IO Bool) -- ^ check authentication, see 'sharedSecretPerform'+ -> PortNumber -- ^ Port to listen on+ -> AcidState st -- ^ state to serve+ -> IO ()+acidServer checkAuth port acidState+ = acidServerSockAddr checkAuth (SockAddrInet port 0) acidState++listenOn :: SockAddr -> IO Socket+listenOn sockAddr = do+#if !defined(mingw32_HOST_OS) && !defined(cygwin32_HOST_OS) && !defined(_WIN32)+ proto <- case sockAddr of+ (SockAddrUnix {}) -> pure 0+ _ -> getProtocolNumber "tcp"+#else+ proto <- getProtocolNumber "tcp"+#endif+ bracketOnError+ (socket af Stream proto)+ close+ (\sock -> do+ setSocketOption sock ReuseAddr 1+ bind sock sockAddr+ listen sock maxListenQueue+ return sock+ )++ where+ af = case sockAddr of+ (SockAddrInet {}) -> AF_INET+ (SockAddrInet6 {}) -> AF_INET6+#if !defined(mingw32_HOST_OS) && !defined(cygwin32_HOST_OS) && !defined(_WIN32)+ (SockAddrUnix {}) -> AF_UNIX+#endif+ {- | Works the same way as 'acidServer', but uses pre-binded socket @listenSocket@. Can be useful when fine-tuning of socket binding parameters is needed (for example, listening on a particular network interface, IPv4/IPv6 options). -}-acidServer' :: SafeCopy st =>- (CommChannel -> IO Bool) -- ^ check authentication, see 'sharedSecretPerform'+acidServer' :: (CommChannel -> IO Bool) -- ^ check authentication, see 'sharedSecretPerform' -> Socket -- ^ binded socket to accept connections from -> AcidState st -- ^ state to serve -> IO ()@@ -291,7 +327,7 @@ 1 -> liftM RunUpdate get 2 -> return CreateCheckpoint 3 -> return CreateArchive- _ -> error $ "Serialize.get for Command, invalid tag: " ++ show tag+ _ -> error $ "Data.Acid.Remote: Serialize.get for Command, invalid tag: " ++ show tag data Response = Result Lazy.ByteString | Acknowledgement | ConnectionError @@ -305,14 +341,13 @@ 0 -> liftM Result get 1 -> return Acknowledgement 2 -> return ConnectionError- _ -> error $ "Serialize.get for Response, invalid tag: " ++ show tag+ _ -> error $ "Data.Acid.Remote: Serialize.get for Response, invalid tag: " ++ show tag {- | Server inner-loop This function is generally only needed if you are adding a new communication channel. -}-process :: SafeCopy st =>- CommChannel -- ^ a connected, authenticated communication channel+process :: CommChannel -- ^ a connected, authenticated communication channel -> AcidState st -- ^ state to share -> IO () process CommChannel{..} acidState@@ -341,23 +376,53 @@ writeChan chan (return Acknowledgement) data RemoteState st = RemoteState (Command -> IO (MVar Response)) (IO ())- deriving (Typeable) {- | Connect to an acid-state server which is sharing an 'AcidState'. -} openRemoteState :: IsAcidic st => (CommChannel -> IO ()) -- ^ authentication function, see 'sharedSecretPerform'- -> HostName -- ^ remote host to connect to (ignored when 'PortID' is 'UnixSocket')- -> PortID -- ^ remote port to connect to+ -> HostName -- ^ remote host to connect to+ -> PortNumber -- ^ remote port to connect to -> IO (AcidState st)-openRemoteState performAuthorization host port+openRemoteState performAuthorization host port =+ do he <- getHostByName host+ openRemoteStateSockAddr performAuthorization (SockAddrInet port (hostAddress he))++{- | Connect to an acid-state server which is sharing an 'AcidState'. -}+openRemoteStateSockAddr :: IsAcidic st =>+ (CommChannel -> IO ()) -- ^ authentication function, see 'sharedSecretPerform'+ -> SockAddr -- ^ remote SockAddr to connect to+ -> IO (AcidState st)+openRemoteStateSockAddr performAuthorization sockAddr = withSocketsDo $ do processRemoteState reconnect where+ af :: Family+ af = case sockAddr of+ (SockAddrInet {}) -> AF_INET+ (SockAddrInet6 {}) -> AF_INET6+#if !defined(mingw32_HOST_OS) && !defined(cygwin32_HOST_OS) && !defined(_WIN32)+ (SockAddrUnix {}) -> AF_UNIX+#endif+ -- | reconnect reconnect :: IO CommChannel reconnect = (do debugStrLn "Reconnecting."- handle <- connectTo host port+#if !defined(mingw32_HOST_OS) && !defined(cygwin32_HOST_OS) && !defined(_WIN32)+ proto <- case sockAddr of+ (SockAddrUnix {}) -> pure 0+ _ -> getProtocolNumber "tcp"+#else+ proto <- getProtocolNumber "tcp"+#endif+ handle <- bracketOnError+ (socket af Stream proto)+ close -- only done if there's an error+ (\sock -> do+ connect sock sockAddr+ socketToHandle sock ReadWriteMode+ )+ let cc = handleToCommChannel handle performAuthorization cc debugStrLn "Reconnected."@@ -429,7 +494,7 @@ getResponse leftover = do debugStrLn $ "listener: listening for Response." let go inp = case inp of- Fail msg _ -> error msg+ Fail msg _ -> error $ "Data.Acid.Remote: " <> msg Partial cont -> do debugStrLn $ "listener: ccGetSome" bs <- ccGetSome cc 1024 go (cont bs)@@ -482,13 +547,15 @@ return (toAcidState $ RemoteState actor (shutdown actorTID)) -remoteQuery :: QueryEvent event => RemoteState (EventState event) -> event -> IO (EventResult event)-remoteQuery acidState event- = do let encoded = runPutLazy (safePut event)+remoteQuery :: QueryEvent event => RemoteState (EventState event) -> MethodMap (EventState event) -> event -> IO (EventResult event)+remoteQuery acidState mmap event+ = do let encoded = encodeMethod ms event resp <- remoteQueryCold acidState (methodTag event, encoded)- return (case runGetLazyFix safeGet resp of- Left msg -> error msg+ return (case decodeResult ms resp of+ Left msg -> error $ "Data.Acid.Remote: " <> msg Right result -> result)+ where+ (_, ms) = lookupHotMethodAndSerialiser mmap event remoteQueryCold :: RemoteState st -> Tagged Lazy.ByteString -> IO Lazy.ByteString remoteQueryCold rs@(RemoteState fn _shutdown) event@@ -497,18 +564,20 @@ (Result result) -> return result ConnectionError -> do debugStrLn "retrying query event." remoteQueryCold rs event- Acknowledgement -> error "remoteQueryCold got Acknowledgement. That should never happen."+ Acknowledgement -> error "Data.Acid.Remote: remoteQueryCold got Acknowledgement. That should never happen." -scheduleRemoteUpdate :: UpdateEvent event => RemoteState (EventState event) -> event -> IO (MVar (EventResult event))-scheduleRemoteUpdate (RemoteState fn _shutdown) event- = do let encoded = runPutLazy (safePut event)+scheduleRemoteUpdate :: UpdateEvent event => RemoteState (EventState event) -> MethodMap (EventState event) -> event -> IO (MVar (EventResult event))+scheduleRemoteUpdate (RemoteState fn _shutdown) mmap event+ = do let encoded = encodeMethod ms event parsed <- newEmptyMVar respRef <- fn (RunUpdate (methodTag event, encoded)) forkIO $ do Result resp <- takeMVar respRef- putMVar parsed (case runGetLazyFix safeGet resp of- Left msg -> error msg+ putMVar parsed (case decodeResult ms resp of+ Left msg -> error $ "Data.Acid.Remote: " <> msg Right result -> result) return parsed+ where+ (_, ms) = lookupHotMethodAndSerialiser mmap event scheduleRemoteColdUpdate :: RemoteState st -> Tagged Lazy.ByteString -> IO (MVar Lazy.ByteString) scheduleRemoteColdUpdate (RemoteState fn _shutdown) event@@ -531,15 +600,17 @@ = do Acknowledgement <- takeMVar =<< fn CreateArchive return () -toAcidState :: IsAcidic st => RemoteState st -> AcidState st+toAcidState :: forall st . IsAcidic st => RemoteState st -> AcidState st toAcidState remote- = AcidState { _scheduleUpdate = scheduleRemoteUpdate remote+ = AcidState { _scheduleUpdate = scheduleRemoteUpdate remote mmap , scheduleColdUpdate = scheduleRemoteColdUpdate remote- , _query = remoteQuery remote+ , _query = remoteQuery remote mmap , queryCold = remoteQueryCold remote , createCheckpoint = createRemoteCheckpoint remote , createArchive = createRemoteArchive remote , closeAcidState = closeRemoteState remote , acidSubState = mkAnyState remote }-+ where+ mmap :: MethodMap st+ mmap = mkMethodMap (eventsToMethods acidEvents)
@@ -0,0 +1,142 @@+{-# LANGUAGE LambdaCase #-}++module Data.Acid.Repair+ ( repairFile+ , repairEvents+ , repairCheckpoints+ ) where++import qualified Data.Acid.Archive as Archive+import Data.Acid.Local (mkEventsLogKey, mkCheckpointsLogKey)+import Data.Acid.Log (LogKey)+import qualified Data.Acid.Log as Log+import qualified Data.ByteString.Lazy as Lazy+import Data.List (sort)+import System.Directory+import System.FilePath.Posix+import System.IO (hClose, openTempFile)++repairEntries :: Lazy.ByteString -> Lazy.ByteString+repairEntries =+ Archive.packEntries . Archive.entriesToListNoFail . Archive.readEntries++-- | @'repairFile' path@ will truncate the entries in @file@ until there are+-- only valid entries (if a corrupted entry is found, then the rest of the file+-- is truncated).+--+-- The old file will be copied to @path.bak@ (or @path.bak.1@, etc… if the file+-- already exists).+--+-- 'repairFile' tries very hard to avoid leaving files in an inconsistent state:+-- the truncated file is written in a temporary file, which is then moved into+-- place, similarly copies are performed with moves instead. Still this is not+-- fully atomic: there are two consecutive moves, so 'repairFile' may, in case+-- of crash, yield a state where the @path.bak@ file is there but no @path@ is+-- there anymore, this would require manual intervention.+repairFile :: FilePath -> IO ()+repairFile fp = do+ broken <- Lazy.readFile fp+ let repaired = repairEntries broken+ (tmp, temph) <- openTempFile (takeDirectory fp) (takeFileName fp)+ -- We use `openTempFile`, here, rather than `findNext` because we want to+ -- make extra-sure that we are not overriding an important file.+ hClose temph+ -- Closing immediately to benefit from the bracket guarantees of+ -- `writeFile`. A more elegant solution would be to use a `withTempFile`+ -- function, such as that from package `temporary`.+ Lazy.writeFile tmp repaired+ dropFile fp+ renameFile tmp fp++-- Repairs the files corresponding to the given 'LogKey'. It implements the+-- logic described in 'repairEvents'.+repairLogs :: LogKey object -> IO ()+repairLogs identifier = do+ logFiles <- Log.findLogFiles identifier+ let sorted = sort logFiles+ (_eventIds, files) = unzip sorted+ broken_files <- mapM needsRepair files+ -- We're doing a second deserialisation of the files here (see+ -- 'needsRepair'). It would be better, computation-time-wise to make as+ -- single pass and let `repairEntries`, for instance, return whether a fix+ -- is needed. But it's a lot of complication and requires loading the+ -- entire base in memory, rather than streaming files one-by-one. So it's+ -- better to just do the second pass.+ repair $ map snd $ dropWhile (\(b,_) -> not b) $ zip broken_files files+ where+ repair [] = return ()+ repair (file:rest) = do+ mapM_ dropFile (reverse rest)+ repairFile file++-- Moves (atomically) a file `path` to `path.bak` (or `path.bak.1`, etc… if the+-- file already exists).+dropFile :: FilePath -> IO ()+dropFile fp = do+ bak <- findNext (fp ++ ".bak")+ -- We're using `findNext` rather than `openTempFile`, here, because we+ -- want predictable names+ renameFile fp bak++-- | Repairs the WAL files with the following strategy:+--+-- * Let `f` be the oldest corrupted file.+-- * All files older than `f` is left untouched+-- * `f` is repaired with `repairFile`+-- * Old files younger than `f` is dropped (and saved to `path.bak`, or+-- `path.bak.1`, etc…)+--+-- In other words, all the log entries after the first corrupted entry is+-- dropped. The reasoning is that newer entries are likely not to make sense+-- after some entries have been removed from the log. This strategy guarantees a+-- consistent state, albeit a potentially old one.+repairEvents+ :: FilePath -- ^ Directory in which the events files can be found.+ -> IO ()+repairEvents directory =+ repairLogs (mkEventsLogKey directory noserialisation)+ where+ noserialisation =+ error "Repair.repairEvents: the serialisation layer shouldn't be forced"++-- | Repairs the checkpoints file using the following strategy:+--+-- * Every checkpoints file is repaired with `repairFile`+--+-- Checkpoints are mostly independent. Contrary to 'repairEvents', dropping a+-- checkpoint doesn't affect the consistency of later checkpoints.+repairCheckpoints+ :: FilePath -- ^ Directory in which the checkpoints files can be found.+ -> IO ()+repairCheckpoints directory = do+ let checkpointLogKey = mkCheckpointsLogKey directory noserialisation+ checkpointFiles <- Log.findLogFiles checkpointLogKey+ let (_eventIds, files) = unzip checkpointFiles+ mapM_ repairFile files+ where+ noserialisation =+ error "Repair.repairCheckpoints: the serialisation layer shouldn't be forced"++needsRepair :: FilePath -> IO Bool+needsRepair fp = do+ contents <- Lazy.readFile fp+ let entries = Archive.readEntries contents+ return $ entriesNeedRepair entries+ where+ entriesNeedRepair Archive.Fail{} = True+ entriesNeedRepair Archive.Done = False+ entriesNeedRepair (Archive.Next _ rest) = entriesNeedRepair rest++findNext :: FilePath -> IO (FilePath)+findNext fp = go 0+ where+ go n =+ let next = fileWithSuffix fp n in+ doesFileExist next >>= \case+ False -> return next+ True -> go (n+1)++fileWithSuffix :: FilePath -> Int -> FilePath+fileWithSuffix fp i =+ if i == 0 then fp+ else fp ++ "." ++ show i
@@ -1,22 +1,30 @@-{-# LANGUAGE TemplateHaskell, CPP #-}+{-# LANGUAGE TemplateHaskell, CPP, NamedFieldPuns #-}+ {- Holy crap this code is messy. -}-module Data.Acid.TemplateHaskell- ( makeAcidic- ) where+module Data.Acid.TemplateHaskell where import Language.Haskell.TH import Language.Haskell.TH.Ppr+import Language.Haskell.TH.ExpandSyns import Data.Acid.Core import Data.Acid.Common -import Data.List ((\\), nub)+import Data.List ((\\), nub, delete) import Data.SafeCopy import Data.Typeable import Data.Char-import Control.Applicative+#if !MIN_VERSION_base(4,11,0)+import Data.Monoid ((<>))+#endif import Control.Monad+import Control.Monad.State (MonadState)+import Control.Monad.Reader (MonadReader) +#if !MIN_VERSION_template_haskell(2,17,0)+type TyVarBndrUnit = TyVarBndr+#endif+ {-| Create the control structures required for acid states using Template Haskell. @@ -42,56 +50,119 @@ -} makeAcidic :: Name -> [Name] -> Q [Dec]-makeAcidic stateName eventNames+makeAcidic = makeAcidicWithSerialiser safeCopySerialiserSpec+++-- | Specifies how to customise the 'IsAcidic' instance and event data+-- type serialisation instances for a particular serialisation layer.+data SerialiserSpec =+ SerialiserSpec+ { serialisationClassName :: Name+ -- ^ Class for serialisable types, e.g. @''Safecopy@.+ , methodSerialiserName :: Name+ -- ^ Name of the 'MethodSerialiser' to use in the list of+ -- events in the 'IsAcidic' instance.+ , makeEventSerialiser :: Name -> Type -> DecQ+ -- ^ Function to generate an instance of the class named by+ -- 'serialisationClassName', given the event name and its type.+ }++-- | Default implementation of 'SerialiserSpec' that uses 'SafeCopy'+-- for serialising events.+safeCopySerialiserSpec :: SerialiserSpec+safeCopySerialiserSpec =+ SerialiserSpec { serialisationClassName = ''SafeCopy+ , methodSerialiserName = 'safeCopyMethodSerialiser+ , makeEventSerialiser = makeSafeCopyInstance+ }+++-- | A variant on 'makeAcidic' that makes it possible to explicitly choose the+-- serialisation implementation to be used for methods.+makeAcidicWithSerialiser :: SerialiserSpec -> Name -> [Name] -> Q [Dec]+makeAcidicWithSerialiser ss stateName eventNames = do stateInfo <- reify stateName case stateInfo of TyConI tycon ->case tycon of- DataD _cxt _name tyvars constructors _derivs- -> makeAcidic' eventNames stateName tyvars constructors- NewtypeD _cxt _name tyvars constructor _derivs- -> makeAcidic' eventNames stateName tyvars [constructor]++ DataD _cxt _name tyvars _kind constructors _derivs+#if MIN_VERSION_template_haskell(2,21,0)+ -> makeAcidic' ss eventNames stateName (map void tyvars) constructors+#else+ -> makeAcidic' ss eventNames stateName tyvars constructors+#endif++ NewtypeD _cxt _name tyvars _kind constructor _derivs+#if MIN_VERSION_template_haskell(2,21,0)+ -> makeAcidic' ss eventNames stateName (map void tyvars) [constructor]+#else+ -> makeAcidic' ss eventNames stateName tyvars [constructor]+#endif+ TySynD _name tyvars _ty- -> makeAcidic' eventNames stateName tyvars []- _ -> error "Unsupported state type. Only 'data', 'newtype' and 'type' are supported."- _ -> error "Given state is not a type."+#if MIN_VERSION_template_haskell(2,21,0)+ -> makeAcidic' ss eventNames stateName (map void tyvars) []+#else+ -> makeAcidic' ss eventNames stateName tyvars []+#endif -makeAcidic' :: [Name] -> Name -> [TyVarBndr] -> [Con] -> Q [Dec]-makeAcidic' eventNames stateName tyvars constructors- = do events <- sequence [ makeEvent eventName | eventName <- eventNames ]- acidic <- makeIsAcidic eventNames stateName tyvars constructors+ _ -> error "Data.Acid.TemplateHaskell: Unsupported state type. Only 'data', 'newtype' and 'type' are supported."+ _ -> error "Data.Acid.TemplateHaskell: Given state is not a type."++makeAcidic' :: SerialiserSpec -> [Name] -> Name -> [TyVarBndrUnit] -> [Con] -> Q [Dec]+makeAcidic' ss eventNames stateName tyvars constructors+ = do events <- sequence [ makeEvent ss eventName | eventName <- eventNames ]+ acidic <- makeIsAcidic ss eventNames stateName tyvars constructors return $ acidic : concat events -makeEvent :: Name -> Q [Dec]-makeEvent eventName- = do eventType <- getEventType eventName- d <- makeEventDataType eventName eventType- b <- makeSafeCopyInstance eventName eventType- i <- makeMethodInstance eventName eventType- e <- makeEventInstance eventName eventType- return [d,b,i,e]+-- | Given an event name (e.g. @'myUpdate@), produce a data type like+--+-- > data MyUpdate = MyUpdate Argument+--+-- along with the 'Method' class instance, 'Event' class instance and+-- the instance of the appropriate serialisation class.+--+-- However, if the event data type already exists, this will generate+-- the serialisation instance only. This makes it possible to call+-- 'makeAcidicWithSerialiser' multiple times on the same events but+-- with different 'SerialiserSpec's, to support multiple serialisation+-- backends.+makeEvent :: SerialiserSpec -> Name -> Q [Dec]+makeEvent ss eventName+ = do exists <- recover (return False) (reify (toStructName eventName) >> return True)+ eventType <- getEventType eventName+ if exists+ then do b <- makeEventSerialiser ss eventName eventType+ return [b]+ else do d <- makeEventDataType eventName eventType+ b <- makeEventSerialiser ss eventName eventType+ i <- makeMethodInstance eventName eventType+ e <- makeEventInstance eventName eventType+ return [d,b,i,e] getEventType :: Name -> Q Type getEventType eventName = do eventInfo <- reify eventName case eventInfo of- VarI _name eventType _decl _fixity- -> return eventType- _ -> error $ "Events must be functions: " ++ show eventName+ VarI _name eventType _decl+ -> expandSyns eventType+ _ -> error $ "Data.Acid.TemplateHaskell: Events must be functions: " ++ show eventName --instance (SafeCopy key, Typeable key, SafeCopy val, Typeable val) => IsAcidic State where -- acidEvents = [ UpdateEvent (\(MyUpdateEvent arg1 arg2 -> myUpdateEvent arg1 arg2) ]-makeIsAcidic eventNames stateName tyvars constructors+makeIsAcidic ss eventNames stateName tyvars constructors = do types <- mapM getEventType eventNames stateType' <- stateType- let preds = [ ''SafeCopy, ''Typeable ]+ let preds = [ serialisationClassName ss, ''Typeable ] ty = appT (conT ''IsAcidic) stateType- handlers = zipWith makeEventHandler eventNames types+ handlers = zipWith (makeEventHandler ss) eventNames types cxtFromEvents = nub $ concat $ zipWith (eventCxts stateType' tyvars) eventNames types cxts' <- mkCxtFromTyVars preds tyvars cxtFromEvents instanceD (return cxts') ty- [ valD (varP 'acidEvents) (normalB (listE handlers)) [] ]- where stateType = foldl appT (conT stateName) [ varT var | PlainTV var <- tyvars ]+ [ valD (varP 'acidEvents) (normalB (listE handlers)) []+ ]+ where stateType = foldl appT (conT stateName) (map varT (allTyVarBndrNames tyvars)) -- | This function analyses an event function and extracts any -- additional class contexts which need to be added to the IsAcidic@@ -127,34 +198,48 @@ -- need to rename the variables in the context to match. -- -- The contexts returned by this function will have the variables renamed.-eventCxts :: Type -- ^ State type (used for error messages)- -> [TyVarBndr] -- ^ type variables that will be used for the State type in the IsAcidic instance- -> Name -- ^ 'Name' of the event- -> Type -- ^ 'Type' of the event- -> [Pred] -- ^ extra context to add to 'IsAcidic' instance+--+-- Additionally, if the event uses MonadReader or MonadState it might look+-- like this:+--+-- > setState :: (MonadState x m, IsFoo x) => m ()+--+-- In this case we have to rename 'x' to the actual state we're going to+-- use. This is done by 'renameState'.+eventCxts :: Type -- ^ State type+ -> [TyVarBndrUnit] -- ^ type variables that will be used for the State type in the IsAcidic instance+ -> Name -- ^ 'Name' of the event+ -> Type -- ^ 'Type' of the event+ -> [Pred] -- ^ extra context to add to 'IsAcidic' instance eventCxts targetStateType targetTyVars eventName eventType =- let (_tyvars, cxt, _args, stateType, _resultType, _isUpdate)+ let TypeAnalysis { context = cxt, stateType } = analyseType eventName eventType- eventTyVars = findTyVars stateType -- find the type variable names that this event is using for the State type- table = zip eventTyVars (map tyVarBndrName targetTyVars) -- create a lookup table- in map (unify table) cxt -- rename the type variables+ -- find the type variable names that this event is using+ -- for the State type+ eventTyVars = findTyVars stateType+ -- create a lookup table+ table = zip eventTyVars (map tyVarBndrName targetTyVars)+ in map (unify table) -- rename the type variables+ (renameState stateType targetStateType cxt) where -- | rename the type variables in a Pred unify :: [(Name, Name)] -> Pred -> Pred-#if MIN_VERSION_template_haskell(2,10,0) unify table p = rename p table p -- in 2.10.0: type Pred = Type-#else- unify table p@(ClassP n tys) = ClassP n (map (rename p table) tys)- unify table p@(EqualP a b) = EqualP (rename p table a) (rename p table b)-#endif -- | rename the type variables in a Type rename :: Pred -> [(Name, Name)] -> Type -> Type rename pred table t@(ForallT tyvarbndrs cxt typ) = -- this is probably wrong? I don't think acid-state can really handle this type anyway.. ForallT (map renameTyVar tyvarbndrs) (map (unify table) cxt) (rename pred table typ) where+#if MIN_VERSION_template_haskell(2,17,0)+ renameTyVar :: TyVarBndr a -> TyVarBndr a+ renameTyVar (PlainTV name ann) = PlainTV (renameName pred table name) ann+ renameTyVar (KindedTV name k ann) = KindedTV (renameName pred table name) k ann+#else+ renameTyVar :: TyVarBndr -> TyVarBndr renameTyVar (PlainTV name) = PlainTV (renameName pred table name) renameTyVar (KindedTV name k) = KindedTV (renameName pred table name) k+#endif rename pred table (VarT n) = VarT $ renameName pred table n rename pred table (AppT a b) = AppT (rename pred table a) (rename pred table b) rename pred table (SigT a k) = SigT (rename pred table a) k@@ -164,8 +249,10 @@ renameName :: Pred -> [(Name, Name)] -> Name -> Name renameName pred table n = case lookup n table of- Nothing -> error $ unlines [ show $ ppr_sig eventName eventType+ Nothing -> error $ unlines [ "Data.Acid.TemplateHaskell: " , ""+ , show $ ppr_sig eventName eventType+ , "" , "can not be used as an UpdateEvent because the class context: " , "" , pprint pred@@ -178,24 +265,33 @@ ] (Just n') -> n' --- UpdateEvent (\(MyUpdateEvent arg1 arg2) -> myUpdateEvent arg1 arg2)-makeEventHandler :: Name -> Type -> ExpQ-makeEventHandler eventName eventType+-- | See the end of comment for 'eventCxts'.+renameState :: Type -> Type -> Cxt -> Cxt+renameState tfrom tto cxt = map renamePred cxt+ where+ renamePred p = renameType p -- in 2.10.0: type Pred = Type+ renameType n | n == tfrom = tto+ renameType (AppT a b) = AppT (renameType a) (renameType b)+ renameType (SigT a k) = SigT (renameType a) k+ renameType typ = typ++-- UpdateEvent (\(MyUpdateEvent arg1 arg2) -> myUpdateEvent arg1 arg2) safeCopyMethodSerialiser+makeEventHandler :: SerialiserSpec -> Name -> Type -> ExpQ+makeEventHandler ss eventName eventType = do assertTyVarsOk vars <- replicateM (length args) (newName "arg") let lamClause = conP eventStructName [varP var | var <- vars ] conE constr `appE` lamE [lamClause] (foldl appE (varE eventName) (map varE vars))+ `appE` varE (methodSerialiserName ss) where constr = if isUpdate then 'UpdateEvent else 'QueryEvent- (tyvars, _cxt, args, stateType, _resultType, isUpdate) = analyseType eventName eventType- eventStructName = mkName (structName (nameBase eventName))- structName [] = []- structName (x:xs) = toUpper x : xs+ TypeAnalysis { tyvars, argumentTypes = args, stateType, isUpdate } = analyseType eventName eventType+ eventStructName = toStructName eventName stateTypeTyVars = findTyVars stateType tyVarNames = map tyVarBndrName tyvars assertTyVarsOk = case tyVarNames \\ stateTypeTyVars of [] -> return ()- ns -> error $ unlines+ ns -> error $ "Data.Acid.TemplateHaskell: " <> unlines [show $ ppr_sig eventName eventType , "" , "can not be used as an UpdateEvent because it contains the type variables: "@@ -207,26 +303,32 @@ , pprint stateType ] -----data MyUpdateEvent = MyUpdateEvent Arg1 Arg2--- deriving (Typeable)+-- data MyUpdateEvent = MyUpdateEvent Arg1 Arg2+makeEventDataType :: Name -> Type -> DecQ makeEventDataType eventName eventType = do let con = normalC eventStructName [ strictType notStrict (return arg) | arg <- args ]+ cxt = [] case args of- [_] -> newtypeD (return []) eventStructName tyvars con [''Typeable]- _ -> dataD (return []) eventStructName tyvars [con] [''Typeable]- where (tyvars, _cxt, args, _stateType, _resultType, _isUpdate) = analyseType eventName eventType- eventStructName = mkName (structName (nameBase eventName))- structName [] = []- structName (x:xs) = toUpper x : xs+#if MIN_VERSION_template_haskell(2,21,0)+ [_] -> newtypeD (return []) eventStructName (map (BndrReq <$) tyvars) Nothing con cxt+ _ -> dataD (return []) eventStructName (map (BndrReq <$) tyvars) Nothing [con] cxt+#elif MIN_VERSION_template_haskell(2,11,0)+ [_] -> newtypeD (return []) eventStructName tyvars Nothing con cxt+ _ -> dataD (return []) eventStructName tyvars Nothing [con] cxt+#else+ [_] -> newtypeD (return []) eventStructName tyvars con cxt+ _ -> dataD (return []) eventStructName tyvars [con] cxt+#endif+ where TypeAnalysis { tyvars, argumentTypes = args } = analyseType eventName eventType+ eventStructName = toStructName eventName -- instance (SafeCopy key, SafeCopy val) => SafeCopy (MyUpdateEvent key val) where -- put (MyUpdateEvent a b) = do put a; put b -- get = MyUpdateEvent <$> get <*> get+makeSafeCopyInstance :: Name -> Type -> DecQ makeSafeCopyInstance eventName eventType = do let preds = [ ''SafeCopy ]- ty = AppT (ConT ''SafeCopy) (foldl AppT (ConT eventStructName) [ VarT tyvar | PlainTV tyvar <- tyvars ])+ ty = AppT (ConT ''SafeCopy) (foldl AppT (ConT eventStructName) (map VarT (allTyVarBndrNames tyvars))) getBase = appE (varE 'return) (conE eventStructName) getArgs = foldl (\a b -> infixE (Just a) (varE '(<*>)) (Just (varE 'safeGet))) getBase args@@ -241,79 +343,134 @@ (return ty) [ funD 'putCopy [clause [putClause] (normalB (contained putExp)) []] , valD (varP 'getCopy) (normalB (contained getArgs)) []+ , funD 'errorTypeName [clause [wildP] (normalB (litE (stringL (pprint ty)))) []] ]- where (tyvars, context, args, _stateType, _resultType, _isUpdate) = analyseType eventName eventType- eventStructName = mkName (structName (nameBase eventName))- structName [] = []- structName (x:xs) = toUpper x : xs+ where TypeAnalysis { tyvars, context, argumentTypes = args } = analyseType eventName eventType+ eventStructName = toStructName eventName mkCxtFromTyVars preds tyvars extraContext- = cxt $ [ classP classPred [varT tyvar] | PlainTV tyvar <- tyvars, classPred <- preds ] +++ = cxt $ [ classP classPred [varT tyvar] | tyvar <- allTyVarBndrNames tyvars, classPred <- preds ] ++ map return extraContext {--instance (SafeCopy key, Typeable key- ,SafeCopy val, Typeable val) => Method (MyUpdateEvent key val) where+instance (Typeable key, Typeable val) => Method (MyUpdateEvent key val) where type MethodResult (MyUpdateEvent key val) = Return type MethodState (MyUpdateEvent key val) = State key val -}-makeMethodInstance eventName eventType- = do let preds = [ ''SafeCopy, ''Typeable ]- ty = AppT (ConT ''Method) (foldl AppT (ConT eventStructName) [ VarT tyvar | PlainTV tyvar <- tyvars ])- structType = foldl appT (conT eventStructName) [ varT tyvar | PlainTV tyvar <- tyvars ]- instanceD (cxt $ [ classP classPred [varT tyvar] | PlainTV tyvar <- tyvars, classPred <- preds ] ++ map return context)- (return ty)-#if __GLASGOW_HASKELL__ >= 707- [ tySynInstD ''MethodResult (tySynEqn [structType] (return resultType))- , tySynInstD ''MethodState (tySynEqn [structType] (return stateType))+makeMethodInstance :: Name -> Type -> DecQ+makeMethodInstance eventName eventType = do+ let preds =+ [ ''Typeable ]+ ty =+ AppT (ConT ''Method) (foldl AppT (ConT eventStructName) (map VarT (allTyVarBndrNames tyvars)))+ structType =+ foldl appT (conT eventStructName) (map varT (allTyVarBndrNames tyvars))+ instanceContext =+ cxt $+ [ classP classPred [varT tyvar]+ | tyvar <- allTyVarBndrNames tyvars+ , classPred <- preds+ ]+ ++ map return context+ instanceD+ instanceContext+ (return ty)+#if MIN_VERSION_template_haskell(2,15,0)+ [ tySynInstD $ tySynEqn Nothing (conT ''MethodResult `appT` structType) (return resultType)+ , tySynInstD $ tySynEqn Nothing (conT ''MethodState `appT` structType) (return stateType)+#elif __GLASGOW_HASKELL__ >= 707+ [ tySynInstD ''MethodResult (tySynEqn [structType] (return resultType))+ , tySynInstD ''MethodState (tySynEqn [structType] (return stateType)) #else- [ tySynInstD ''MethodResult [structType] (return resultType)- , tySynInstD ''MethodState [structType] (return stateType)+ [ tySynInstD ''MethodResult [structType] (return resultType)+ , tySynInstD ''MethodState [structType] (return stateType) #endif- ]- where (tyvars, context, _args, stateType, resultType, _isUpdate) = analyseType eventName eventType- eventStructName = mkName (structName (nameBase eventName))- structName [] = []- structName (x:xs) = toUpper x : xs+ ]+ where TypeAnalysis { tyvars, context, stateType, resultType } = analyseType eventName eventType+ eventStructName = toStructName eventName ---instance (SafeCopy key, Typeable key--- ,SafeCopy val, Typeable val) => UpdateEvent (MyUpdateEvent key val)+--instance (Typeable key, Typeable val) => UpdateEvent (MyUpdateEvent key val)+makeEventInstance :: Name -> Type -> DecQ makeEventInstance eventName eventType- = do let preds = [ ''SafeCopy, ''Typeable ]+ = do let preds = [ ''Typeable ] eventClass = if isUpdate then ''UpdateEvent else ''QueryEvent- ty = AppT (ConT eventClass) (foldl AppT (ConT eventStructName) [ VarT tyvar | PlainTV tyvar <- tyvars ])- instanceD (cxt $ [ classP classPred [varT tyvar] | PlainTV tyvar <- tyvars, classPred <- preds ] ++ map return context)+ ty = AppT (ConT eventClass) (foldl AppT (ConT eventStructName) (map VarT (allTyVarBndrNames tyvars)))+ instanceD (cxt $ [ classP classPred [varT tyvar] | tyvar <- allTyVarBndrNames tyvars, classPred <- preds ] ++ map return context) (return ty) []- where (tyvars, context, _args, _stateType, _resultType, isUpdate) = analyseType eventName eventType- eventStructName = mkName (structName (nameBase eventName))- structName [] = []- structName (x:xs) = toUpper x : xs+ where TypeAnalysis { tyvars, context, isUpdate } = analyseType eventName eventType+ eventStructName = toStructName eventName +data TypeAnalysis = TypeAnalysis+ { tyvars :: [TyVarBndrUnit]+ , context :: Cxt+ , argumentTypes :: [Type]+ , stateType :: Type+ , resultType :: Type+ , isUpdate :: Bool+ } deriving (Eq, Show) --- (tyvars, cxt, args, state type, result type, is update)-analyseType :: Name -> Type -> ([TyVarBndr], Cxt, [Type], Type, Type, Bool)-analyseType eventName t- = let (tyvars, cxt, t') = case t of- ForallT binds [] t' ->- (binds, [], t')- ForallT binds cxt t' ->- (binds, cxt, t')- _ -> ([], [], t)- args = getArgs t'- (stateType, resultType, isUpdate) = findMonad t'- in (tyvars, cxt, args, stateType, resultType, isUpdate)- where getArgs ForallT{} = error $ "Event has an invalid type signature: Nested forall: " ++ show eventName- getArgs (AppT (AppT ArrowT a) b) = a : getArgs b- getArgs _ = []+analyseType :: Name -> Type -> TypeAnalysis+analyseType eventName t = go [] [] [] t+ where+ getMonadReader :: Cxt -> Name -> [(Type, Type)]+ getMonadReader cxt m = do+ constraint@(AppT (AppT (ConT c) x) m') <- cxt+ guard (c == ''MonadReader && m' == VarT m)+ return (constraint, x) - findMonad (AppT (AppT ArrowT a) b)- = findMonad b- findMonad (AppT (AppT (ConT con) state) result)- | con == ''Update = (state, result, True)- | con == ''Query = (state, result, False)- findMonad _ = error $ "Event has an invalid type signature: Not an Update or a Query: " ++ show eventName+ getMonadState :: Cxt -> Name -> [(Type, Type)]+ getMonadState cxt m = do+ constraint@(AppT (AppT (ConT c) x) m') <- cxt+ guard (c == ''MonadState && m' == VarT m)+ return (constraint, x) + -- a -> b+ go tyvars cxt args (AppT (AppT ArrowT a) b)+ = go tyvars cxt (args ++ [a]) b+ -- Update st res+ -- Query st res+ go tyvars context argumentTypes (AppT (AppT (ConT con) stateType) resultType)+ | con == ''Update =+ TypeAnalysis+ { tyvars, context, argumentTypes, stateType, resultType+ , isUpdate = True+ }+ | con == ''Query =+ TypeAnalysis+ { tyvars, context, argumentTypes, stateType, resultType+ , isUpdate = False+ }+ -- (...) => a+ go tyvars cxt args (ForallT tyvars2 cxt2 a)+#if MIN_VERSION_template_haskell(2,17,0)+ = go (tyvars ++ fmap void tyvars2) (cxt ++ cxt2) args a+#else+ = go (tyvars ++ tyvars2) (cxt ++ cxt2) args a+#endif+ -- (MonadState state m) => ... -> m result+ -- (MonadReader state m) => ... -> m result+ go tyvars' cxt argumentTypes (AppT (VarT m) resultType)+ | [] <- queries, [(cx, stateType)] <- updates+ = TypeAnalysis+ { tyvars, argumentTypes , stateType, resultType+ , isUpdate = True+ , context = delete cx cxt+ }++ | [(cx, stateType)] <- queries, [] <- updates+ = TypeAnalysis+ { tyvars, argumentTypes , stateType, resultType+ , isUpdate = False+ , context = delete cx cxt+ }+ where+ queries = getMonadReader cxt m+ updates = getMonadState cxt m+ tyvars = filter ((/= m) . tyVarBndrName) tyvars'+ -- otherwise, fail+ go _ _ _ _ = error $ "Data.Acid.TemplateHaskell: Event has an invalid type signature: Not an Update, Query, MonadState, or MonadReader: " ++ show eventName+ -- | find the type variables -- | e.g. State a b ==> [a,b] findTyVars :: Type -> [Name]@@ -324,6 +481,26 @@ findTyVars _ = [] -- | extract the 'Name' from a 'TyVarBndr'+#if MIN_VERSION_template_haskell(2,17,0)+tyVarBndrName :: TyVarBndr a -> Name+tyVarBndrName (PlainTV n _) = n+tyVarBndrName (KindedTV n _ _) = n++allTyVarBndrNames :: [TyVarBndr a] -> [Name]+allTyVarBndrNames tyvars = map tyVarBndrName tyvars+#else tyVarBndrName :: TyVarBndr -> Name tyVarBndrName (PlainTV n) = n tyVarBndrName (KindedTV n _) = n++allTyVarBndrNames :: [TyVarBndr] -> [Name]+allTyVarBndrNames tyvars = map tyVarBndrName tyvars+#endif++-- | Convert the 'Name' of the event function into the name of the+-- corresponding data constructor.+toStructName :: Name -> Name+toStructName eventName = mkName (structName (nameBase eventName))+ where+ structName [] = []+ structName (x:xs) = toUpper x : xs
@@ -0,0 +1,1 @@+0.15.0
binary file changed (absent → 71486 bytes)
@@ -0,0 +1,1 @@+0.15.0
@@ -0,0 +1,1 @@+0.15.0
binary file changed (absent → 328 bytes)
binary file changed (absent → 446 bytes)
binary file changed (absent → 512 bytes)
binary file changed (absent → 225 bytes)
binary file changed (absent → 327 bytes)
binary file changed (absent → 190 bytes)
binary file changed (absent → 1259 bytes)
binary file changed (absent → 1141 bytes)
binary file changed (absent → 215 bytes)
binary file changed (absent → 206 bytes)
binary file changed (absent → 1147 bytes)
binary file changed (absent → 205 bytes)
binary file changed (absent → 116 bytes)
binary file changed (absent → 936 bytes)
binary file changed (absent → 872 bytes)
binary file changed (absent → 111 bytes)
binary file changed (absent → 303 bytes)
binary file changed (absent → 1663 bytes)
binary file changed (absent → 190 bytes)
binary file changed (absent → 190 bytes)
binary file changed (absent → 209 bytes)
binary file changed (absent → 867 bytes)
binary file changed (absent → 553 bytes)
binary file changed (absent → 494 bytes)
binary file changed (absent → 414 bytes)
binary file changed (absent → 110 bytes)
binary file changed (absent → 396 bytes)
binary file changed (absent → 1059 bytes)
binary file changed (absent → 190 bytes)
binary file changed (absent → 653 bytes)
@@ -0,0 +1,1 @@+0.15.0
binary file changed (absent → 96 bytes)
binary file changed (absent → 192 bytes)
binary file changed (absent → 134 bytes)
binary file changed (absent → 226 bytes)
binary file changed (absent → 226 bytes)
binary file changed (absent → 226 bytes)
binary file changed (absent → 226 bytes)
binary file changed (absent → 292 bytes)
binary file changed (absent → 301 bytes)
binary file changed (absent → 301 bytes)
binary file changed (absent → 330 bytes)
binary file changed (absent → 664 bytes)
binary file changed (absent → 708 bytes)
binary file changed (absent → 354 bytes)
binary file changed (absent → 708 bytes)
binary file changed (absent → 354 bytes)
binary file changed (absent → 747 bytes)
binary file changed (absent → 393 bytes)
binary file changed (absent → 393 bytes)
binary file changed (absent → 393 bytes)
binary file changed (absent → 1169 bytes)
binary file changed (absent → 388 bytes)
binary file changed (absent → 778 bytes)
binary file changed (absent → 392 bytes)
binary file changed (absent → 392 bytes)
binary file changed (absent → 393 bytes)
binary file changed (absent → 1175 bytes)
binary file changed (absent → 1183 bytes)
binary file changed (absent → 387 bytes)
binary file changed (absent → 781 bytes)
binary file changed (absent → 391 bytes)
binary file changed (absent → 400 bytes)
binary file changed (absent → 400 bytes)
binary file changed (absent → 404 bytes)
@@ -0,0 +1,1 @@+0.15.0
binary file changed (absent → 120 bytes)
binary file changed (absent → 95 bytes)
binary file changed (absent → 95 bytes)
binary file changed (absent → 310 bytes)
binary file changed (absent → 1075 bytes)
binary file changed (absent → 190 bytes)
binary file changed (absent → 541 bytes)
binary file changed (absent → 399 bytes)
binary file changed (absent → 214 bytes)
binary file changed (absent → 95 bytes)
binary file changed (absent → 95 bytes)
binary file changed (absent → 226 bytes)
binary file changed (absent → 95 bytes)
binary file changed (absent → 213 bytes)
binary file changed (absent → 95 bytes)
binary file changed (absent → 111 bytes)
binary file changed (absent → 324 bytes)
binary file changed (absent → 324 bytes)
binary file changed (absent → 665 bytes)
binary file changed (absent → 300 bytes)
binary file changed (absent → 95 bytes)
binary file changed (absent → 353 bytes)
binary file changed (absent → 95 bytes)
binary file changed (absent → 117 bytes)
binary file changed (absent → 113 bytes)
binary file changed (absent → 113 bytes)
binary file changed (absent → 214 bytes)
binary file changed (absent → 95 bytes)
binary file changed (absent → 115 bytes)
binary file changed (absent → 117 bytes)
binary file changed (absent → 95 bytes)
binary file changed (absent → 225 bytes)
binary file changed (absent → 558 bytes)
binary file changed (absent → 110 bytes)
binary file changed (absent → 95 bytes)
binary file changed (absent → 116 bytes)
binary file changed (absent → 113 bytes)
binary file changed (absent → 234 bytes)
binary file changed (absent → 223 bytes)
binary file changed (absent → 533 bytes)
binary file changed (absent → 120 bytes)
binary file changed (absent → 326 bytes)
binary file changed (absent → 190 bytes)
binary file changed (absent → 346 bytes)
binary file changed (absent → 212 bytes)
binary file changed (absent → 95 bytes)
@@ -0,0 +1,1 @@+0.15.0
@@ -0,0 +1,169 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}++-- | This module instantiates the general framework in+-- 'Data.Acid.StateMachineTest' with an acid-state component that+-- implements a simple key-value store.+module Data.Acid.KeyValueStateMachine (tests) where++import Control.DeepSeq+import Control.Exception+import Control.Monad.IO.Class ( MonadIO(..) )+import Control.Monad.Reader ( ask )+import Control.Monad.State ( get, put )+import Data.Acid+import Data.Acid.StateMachineTest+import Data.SafeCopy+import qualified Data.Map as Map+import GHC.Generics+import Hedgehog+import qualified Hedgehog.Gen as Gen+import qualified Hedgehog.Range as Range+++type Key = Int+type Value = String++data KeyValue = KeyValue !(Map.Map Key Value)+ deriving (Eq, Show)++$(deriveSafeCopy 0 'base ''KeyValue)++-- | Insert a key into the key-value store.+insertKey :: Key -> Value -> Update KeyValue ()+insertKey key value+ = do KeyValue m <- get+ put (KeyValue (Map.insert key value m))++-- | A slightly more complicated update transaction: reverse the value+-- at the given key, and return the resulting state. Crucially, this+-- is non-idempotent, unlike 'insertKey'.+reverseKey :: Key -> Update KeyValue KeyValue+reverseKey key+ = do KeyValue m <- get+ let r = KeyValue (Map.adjust reverse key m)+ put r+ return r++-- | An update that may fail: reverse the value at the given key, or+-- fail if it is missing.+reverseKeyOrFail :: Key -> Bomb -> Update KeyValue ()+reverseKeyOrFail key _+ = do KeyValue m <- get+ case Map.lookup key m of+ Nothing -> failUpdate "key not in map"+ Just val -> put (KeyValue (Map.insert key (reverse val) m))++-- | An update that attempts to put an undefined state. This transaction should+-- simply fail and not modify the state.+breakState :: Update KeyValue ()+breakState = put (throw (TransactionError "broken state"))++-- | An update that puts a partially-defined state. Unfortunately+-- acid-state does not handle this case gracefully, and will fail with+-- 'BlockedIndefinitelyOnMVar' (see #38). Thus this update is not+-- included in 'keyValueCommands' below.+breakState2 :: Update KeyValue ()+breakState2 = put (KeyValue (Map.singleton 1 (throw (TransactionError "broken state"))))++-- | Look up a key from the store.+lookupKey :: Key -> Query KeyValue (Maybe Value)+lookupKey key+ = do KeyValue m <- ask+ return (Map.lookup key m)++-- | Look up a key from the store, or fail if it is missing.+lookupKeyOrFail :: Key -> Bomb -> Query KeyValue Value+lookupKeyOrFail key _+ = do KeyValue m <- ask+ maybe (failQuery "key not in map") return (Map.lookup key m)++-- | Query the current value of the state. This is not used in the+-- generated commands, but is used for checking the state we get back+-- in 'prop_restore_old_state_1' etc.+askState :: Query KeyValue KeyValue+askState = ask++$(makeAcidic ''KeyValue ['insertKey, 'reverseKey, 'reverseKeyOrFail, 'breakState, 'breakState2, 'lookupKey, 'lookupKeyOrFail, 'askState])++deriving instance Generic InsertKey+deriving instance Generic ReverseKey+deriving instance Generic ReverseKeyOrFail+deriving instance Generic BreakState+deriving instance Generic LookupKey+deriving instance Generic LookupKeyOrFail++deriving instance Show InsertKey+deriving instance Show ReverseKey+deriving instance Show ReverseKeyOrFail+deriving instance Show BreakState+deriving instance Show LookupKey+deriving instance Show LookupKeyOrFail++instance NFData InsertKey+instance NFData ReverseKey+instance NFData ReverseKeyOrFail+instance NFData BreakState+instance NFData LookupKey+instance NFData LookupKeyOrFail++genKey :: Gen Key+genKey = Gen.int (Range.constant 1 10)++genValue :: Gen Value+genValue = Gen.string (Range.constant 0 10) Gen.alphaNum++keyValueCommands :: MonadIO m => [Command Gen m (Model KeyValue)]+keyValueCommands = [ acidUpdate (InsertKey <$> genKey <*> genValue)+ , acidUpdate (ReverseKey <$> genKey)+ , acidUpdateMayFail (ReverseKeyOrFail <$> genKey <*> genBomb)+ , acidUpdateMayFail (pure BreakState)+ , acidQuery (LookupKey <$> genKey)+ , acidQueryMayFail (LookupKeyOrFail <$> genKey <*> genBomb)+ ]++-- | Possible initial states; because of #20 we can currently only use+-- one of these when testing the properties.+initialStates :: [KeyValue]+initialStates = [ KeyValue Map.empty+ , KeyValue (Map.singleton 1 "foo")+ ]++prop_sequential :: Property+prop_sequential = acidStateSequentialProperty (acidStateInterface fp) (pure (head initialStates)) (Range.linear 1 10) keyValueCommands+ where+ fp = "state/KeyValueSequentialTest"++prop_parallel :: Property+prop_parallel = acidStateParallelProperty (acidStateInterface fp) (pure (head initialStates)) (Range.linear 1 10) (Range.linear 1 10) keyValueCommands+ where+ fp = "state/KeyValueParallelTest"++prop_restore_old_state_1 :: Property+prop_restore_old_state_1 = restoreOldStateProperty (acidStateInterface fp) (KeyValue Map.empty) AskState r+ where+ fp = "test-state/OldStateTest1"+ r = KeyValue (Map.fromList [(1,""),(2,""),(3,"y5Pl"),(4,""),(5,"Zc"),(6,"8aENKK")+ ,(7,"FDzyGCz"),(8,""),(9,"xq"),(10,"1Ra1obuINa")])++prop_restore_old_state_2 :: Property+prop_restore_old_state_2 = restoreOldStateProperty (acidStateInterface fp) (KeyValue Map.empty) AskState r+ where+ fp = "test-state/OldStateTest2"+ r = KeyValue (Map.fromList [(1,"PLwR1S6F"),(2,"0yrcVQM0c"),(3,"zAA"),(4,"prAocOc")+ ,(5,"HM"),(6,"ENdfLrrW"),(7,"sESXGsI"),(8,"AFa69uu5")+ ,(9,"XBvIQHX"),(10,"A2CzkvW")])++prop_restore_old_state_3 :: Property+prop_restore_old_state_3 = restoreOldStateProperty (acidStateInterface fp) (KeyValue Map.empty) AskState r+ where+ fp = "test-state/OldStateTest3"+ r = KeyValue (Map.fromList [(1,"4"),(2,"RQ1xEc5"),(3,"aT0Hqk"),(4,"Duf")+ ,(5,"tssCng7e0d"),(6,"uQW0hCVze"),(7,"FSCZPMGL")+ ,(8,"q1WI9He"),(9,"IYHbWmO"),(10,"lCErPJC3")])+++tests :: IO Bool+tests = checkParallel $$(discover)
@@ -0,0 +1,527 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}++-- | This module provides a general framework for state-machine testing of+-- acid-state components. It needs to be instantiated with a+-- particular acid-state component to get a concrete test.+module Data.Acid.StateMachineTest+ ( acidUpdate+ , acidUpdateMayFail+ , acidUpdateCheckFail+ , acidQuery+ , acidQueryMayFail+ , acidQueryCheckFail+ , acidStateSequentialProperty+ , acidStateParallelProperty+ , restoreOldStateProperty+ , acidStateInterface+ , AcidStateInterface(..)++ , Model(..)+ , open+ , close+ , checkpoint+ , checkpointClose+ , kill++ -- * Testing exceptions+ , TransactionError(..)+ , failQuery+ , failUpdate+ , Bomb(..)+ , explodeWHNF+ , explodeNF+ , genBomb+ ) where++import Control.DeepSeq+import Control.Exception (Exception, IOException, throw, catch, try, evaluate)+import Control.Monad.IO.Class ( MonadIO(..) )+import Control.Monad.State ( State, evalState, execState )+import qualified Data.Acid as Acid+import qualified Data.Acid.Common as Common+import qualified Data.Acid.Core as Core+import qualified Data.Acid.Local as Local+import Data.Maybe+import qualified Data.SafeCopy as SafeCopy+import Data.Typeable+import Hedgehog+import qualified Hedgehog.Gen as Gen+import System.Directory (removeDirectoryRecursive, removeFile)+import System.IO.Unsafe (unsafePerformIO)++-- | Exception to be thrown when a query or update fails.+--+-- At the moment the only way to implement failing transactions is by+-- throwing an exception from pure code. The state machine tests+-- assume that all failures use 'failQuery' or 'failUpdate', which+-- throw this exception. Thus we can catch this exception in cases+-- where transaction failures are accepted.+--+-- Even if acid-state provided a pure way to throw and catch errors in+-- the 'Update' and 'Query' monads, we would still need to test that+-- it correctly handles exceptions thrown during transactions.+data TransactionError = TransactionError String+ deriving Show+instance Exception TransactionError++-- | Cause a 'Query' to fail in a (possibly) expected manner. See+-- 'acidQueryMayFail' and 'acidQueryCheckFail'.+failQuery :: String -> Acid.Query s a+failQuery s = throw (TransactionError s)++-- | Cause an 'Update' to fail in a (possibly) expected manner. See+-- 'acidUpdateMayFail' and 'acidUpdateCheckFail'.+failUpdate :: String -> Acid.Update s a+failUpdate s = throw (TransactionError s)++-- | Container for exceptions thrown from pure code. This is+-- intended for use as an argument to transactions, for testing+-- transactions that cannot be serialised.+data Bomb = Bomb { unBomb :: Int }++instance NFData Bomb where+ rnf (Bomb n) = rnf n++-- | Slightly hacky 'Show' instance that will attempt to show+-- something sensible even if evaluating the 'Bomb' throws an+-- exception.+instance Show Bomb where+ show b = "(Bomb " ++ s ++ ")"+ where+ s = unsafePerformIO $ evaluate (show (unBomb b)) `catch` \ TransactionError{} -> return "(exploded)"++-- | Throw an exception when evaluated to WHNF.+explodeWHNF :: Bomb+explodeWHNF = throw (TransactionError "boom!")++-- | Throw an exception when evaluated to NF (but not when+-- evaluated to WHNF).+explodeNF :: Bomb+explodeNF = Bomb (throw (TransactionError "boom!"))++-- | Generate a bomb that may or may not explode (throw an exception)+-- when evaluated.+genBomb :: Gen Bomb+genBomb = Gen.element [Bomb 0, Bomb 1, Bomb 2, Bomb 3, explodeWHNF, explodeNF]++$(SafeCopy.deriveSafeCopy 0 'SafeCopy.base ''Bomb)+++-- | Model of an acid-state component, for state machine property+-- testing. We start in 'StateAbsent' and can transition to+-- 'StateOpen' by opening the state for the first time. Thereafter we+-- can transition between 'StateOpen' and 'StateClosed' by opening and+-- closing the state. Both of the latter keep track of the current+-- value of the state.+data Model s (v :: * -> *)+ = StateAbsent -- ^ State is not present on disk+ | StateClosed s -- ^ State is present on disk, but not in memory+ | StateOpen s (Var (Opaque (Acid.AcidState s)) v) -- ^ State is open in memory++-- | Return the handle to the acid-state component if it is open, or+-- return 'Nothing' if it is closed or absent.+modelHandle :: Model s v -> Maybe (Var (Opaque (Acid.AcidState s)) v)+modelHandle (StateOpen _ hdl) = Just hdl+modelHandle _ = Nothing++-- | Is the state currently open?+isOpen :: Model s v -> Bool+isOpen = isJust . modelHandle++-- | Return the current value of the state, if it is present.+modelValue :: Model s v -> Maybe s+modelValue (StateClosed s) = Just s+modelValue (StateOpen s _) = Just s+modelValue StateAbsent = Nothing+++-- | Description of the interface for performing+-- meta-operations on an acid-state, such as opening, closing and+-- checkpointing it. The testing code is abstracted over this+-- interface so that we can substitute alternate serialisation+-- backends.+data AcidStateInterface s =+ (Typeable s, Acid.IsAcidic s, Show s)+ => AcidStateInterface+ { openState :: s -> IO (Acid.AcidState s)+ , closeState :: Acid.AcidState s -> IO ()+ , checkpointState :: Acid.AcidState s -> IO ()+ , checkpointCloseState :: Acid.AcidState s -> IO ()+ , resetState :: IO ()+ , statePath :: FilePath+ }++-- | Standard implementation of the acid-state interface, using+-- 'SafeCopy'-based serialisation.+--+-- This takes the path to the state directory as an argument.+-- Warning: this path will be deleted/overwritten!+acidStateInterface :: (Acid.IsAcidic s, SafeCopy.SafeCopy s, Typeable s, Show s)+ => FilePath -> AcidStateInterface s+acidStateInterface fp =+ AcidStateInterface { openState = Acid.openLocalStateFrom fp+ , closeState = Acid.closeAcidState+ , checkpointState = Acid.createCheckpoint+ , checkpointCloseState = Local.createCheckpointAndClose+ , resetState = removeDirectoryRecursive fp+ `catch` (\ (_ :: IOException) -> return ())+ , statePath = fp+ }+++data Open s (v :: * -> *) = Open s+ deriving Show++#if MIN_VERSION_hedgehog(1,1,0)+instance FunctorB (Open s) where+ bmap _ (Open s) = Open s+instance TraversableB (Open s) where+ btraverse _ (Open s) = pure (Open s)+#else+instance HTraversable (Open s) where+ htraverse _ (Open s) = pure (Open s)+#endif++-- | Command to open the state, given the interface to use and a+-- generator for possible initial state values.+open :: MonadIO m => AcidStateInterface s -> Gen s -> Command Gen m (Model s)+open AcidStateInterface{..} gen_initial_state =+ Command gen execute [ Require require, Update update ]+ where+ require model _ = not (isOpen model)++ gen StateAbsent = Just (Open <$> gen_initial_state)+ gen StateClosed{} = Just (Open <$> gen_initial_state)+ gen StateOpen{} = Nothing++ execute (Open initial_state) = liftIO (Opaque <$> openState initial_state)++ update StateAbsent (Open s) hdl = StateOpen s hdl+ update (StateClosed s) (Open _) hdl = StateOpen s hdl+ update StateOpen{} _ _ = error "open: state already open!"+++data WithState s (v :: * -> *) = WithState String (Var (Opaque (Acid.AcidState s)) v)+ deriving (Show)++#if MIN_VERSION_hedgehog(1,1,0)+instance FunctorB (WithState s) where+ bmap k (WithState l v) = WithState l (bmap k v)+instance TraversableB (WithState s) where+ btraverse k (WithState l v) = WithState l <$> btraverse k v+#else+instance HTraversable (WithState s) where+ htraverse k (WithState l v) = WithState l <$> htraverse k v+#endif++genWithState :: Applicative g => String -> Model s v -> Maybe (g (WithState s v))+genWithState l model = pure . WithState l <$> modelHandle model++-- | Command to close the state.+close :: MonadIO m => AcidStateInterface s -> Command Gen m (Model s)+close AcidStateInterface{..} =+ Command (genWithState "Close") execute [ Require require, Update update ]+ where+ require model _ = isOpen model++ execute (WithState _ (Var (Concrete (Opaque st)))) = liftIO (closeState st)+ update (StateOpen s _) _ _ = StateClosed s+ update _ _ _ = error "close: not open"++-- | Command to take a checkpoint of the state.+checkpoint :: MonadIO m => AcidStateInterface s -> Command Gen m (Model s)+checkpoint AcidStateInterface{..} =+ Command (genWithState "checkpoint") execute [ Require require ]+ where+ require model _ = isOpen model+ execute (WithState _ (Var (Concrete (Opaque st)))) = liftIO (checkpointState st)++-- | Command to take a checkpoint and close the state, as a single atomic action.+checkpointClose :: MonadIO m => AcidStateInterface s -> Command Gen m (Model s)+checkpointClose AcidStateInterface{..} =+ Command (genWithState "checkpointClose") execute [ Require require, Update update ]+ where+ require model _ = isOpen model+ execute (WithState _ (Var (Concrete (Opaque st)))) = liftIO (checkpointCloseState st)+ update (StateOpen s _) _ _ = StateClosed s+ update _ _ _ = error "checkpointClose: not open"++-- | Command to simulate killing the process without closing the+-- state. This does not actually stop the old thread, though.+--+-- The lock file is removed so that the state can be reopened+-- (otherwise further commands would immediately fail).+kill :: MonadIO m => AcidStateInterface s -> Command Gen m (Model s)+kill AcidStateInterface{..} =+ Command (genWithState "kill") execute [ Require require, Update update ]+ where+ require model _ = isOpen model+ execute WithState{} = liftIO $ removeFile (statePath ++ "/open.lock")+ update (StateOpen s _) _ _ = StateClosed s+ update _ _ _ = error "kill: not open"++++data AcidCommand s e (v :: * -> *) = AcidCommand e (Var (Opaque (Acid.AcidState s)) v)+ deriving (Show)++#if MIN_VERSION_hedgehog(1,1,0)+instance FunctorB (AcidCommand s e) where+ bmap k (AcidCommand e s) = AcidCommand e (bmap k s)+instance TraversableB (AcidCommand s e) where+ btraverse k (AcidCommand e s) = AcidCommand e <$> btraverse k s+#else+instance HTraversable (AcidCommand s e) where+ htraverse k (AcidCommand e s) = AcidCommand e <$> htraverse k s+#endif++-- | Translate an acid-state update into a command that executes the+-- update, given a generator of inputs. If the update fails, the+-- property as a whole fails.+acidUpdate :: forall s e m .+ ( Acid.IsAcidic s+ , Acid.EventState e ~ s+ , Acid.UpdateEvent e+ , Show e+ , NFData e+ , Eq (Acid.EventResult e)+ , Show (Acid.EventResult e)+ , Typeable (Acid.EventResult e)+ , MonadIO m+ )+ => Gen e -> Command Gen m (Model s)+acidUpdate = acidUpdateCheckFail (\ _ _ _ _ -> failure)++-- | Translate an acid-state update into a command that executes the+-- update, given a generator of inputs. If the update fails, the+-- property as a whole succeeds.+acidUpdateMayFail :: forall s e m .+ ( Acid.IsAcidic s+ , Acid.EventState e ~ s+ , Acid.UpdateEvent e+ , Show e+ , NFData e+ , Eq (Acid.EventResult e)+ , Show (Acid.EventResult e)+ , Typeable (Acid.EventResult e)+ , MonadIO m+ )+ => Gen e -> Command Gen m (Model s)+acidUpdateMayFail = acidUpdateCheckFail (\ _ _ _ _ -> return ())++-- | Translate an acid-state update into a command that executes the+-- update, given a generator of inputs. If the update fails, the+-- given predicate is tested on the old and new states, the event and+-- the 'TransactionError' exception.+acidUpdateCheckFail :: forall s e m .+ ( Acid.IsAcidic s+ , Acid.EventState e ~ s+ , Acid.UpdateEvent e+ , Show e+ , NFData e+ , Eq (Acid.EventResult e)+ , Show (Acid.EventResult e)+ , Typeable (Acid.EventResult e)+ , MonadIO m+ )+ => (s -> s -> e -> TransactionError -> Test ())+ -> Gen e -> Command Gen m (Model s)+acidUpdateCheckFail allow_failure gen_event =+ Command gen execute [ Require require, Update update, Ensure ensure ]+ where+ -- Generate updates only when state is open+ gen :: Model s Symbolic -> Maybe (Gen (AcidCommand s e Symbolic))+ gen model = case modelHandle model of+ Just st -> Just (AcidCommand <$> gen_event <*> pure st)+ Nothing -> Nothing++ -- Execute a concrete update directly using acid-state+ execute :: AcidCommand s e Concrete -> m (Either TransactionError (Acid.EventResult e))+ execute (AcidCommand e (Var (Concrete (Opaque st)))) = liftIO (try (Acid.update st e))++ -- Shrinking updates requires the state to be open+ require :: Model s Symbolic -> AcidCommand s e Symbolic -> Bool+ require model _ = isOpen model++ -- Updates cause the model state to be updated. This needs+ -- 'unsafePerformIO' because the update function must be pure, but+ -- we need to deal with the possibility of the transaction+ -- throwing a 'TransactionError' exception, in which case the+ -- state of the model should not change. We need to deepseq the+ -- update event itself, in case it contains a nested error that+ -- will show up during serialisation but is not forced by+ -- executing the transaction.+ update :: Model s v+ -> AcidCommand s e v+ -> Var (Either TransactionError (Acid.EventResult e)) v+ -> Model s v+ update (StateOpen s hdl) (AcidCommand c _) _ =+ unsafePerformIO $ do+ s' <- evaluate (c `deepseq` execState (lookupMethod c) s)+ `catch` \ (_ :: TransactionError) -> return s+ return (StateOpen s' hdl)+ update _ _ _ = error "acidUpdate: state not open"++ -- Evaluating the update directly on the model value of the old+ -- state should give the same result+ ensure :: Model s Concrete+ -> Model s Concrete+ -> AcidCommand s e Concrete+ -> Either TransactionError (Acid.EventResult e)+ -> Test ()+ ensure model0 model1 (AcidCommand c _) (Left e) = case (modelValue model0, modelValue model1) of+ (Just v0, Just v1) -> allow_failure v0 v1 c e+ _ -> failure+ ensure model _ (AcidCommand c _) (Right o) = case modelValue model of+ Just v -> evalState (lookupMethod c) v === o+ Nothing -> failure++-- | Translate an acid-state query into a command that executes the+-- query, given a generator of inputs. If the query fails, the+-- property as a whole fails.+acidQuery :: forall s e m .+ ( Acid.IsAcidic s+ , Acid.EventState e ~ s+ , Acid.QueryEvent e+ , Show e+ , Eq (Acid.EventResult e)+ , Show (Acid.EventResult e)+ , Typeable (Acid.EventResult e)+ , MonadIO m+ , Eq s+ , Show s+ )+ => Gen e -> Command Gen m (Model s)+acidQuery = acidQueryCheckFail (\ _ _ _ -> failure)++-- | Translate an acid-state query into a command that executes the+-- query, given a generator of inputs. If the query fails, the+-- property as a whole succeeds.+acidQueryMayFail :: forall s e m .+ ( Acid.IsAcidic s+ , Acid.EventState e ~ s+ , Acid.QueryEvent e+ , Show e+ , Eq (Acid.EventResult e)+ , Show (Acid.EventResult e)+ , Typeable (Acid.EventResult e)+ , MonadIO m+ , Eq s+ , Show s+ )+ => Gen e -> Command Gen m (Model s)+acidQueryMayFail = acidQueryCheckFail (\ _ _ _ -> return ())++-- | Translate an acid-state query into a command that executes the+-- query, given a generator of inputs. If the query fails, the given+-- predicate is tested on the state, the event and the+-- 'TransactionError' exception.+acidQueryCheckFail :: forall s e m .+ ( Acid.IsAcidic s+ , Acid.EventState e ~ s+ , Acid.QueryEvent e+ , Show e+ , Eq (Acid.EventResult e)+ , Show (Acid.EventResult e)+ , Typeable (Acid.EventResult e)+ , MonadIO m+ , Eq s+ , Show s+ )+ => (s -> e -> TransactionError -> Test ()) -> Gen e -> Command Gen m (Model s)+acidQueryCheckFail allow_failure gen_event = Command gen execute+ [ Require require+ , Ensure unchanged_model+ , Ensure correct_output+ ]+ where+ -- Generate queries only when state is open+ gen model = case modelHandle model of+ Just st -> Just (AcidCommand <$> gen_event <*> pure st)+ Nothing -> Nothing++ -- Execute a concrete query directly using acid-state+ execute (AcidCommand e (Var (Concrete (Opaque st)))) =+ liftIO (try (evaluate =<< Acid.query st e))++ -- Shrinking queries requires the state to be open+ require model _ = isOpen model++ -- Queries should not change the value in the model+ unchanged_model model0 model1 _ _ = modelValue model0 === modelValue model1++ -- Evaluating the query directly on the model value of the old+ -- state should give the same result+ correct_output model _ (AcidCommand c _) r = case modelValue model of+ Just v -> case r of+ Right o -> evalState (lookupMethod c) v === o+ Left e -> allow_failure v c e+ Nothing -> failure++-- | Extract the underlying method implementation in the pure 'State'+-- monad for an acid-state query or update.+lookupMethod :: (Core.Method m, Acid.IsAcidic (Core.MethodState m))+ => m -> State (Core.MethodState m) (Core.MethodResult m)+lookupMethod m = Core.lookupHotMethod mmap m+ where+ mmap = Core.mkMethodMap (Common.eventsToMethods Common.acidEvents)+++-- | Test the sequential property (agreement between model and+-- implementation) for an acid-state component, given the interface, a+-- generator for initial values of the state, and a list of commands+-- built from 'acidQuery' and 'acidUpdate'. Additional commands will+-- be added to open and close the state.+--+-- Note that if the generator for initial values can return more than+-- one result, this will fail due to #20.+acidStateSequentialProperty :: AcidStateInterface s -> Gen s -> Range Int -> [Command Gen (TestT IO) (Model s)] -> Property+acidStateSequentialProperty i gen_initial_state range commands = property $ do+ actions <- forAll $ Gen.sequential range StateAbsent $+ [ open i gen_initial_state+ , close i+ , checkpoint i+ , checkpointClose i+ , kill i+ ] ++ commands ++ commands+ test $ do liftIO $ resetState i+ executeSequential StateAbsent actions++-- | Test the parallel property (absence of race conditions) for an+-- acid-state component, given the interface, a generator for initial+-- values of the state, and a list of commands built from 'acidQuery'+-- and 'acidUpdate'. Additional commands will be added to open and+-- close the state.+--+-- Note that if the generator for initial values can return more than+-- one result, this will fail due to #20.+--+-- The state cannot be opened twice in parallel, so the length of the+-- sequential prefix must be at least 1, so that the open command+-- happens exactly once.+acidStateParallelProperty :: AcidStateInterface s -> Gen s -> Range Int -> Range Int -> [Command Gen (TestT IO) (Model s)] -> Property+acidStateParallelProperty i gen_initial_state prefix_range parallel_range commands = property $ do+ actions <- forAll $ Gen.parallel prefix_range parallel_range StateAbsent $+ [ open i gen_initial_state+ , checkpoint i+ ] ++ commands ++ commands+ test $ do liftIO $ resetState i+ executeParallel StateAbsent actions++-- | Test that restoring an acid-state component (with the given initial+-- value), then executing the given query, results in the given+-- expected value. This is mostly useful to test that restoring from+-- files created by an older version of acid-state can still be read.+restoreOldStateProperty :: (Acid.EventState e ~ s, Acid.EventResult e ~ r, Acid.QueryEvent e, Eq r, Show r)+ => AcidStateInterface s -> s -> e -> r -> Property+restoreOldStateProperty i initial_state q expected_result = withTests 1 $ property $ test $ do+ liftIO $ removeFile (statePath i ++ "/open.lock") `catch` (\ (_ :: IOException) -> return ())+ st <- liftIO $ openState i initial_state+ r <- liftIO $ Acid.query st q+ r === expected_result
@@ -0,0 +1,165 @@+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE TemplateHaskell #-}++module Data.Acid.TemplateHaskellSpec where++import Test.Hspec hiding (context)++import Data.SafeCopy (SafeCopy)+import Control.DeepSeq (force)+import Control.Exception (evaluate)+import Language.Haskell.TH+import Language.Haskell.TH.Quote+import Control.Monad.Reader+import Control.Monad.State++import Data.Acid+import Data.Acid.TemplateHaskell++spec :: Spec+spec = do+ let name = mkName "foo"+ nameT = ConT name+ upperName = mkName "Foo"+ upperNameT = ConT upperName++ describe "makeEventInstance" $ do+ it "works with monomorphic types" $ do+ eventType <- runQ [t| Int -> Query Char () |]+ makeEventInstance name eventType+ `quoteShouldBe`+ [d| instance QueryEvent $(return upperNameT) |]++ it "requires instances on polymorphic types" $ do+ let a = VarT (mkName "a")+ a' = return a+ eventType <- runQ [t| (Ord $(a')) => $(a') -> Update Char $(a') |]++ makeEventInstance name eventType+ `quoteShouldBe`+ [d| instance (Ord $(a')) => UpdateEvent $(return upperNameT)+ |]+++ describe "analyseType" $ do+ it "can work with the Query type" $ do+ typ <- runQ [t| Int -> Query String Char |]++ analyseType name typ+ `shouldBe` TypeAnalysis+ { tyvars = []+ , context = []+ , argumentTypes = [ConT ''Int]+ , stateType = ConT ''String+ , resultType = ConT ''Char+ , isUpdate = False+ }++ it "can work with the Update type" $ do+ typ <- runQ [t| Int -> Update String Char |]++ analyseType name typ+ `shouldBe` TypeAnalysis+ { tyvars = []+ , context = []+ , argumentTypes = [ConT ''Int]+ , stateType = ConT ''String+ , resultType = ConT ''Char+ , isUpdate = True+ }++ it "can work with MonadReader" $ do+ typ <- runQ [t| forall m. (MonadReader Int m) => Int -> m () |]+ analyseType name typ+ `shouldBe` TypeAnalysis+ { tyvars = []+ , context = []+ , argumentTypes = [ConT ''Int]+ , stateType = ConT ''Int+ , resultType = TupleT 0+ , isUpdate = False+ }++ it "can work with MonadState" $ do+ typ <- runQ [t| forall m. (MonadState Int m) => Int -> m () |]+ analyseType name typ+ `shouldBe` TypeAnalysis+ { tyvars = []+ , context = []+ , argumentTypes = [ConT ''Int]+ , stateType = ConT ''Int+ , resultType = TupleT 0+ , isUpdate = True+ }++ it "can work with many type variables (note that eventCxts later rejects this)" $ do+ let m = mkName "m"+ typ <- runQ [t| (MonadReader Int $(varT m)) => Int -> Query Int ($(varT m) ()) |]+ analyseType name typ+ `shouldBe` TypeAnalysis+ { tyvars = []+ , context =+ [ ConT ''MonadReader+ `AppT` ConT ''Int+ `AppT` VarT m+ ]+ , argumentTypes = [ConT ''Int]+ , stateType = ConT ''Int+ , resultType = VarT m `AppT` TupleT 0+ , isUpdate = False+ }++ describe "eventCxts" $ do+ let binders = []+ stateType = ConT ''Char+ it "rejects types with constrainted type variables unknown to state" $ do+ let predicate eventType =+ evaluate+ . force+ . map show+ $ eventCxts stateType binders name eventType+ eventType <- runQ [t| forall a. (Ord a) => Int -> Query Char a |]++ predicate eventType+ `shouldThrow`+ anyErrorCall++ it "accepts types with unconstrained type variables" $ do+ eventType <- runQ [t| forall a. Int -> Query Char a |]++ eventCxts stateType binders name eventType+ `shouldBe`+ []+ let x = mkName "x"++ it "accepts constrained type variables in the state" $ do+ let binders :: [TyVarBndrUnit]+#if MIN_VERSION_template_haskell(2,17,0)+ binders = [PlainTV (mkName "x") ()]+#else+ binders = [PlainTV (mkName "x")]+#endif+ stateType = ConT ''Maybe `AppT` VarT x+ eventType <- runQ [t| forall a. (Ord a) => Int -> Query (Maybe a) Int|]++ eventCxts stateType binders name eventType+ `shouldBe`+ [ConT ''Ord `AppT` VarT x]++ it "can rename a polymorphic state" $ do+ eventType <- runQ [t| forall r m. (MonadReader r m, Ord r) => Int -> m Char |]+ eventCxts stateType binders name eventType+ `shouldBe`+ [ConT ''Ord `AppT` ConT ''Char]+++quoteShouldBe :: (Eq a, Show a) => Q a -> Q [a] -> Expectation+quoteShouldBe qa qb = do+ actual <- runQ qa+ [expected] <- runQ qb+ actual `shouldBe` expected
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
@@ -0,0 +1,9 @@+module Main (main) where++import Control.Monad (unless)+import Data.Acid.KeyValueStateMachine+import System.Exit (exitFailure)++main :: IO ()+main = do ok <- tests+ unless ok exitFailure