packages feed

haskoin-store 0.40.18 → 0.40.19

raw patch · 8 files changed

+957/−315 lines, 8 filesdep +ekg-coredep +ekg-statsddep +foldlPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: ekg-core, ekg-statsd, foldl, monad-control, stm

API changes (from Hackage documentation)

- Haskoin.Store: [cacheRetries] :: CacheConfig -> !Int
- Haskoin.Store: [storeConfCacheRetries] :: StoreConfig -> !Int
- Haskoin.Store.Cache: [cacheRetries] :: CacheConfig -> !Int
- Haskoin.Store.Manager: [storeConfCacheRetries] :: StoreConfig -> !Int
- Haskoin.Store.Web: instance (Control.Monad.IO.Unlift.MonadUnliftIO m, Control.Monad.Logger.MonadLoggerIO m) => Haskoin.Store.Common.StoreReadBase (Control.Monad.Trans.Reader.ReaderT Haskoin.Store.Web.WebConfig m)
- Haskoin.Store.Web: instance (Control.Monad.IO.Unlift.MonadUnliftIO m, Control.Monad.Logger.MonadLoggerIO m) => Haskoin.Store.Common.StoreReadExtra (Control.Monad.Trans.Reader.ReaderT Haskoin.Store.Web.WebConfig m)
+ Haskoin.Store: [cacheMetrics] :: CacheConfig -> !Maybe CacheMetrics
+ Haskoin.Store: [storeConfStats] :: StoreConfig -> !Maybe Store
+ Haskoin.Store.BlockStore: [blockConfStats] :: BlockStoreConfig -> !Maybe Store
+ Haskoin.Store.Cache: [cacheMetrics] :: CacheConfig -> !Maybe CacheMetrics
+ Haskoin.Store.Cache: data CacheMetrics
+ Haskoin.Store.Cache: newCacheMetrics :: MonadIO m => Store -> m CacheMetrics
+ Haskoin.Store.Manager: [storeConfStats] :: StoreConfig -> !Maybe Store
+ Haskoin.Store.Stats: StatEntry :: !Int64 -> !Int64 -> StatEntry
+ Haskoin.Store.Stats: [statCount] :: StatEntry -> !Int64
+ Haskoin.Store.Stats: [statValue] :: StatEntry -> !Int64
+ Haskoin.Store.Stats: addStatEntry :: MonadIO m => StatDist -> StatEntry -> m ()
+ Haskoin.Store.Stats: createStatDist :: MonadIO m => Text -> Store -> m StatDist
+ Haskoin.Store.Stats: data StatEntry
+ Haskoin.Store.Stats: type StatDist = TQueue StatEntry
+ Haskoin.Store.Stats: withStats :: MonadIO m => Text -> Int -> Text -> (Store -> m a) -> m a
+ Haskoin.Store.Web: [webStats] :: WebConfig -> !Maybe Store
+ Haskoin.Store.Web: instance (Control.Monad.IO.Unlift.MonadUnliftIO m, Control.Monad.Logger.MonadLoggerIO m) => Haskoin.Store.Common.StoreReadBase (Control.Monad.Trans.Reader.ReaderT Haskoin.Store.Web.WebState m)
+ Haskoin.Store.Web: instance (Control.Monad.IO.Unlift.MonadUnliftIO m, Control.Monad.Logger.MonadLoggerIO m) => Haskoin.Store.Common.StoreReadExtra (Control.Monad.Trans.Reader.ReaderT Haskoin.Store.Web.WebState m)
- Haskoin.Store: CacheConfig :: !Connection -> !Int -> !Integer -> !Chain -> !Int -> !Int -> !Int -> CacheConfig
+ Haskoin.Store: CacheConfig :: !Connection -> !Int -> !Integer -> !Chain -> !Int -> !Int -> !Maybe CacheMetrics -> CacheConfig
- Haskoin.Store: StoreConfig :: !Int -> ![HostPort] -> !Bool -> !FilePath -> !Network -> !Maybe String -> !Word32 -> !Word32 -> !Int -> !Integer -> !Bool -> !Bool -> !NominalDiffTime -> !NominalDiffTime -> !SockAddr -> WithConnection -> !Int -> !Int -> !Int -> StoreConfig
+ Haskoin.Store: StoreConfig :: !Int -> ![HostPort] -> !Bool -> !FilePath -> !Network -> !Maybe String -> !Word32 -> !Word32 -> !Int -> !Integer -> !Bool -> !Bool -> !NominalDiffTime -> !NominalDiffTime -> !SockAddr -> WithConnection -> !Int -> !Int -> !Maybe Store -> StoreConfig
- Haskoin.Store.BlockStore: BlockStoreConfig :: !PeerManager -> !Chain -> !Publisher StoreEvent -> !DatabaseReader -> !Network -> !Bool -> !Bool -> !NominalDiffTime -> BlockStoreConfig
+ Haskoin.Store.BlockStore: BlockStoreConfig :: !PeerManager -> !Chain -> !Publisher StoreEvent -> !DatabaseReader -> !Network -> !Bool -> !Bool -> !NominalDiffTime -> !Maybe Store -> BlockStoreConfig
- Haskoin.Store.Cache: CacheConfig :: !Connection -> !Int -> !Integer -> !Chain -> !Int -> !Int -> !Int -> CacheConfig
+ Haskoin.Store.Cache: CacheConfig :: !Connection -> !Int -> !Integer -> !Chain -> !Int -> !Int -> !Maybe CacheMetrics -> CacheConfig
- Haskoin.Store.Manager: StoreConfig :: !Int -> ![HostPort] -> !Bool -> !FilePath -> !Network -> !Maybe String -> !Word32 -> !Word32 -> !Int -> !Integer -> !Bool -> !Bool -> !NominalDiffTime -> !NominalDiffTime -> !SockAddr -> WithConnection -> !Int -> !Int -> !Int -> StoreConfig
+ Haskoin.Store.Manager: StoreConfig :: !Int -> ![HostPort] -> !Bool -> !FilePath -> !Network -> !Maybe String -> !Word32 -> !Word32 -> !Int -> !Integer -> !Bool -> !Bool -> !NominalDiffTime -> !NominalDiffTime -> !SockAddr -> WithConnection -> !Int -> !Int -> !Maybe Store -> StoreConfig
- Haskoin.Store.Web: WebConfig :: !String -> !Int -> !Store -> !Int -> !Int -> !WebLimits -> !WebTimeouts -> !String -> !Bool -> !Bool -> WebConfig
+ Haskoin.Store.Web: WebConfig :: !String -> !Int -> !Store -> !Int -> !Int -> !WebLimits -> !WebTimeouts -> !String -> !Bool -> !Bool -> !Maybe Store -> WebConfig

Files

app/Main.hs view
@@ -7,36 +7,41 @@ {-# LANGUAGE TupleSections     #-} module Main where -import           Control.Arrow           (second)-import           Control.Monad           (when)-import           Control.Monad.Logger    (LogLevel (..), filterLogger, logInfoS,-                                          runStderrLoggingT)-import           Data.Char               (toLower)-import           Data.Default            (Default (..))-import           Data.List               (intercalate)-import           Data.Maybe              (fromMaybe)-import           Data.String.Conversions (cs)-import           Data.Word               (Word32)-import           Haskoin                 (Network (..), allNets, bch,-                                          bchRegTest, bchTest, btc, btcRegTest,-                                          btcTest, eitherToMaybe)-import           Haskoin.Node            (withConnection)-import           Haskoin.Store           (StoreConfig (..), WebConfig (..),-                                          WebLimits (..), WebTimeouts (..),-                                          runWeb, withStore)-import           Options.Applicative     (Parser, auto, eitherReader,-                                          execParser, flag, fullDesc, header,-                                          help, helper, info, long, many,-                                          metavar, option, progDesc, short,-                                          showDefault, strOption, switch, value)-import           System.Exit             (exitSuccess)-import           System.FilePath         ((</>))-import           System.IO.Unsafe        (unsafePerformIO)-import           Text.Read               (readMaybe)-import           UnliftIO                (MonadIO)-import           UnliftIO.Directory      (createDirectoryIfMissing,-                                          getAppUserDataDirectory)-import           UnliftIO.Environment    (lookupEnv)+import           Control.Applicative       ((<|>))+import           Control.Arrow             (second)+import           Control.Monad             (when)+import           Control.Monad.Logger      (LogLevel (..), filterLogger,+                                            logInfoS, runStderrLoggingT)+import           Control.Monad.Trans.Maybe (MaybeT (..), runMaybeT)+import           Data.Char                 (toLower)+import           Data.Default              (Default (..))+import           Data.List                 (intercalate)+import           Data.Maybe                (fromMaybe)+import           Data.String.Conversions   (cs)+import qualified Data.Text                 as T+import           Data.Word                 (Word32)+import           Haskoin                   (Network (..), allNets, bch,+                                            bchRegTest, bchTest, btc,+                                            btcRegTest, btcTest, eitherToMaybe)+import           Haskoin.Node              (withConnection)+import           Haskoin.Store             (StoreConfig (..), WebConfig (..),+                                            WebLimits (..), WebTimeouts (..),+                                            runWeb, withStore)+import           Haskoin.Store.Stats       (withStats)+import           Options.Applicative       (Parser, auto, eitherReader,+                                            execParser, flag, fullDesc, header,+                                            help, helper, info, long, many,+                                            metavar, option, progDesc, short,+                                            showDefault, strOption, switch,+                                            value)+import           System.Exit               (exitSuccess)+import           System.FilePath           ((</>))+import           System.IO.Unsafe          (unsafePerformIO)+import           Text.Read                 (readMaybe)+import           UnliftIO                  (MonadIO)+import           UnliftIO.Directory        (createDirectoryIfMissing,+                                            getAppUserDataDirectory)+import           UnliftIO.Environment      (lookupEnv)  version :: String #ifdef CURRENT_PACKAGE_VERSION@@ -69,9 +74,12 @@     , configMaxPeers        :: !Int     , configMaxDiff         :: !Int     , configCacheRefresh    :: !Int-    , configCacheRetries    :: !Int-    , configCacheRetryDelay :: Int+    , configCacheRetryDelay :: !Int     , configNumTxId         :: !Bool+    , configStatsd          :: !Bool+    , configStatsdHost      :: !String+    , configStatsdPort      :: !Int+    , configStatsdPrefix    :: !String     }  instance Default Config where@@ -98,9 +106,12 @@                  , configMaxPeers        = defMaxPeers                  , configMaxDiff         = defMaxDiff                  , configCacheRefresh    = defCacheRefresh-                 , configCacheRetries    = defCacheRetries                  , configCacheRetryDelay = defCacheRetryDelay                  , configNumTxId         = defNumTxId+                 , configStatsd          = defStatsd+                 , configStatsdHost      = defStatsdHost+                 , configStatsdPort      = defStatsdPort+                 , configStatsdPrefix    = defStatsdPrefix                  }  defEnv :: MonadIO m => String -> a -> (String -> Maybe a) -> m a@@ -113,11 +124,6 @@     defEnv "CACHE_REFRESH" 750 readMaybe {-# NOINLINE defCacheRefresh #-} -defCacheRetries :: Int-defCacheRetries = unsafePerformIO $-    defEnv "CACHE_RETRIES" 100 readMaybe-{-# NOINLINE defCacheRetries #-}- defCacheRetryDelay :: Int defCacheRetryDelay = unsafePerformIO $     defEnv "CACHE_RETRY_DELAY" 100000 readMaybe@@ -250,6 +256,36 @@     defEnv "MAX_DIFF" 2 readMaybe {-# NOINLINE defMaxDiff #-} +defStatsd :: Bool+defStatsd = unsafePerformIO $+    defEnv "STATSD" False parseBool+{-# NOINLINE defStatsd #-}++defStatsdHost :: String+defStatsdHost = unsafePerformIO $+    defEnv "STATSD_HOST" "localhost" pure+{-# NOINLINE defStatsdHost #-}++defStatsdPort :: Int+defStatsdPort = unsafePerformIO $+    defEnv "STATSD_PORT" 8125 readMaybe+{-# NOINLINE defStatsdPort #-}++defStatsdPrefix :: String+defStatsdPrefix =+    unsafePerformIO $+    runMaybeT go >>= \case+        Nothing -> return "haskoin_store"+        Just x -> return x+  where+    go = prefix <|> nomad+    prefix = MaybeT $ lookupEnv "STATSD_PREFIX"+    nomad = do+        task <- MaybeT $ lookupEnv "NOMAD_TASK_NAME"+        service <- MaybeT $ lookupEnv "NOMAD_ALLOC_INDEX"+        return $ task <> "." <> service+{-# NOINLINE defStatsdPrefix #-}+ netNames :: String netNames = intercalate "|" (map getNetworkName allNets) @@ -439,13 +475,6 @@         <> help "Refresh cache this frequently"         <> showDefault         <> value (configCacheRefresh def)-    configCacheRetries <--        option auto $-        metavar "INT"-        <> long "cache-retries"-        <> help "Retry getting cache lock to index xpub"-        <> showDefault-        <> value (configCacheRetries def)     configCacheRetryDelay <-         option auto $         metavar "MICROSECONDS"@@ -472,6 +501,31 @@         flag (configNumTxId def) True $         long "numtxid"         <> help "Numeric tx_index field"+    configStatsd <-+        flag (configStatsd def) True $+        long "statsd"+        <> help "Enable statsd metrics"+    configStatsdHost <-+        strOption $+        metavar "HOST"+        <> long "statsd-host"+        <> help "Host to send statsd metrics"+        <> showDefault+        <> value (configStatsdHost def)+    configStatsdPort <-+        option auto $+        metavar "PORT"+        <> long "statsd-port"+        <> help "Port to send statsd metrics"+        <> showDefault+        <> value (configStatsdPort def)+    configStatsdPrefix <-+        strOption $+        metavar "PREFIX"+        <> long "statsd-prefix"+        <> help "Prefix for statsd metrics"+        <> showDefault+        <> value (configStatsdPrefix def)     pure         Config             { configWebLimits = WebLimits {..}@@ -543,11 +597,14 @@            , configMaxDiff = maxdiff            , configNoMempool = nomem            , configCacheRefresh = crefresh-           , configCacheRetries = cretries            , configCacheRetryDelay = cretrydelay            , configNumTxId = numtxid+           , configStatsd = statsd+           , configStatsdHost = statsdhost+           , configStatsdPort = statsdport+           , configStatsdPrefix = statsdpfx            } =-    runStderrLoggingT . filterLogger l $ do+    runStderrLoggingT . filterLogger l . with_stats $ \stats -> do         $(logInfoS) "Main" $             "Creating working directory if not found: " <> cs wd         createDirectoryIfMissing True wd@@ -573,8 +630,8 @@                     , storeConfPeerMaxLife = fromIntegral peerlife                     , storeConfConnect = withConnection                     , storeConfCacheRefresh = crefresh-                    , storeConfCacheRetries = cretries                     , storeConfCacheRetryDelay = cretrydelay+                    , storeConfStats = stats                     }         withStore scfg $ \st ->             runWeb@@ -589,8 +646,21 @@                     , webMaxDiff = maxdiff                     , webNoMempool = nomem                     , webNumTxId = numtxid+                    , webStats = stats                     }   where+    with_stats go+        | statsd = do+            $(logInfoS) "Main" $+                "Sending stats to " <> T.pack statsdhost <>+                ":" <> cs (show statsdport) <>+                " with prefix: " <> T.pack statsdpfx+            withStats+                (T.pack statsdhost)+                statsdport+                (T.pack statsdpfx)+                (go . Just)+        | otherwise = go Nothing     net' | asert == 0 = net          | otherwise = net { getAsertActivationTime = Just asert }     l _ lvl
haskoin-store.cabal view
@@ -1,11 +1,13 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.34.2.+-- This file has been generated from package.yaml by hpack version 0.33.0. -- -- see: https://github.com/sol/hpack+--+-- hash: 46a8bc9a10e9bbf0faff3e73de4e05ef0a483024f92087f321ee3be8c67f053e  name:           haskoin-store-version:        0.40.18+version:        0.40.19 synopsis:       Storage and index for Bitcoin and Bitcoin Cash description:    Please see the README on GitHub at <https://github.com/haskoin/haskoin-store#readme> category:       Bitcoin, Finance, Network@@ -32,6 +34,7 @@       Haskoin.Store.Database.Writer       Haskoin.Store.Logic       Haskoin.Store.Manager+      Haskoin.Store.Stats       Haskoin.Store.Web   other-modules:       Paths_haskoin_store@@ -47,6 +50,9 @@     , containers >=0.6.2.1     , data-default >=0.7.1.1     , deepseq >=1.4.4.0+    , ekg-core >=0.1.1.7+    , ekg-statsd >=0.2.5+    , foldl >=1.4.10     , hashable >=1.3.0.0     , haskoin-core >=0.19.0     , haskoin-node >=0.17.0@@ -54,6 +60,7 @@     , hedis >=0.12.13     , http-types >=0.12.3     , lens >=4.18.1+    , monad-control >=1.0.2.3     , monad-logger >=0.3.32     , mtl >=2.2.2     , network >=3.1.1.1@@ -62,6 +69,7 @@     , rocksdb-haskell-jprupp >=2.1.3     , rocksdb-query >=0.4.0     , scotty >=0.11.5+    , stm >=2.5.0.0     , string-conversions >=0.4.0.1     , text >=1.2.3.0     , time >=1.9.3@@ -90,7 +98,10 @@     , containers >=0.6.2.1     , data-default >=0.7.1.1     , deepseq >=1.4.4.0+    , ekg-core >=0.1.1.7+    , ekg-statsd >=0.2.5     , filepath+    , foldl >=1.4.10     , hashable >=1.3.0.0     , haskoin-core >=0.19.0     , haskoin-node >=0.17.0@@ -99,6 +110,7 @@     , hedis >=0.12.13     , http-types >=0.12.3     , lens >=4.18.1+    , monad-control >=1.0.2.3     , monad-logger >=0.3.32     , mtl >=2.2.2     , network >=3.1.1.1@@ -108,6 +120,7 @@     , rocksdb-haskell-jprupp >=2.1.3     , rocksdb-query >=0.4.0     , scotty >=0.11.5+    , stm >=2.5.0.0     , string-conversions >=0.4.0.1     , text >=1.2.3.0     , time >=1.9.3@@ -141,6 +154,9 @@     , containers >=0.6.2.1     , data-default >=0.7.1.1     , deepseq >=1.4.4.0+    , ekg-core >=0.1.1.7+    , ekg-statsd >=0.2.5+    , foldl >=1.4.10     , hashable >=1.3.0.0     , haskoin-core >=0.19.0     , haskoin-node >=0.17.0@@ -150,6 +166,7 @@     , hspec >=2.7.1     , http-types >=0.12.3     , lens >=4.18.1+    , monad-control >=1.0.2.3     , monad-logger >=0.3.32     , mtl >=2.2.2     , network >=3.1.1.1@@ -158,6 +175,7 @@     , rocksdb-haskell-jprupp >=2.1.3     , rocksdb-query >=0.4.0     , scotty >=0.11.5+    , stm >=2.5.0.0     , string-conversions >=0.4.0.1     , text >=1.2.3.0     , time >=1.9.3
src/Haskoin/Store/BlockStore.hs view
@@ -82,10 +82,14 @@                                                 getOldMempool, importBlock,                                                 initBest, newMempoolTx,                                                 revertBlock)+import           Haskoin.Store.Stats import           NQE                           (Listen, Mailbox, Publisher,                                                 inboxToMailbox, newInbox,                                                 publish, query, receive, send,                                                 sendSTM)+import qualified System.Metrics                as Metrics+import qualified System.Metrics.Gauge          as Metrics (Gauge)+import qualified System.Metrics.Gauge          as Metrics.Gauge import           System.Random                 (randomRIO) import           UnliftIO                      (Exception, MonadIO,                                                 MonadUnliftIO, STM, TVar, async,@@ -136,8 +140,73 @@         , myPeer    :: !(TVar (Maybe Syncing))         , myTxs     :: !(TVar (HashMap TxHash PendingTx))         , requested :: !(TVar (HashSet TxHash))+        , myMetrics :: !(Maybe StoreMetrics)         } +data StoreMetrics = StoreMetrics+    { storeHeight         :: !Metrics.Gauge+    , headersHeight       :: !Metrics.Gauge+    , storePendingTxs     :: !Metrics.Gauge+    , storePeersConnected :: !Metrics.Gauge+    , storeMempoolSize    :: !Metrics.Gauge+    }++newStoreMetrics :: MonadIO m => Metrics.Store -> m StoreMetrics+newStoreMetrics s = liftIO $ do+    storeHeight             <- g "height"+    headersHeight           <- g "headers"+    storePendingTxs         <- g "pending_txs"+    storePeersConnected     <- g "peers_connected"+    storeMempoolSize        <- g "mempool_size"+    return StoreMetrics{..}+  where+    g x = Metrics.createGauge   ("store." <> x) s++setStoreHeight :: MonadIO m => BlockT m ()+setStoreHeight =+    asks myMetrics >>= \case+    Nothing -> return ()+    Just m ->+        getBestBlock >>= \case+        Nothing -> setit m 0+        Just bb -> getBlock bb >>= \case+            Nothing -> setit m 0+            Just b -> setit m (blockDataHeight b)+  where+    setit m i = liftIO $ storeHeight m `Metrics.Gauge.set` fromIntegral i++setHeadersHeight :: MonadIO m => BlockT m ()+setHeadersHeight =+    asks myMetrics >>= \case+    Nothing -> return ()+    Just m -> do+        h <- fmap nodeHeight $ chainGetBest =<< asks (blockConfChain . myConfig)+        liftIO $ headersHeight m `Metrics.Gauge.set` fromIntegral h++setPendingTxs :: MonadIO m => BlockT m ()+setPendingTxs =+    asks myMetrics >>= \case+    Nothing -> return ()+    Just m -> do+        s <- asks myTxs >>= \t -> atomically (HashMap.size <$> readTVar t)+        liftIO $ storePendingTxs m `Metrics.Gauge.set` fromIntegral s++setPeersConnected :: MonadIO m => BlockT m ()+setPeersConnected =+    asks myMetrics >>= \case+    Nothing -> return ()+    Just m -> do+        ps <- fmap length $ getPeers =<< asks (blockConfManager . myConfig)+        liftIO $ storePeersConnected m `Metrics.Gauge.set` fromIntegral ps++setMempoolSize :: MonadIO m => BlockT m ()+setMempoolSize =+    asks myMetrics >>= \case+    Nothing -> return ()+    Just m -> do+        s <- length <$> getMempool+        liftIO $ storeMempoolSize m `Metrics.Gauge.set` fromIntegral s+ -- | Configuration for a block store. data BlockStoreConfig =     BlockStoreConfig@@ -157,6 +226,7 @@         -- ^ wipe mempool at start         , blockConfPeerTimeout :: !NominalDiffTime         -- ^ disconnect syncing peer if inactive for this long+        , blockConfStats       :: !(Maybe Metrics.Store)         }  type BlockT m = ReaderT BlockStore m@@ -216,11 +286,13 @@     ts <- newTVarIO HashMap.empty     rq <- newTVarIO HashSet.empty     inbox <- newInbox+    metrics <- mapM newStoreMetrics (blockConfStats cfg)     let r = BlockStore { myMailbox = inboxToMailbox inbox                        , myConfig = cfg                        , myPeer = pb                        , myTxs = ts                        , requested = rq+                       , myMetrics = metrics                        }     withAsync (runReaderT (go inbox) r) $ \a -> do         link a@@ -279,7 +351,6 @@  mempool :: (MonadUnliftIO m, MonadLoggerIO m) => Peer -> BlockT m () mempool p = guardMempool $ void $ async $ do-    threadDelay 23849118     isInSync >>= \s -> when s $ do         $(logDebugS) "BlockStore" $             "Requesting mempool from peer: " <> peerText p@@ -310,7 +381,7 @@         "Processing block: " <> blockText node Nothing         <> " from peer: " <> peerText peer     lift $ runImport (importBlock block node) >>= \case-        Left e -> failure e+        Left e   -> failure e         Right () -> success node   where     header = blockHeader block@@ -416,6 +487,8 @@     atomically $ do         modifyTVar ts $ HashMap.insert th p         modifyTVar rq $ HashSet.delete th+        HashMap.size <$> readTVar ts+    setPendingTxs   where     th = txHash (pendingTx p) @@ -438,10 +511,13 @@               || th `HashSet.member` rs  pendingTxs :: MonadIO m => Int -> BlockT m [PendingTx]-pendingTxs i = asks myTxs >>= \box -> atomically $ do-    pending <- readTVar box-    let (selected, rest) = select pending-    writeTVar box rest+pendingTxs i = do+    selected <- asks myTxs >>= \box -> atomically $ do+        pending <- readTVar box+        let (selected, rest) = select pending+        writeTVar box rest+        return (selected)+    setPendingTxs     return selected   where     select pend =@@ -560,7 +636,7 @@             block_read             (pendingTxTime p)             (pendingTx p) >>= \case-                True -> return $ Just (txHash (pendingTx p))+                True  -> return $ Just (txHash (pendingTx p))                 False -> return Nothing     import_txs block_read txs =         let r = mapM (run_import block_read) txs@@ -795,7 +871,7 @@     box <- asks myPeer     readTVarIO box >>= \case         Just Syncing { syncingPeer = p' } | p == p' -> reset_it box-        _ -> return ()+        _                                           -> return ()   where     reset_it box = do         atomically $ writeTVar box Nothing@@ -805,7 +881,7 @@ trySetPeer :: MonadLoggerIO m => Peer -> BlockT m Bool trySetPeer p =     getSyncingState >>= \case-        Just _ -> return False+        Just _  -> return False         Nothing -> set_it   where     set_it =@@ -828,14 +904,14 @@     isInSync >>= \case         True -> return ()         False -> getSyncingState >>= \case-            Just _ -> return ()+            Just _  -> return ()             Nothing -> online_peer   where     recurse [] = return ()     recurse (p : ps) =         trySetPeer p >>= \case             False -> recurse ps-            True -> syncMe+            True  -> syncMe     online_peer = do         ops <- getPeers =<< asks (blockConfManager . myConfig)         let ps = map onlinePeerMailbox ops@@ -847,7 +923,7 @@         True -> mempool p         False -> trySetPeer p >>= \case             False -> return ()-            True -> syncMe+            True  -> syncMe  getSyncingState     :: (MonadIO m, MonadReader BlockStore m) => m (Maybe Syncing)@@ -858,7 +934,7 @@     :: (MonadLoggerIO m, MonadReader BlockStore m) => m () clearSyncingState =     asks myPeer >>= readTVarIO >>= \case-        Nothing -> return ()+        Nothing                          -> return ()         Just Syncing { syncingPeer = p } -> finishPeer p  processBlockStoreMessage :: (MonadUnliftIO m, MonadLoggerIO m)@@ -891,6 +967,11 @@     pruneOrphans     checkTime     pruneMempool+    setStoreHeight+    setHeadersHeight+    setPendingTxs+    setPeersConnected+    setMempoolSize     atomically (r ())  pingMe :: MonadLoggerIO m => Mailbox BlockStoreMessage -> m ()
src/Haskoin/Store/Cache.hs view
@@ -6,12 +6,15 @@ {-# LANGUAGE LambdaCase        #-} {-# LANGUAGE MultiWayIf        #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards   #-} {-# LANGUAGE TemplateHaskell   #-} {-# LANGUAGE TupleSections     #-} module Haskoin.Store.Cache     ( CacheConfig(..)+    , CacheMetrics     , CacheT     , CacheError(..)+    , newCacheMetrics     , withCache     , connectRedis     , blockRefScore@@ -24,71 +27,86 @@     , evictFromCache     ) where -import           Control.DeepSeq           (NFData)-import           Control.Monad             (forM, forM_, forever, guard, unless,-                                            void, when)-import           Control.Monad.Logger      (MonadLoggerIO, logDebugS, logErrorS,-                                            logInfoS, logWarnS)-import           Control.Monad.Reader      (ReaderT (..), ask, asks)-import           Control.Monad.Trans       (lift)-import           Control.Monad.Trans.Maybe (MaybeT (..), runMaybeT)-import           Data.Bits                 (shift, (.&.), (.|.))-import           Data.ByteString           (ByteString)-import qualified Data.ByteString.Short     as BSS-import           Data.Default              (def)-import           Data.Either               (rights)-import           Data.HashMap.Strict       (HashMap)-import qualified Data.HashMap.Strict       as HashMap-import qualified Data.HashSet              as HashSet-import qualified Data.IntMap.Strict        as I-import           Data.List                 (sort)-import qualified Data.Map.Strict           as Map-import           Data.Maybe                (catMaybes, isNothing, mapMaybe)-import           Data.Serialize            (Serialize, decode, encode)-import           Data.String.Conversions   (cs)-import           Data.Text                 (Text)-import           Data.Time.Clock.System    (getSystemTime, systemSeconds)-import           Data.Word                 (Word32, Word64)-import           Database.Redis            (Connection, Redis, RedisCtx, Reply,-                                            checkedConnect, defaultConnectInfo,-                                            hgetall, parseConnectInfo, zadd,-                                            zrangeWithscores,-                                            zrangebyscoreWithscoresLimit, zrem)-import qualified Database.Redis            as Redis-import           GHC.Generics              (Generic)-import           Haskoin                   (Address, BlockHash,-                                            BlockHeader (..), BlockNode (..),-                                            DerivPathI (..), KeyIndex,-                                            OutPoint (..), Tx (..), TxHash,-                                            TxIn (..), TxOut (..), XPubKey,-                                            blockHashToHex, derivePubPath,-                                            eitherToMaybe, headerHash,-                                            pathToList, scriptToAddressBS,-                                            txHash, xPubAddr,-                                            xPubCompatWitnessAddr, xPubExport,-                                            xPubWitnessAddr)-import           Haskoin.Node              (Chain, chainBlockMain,-                                            chainGetAncestor, chainGetBest,-                                            chainGetBlock)+import           Control.DeepSeq             (NFData)+import           Control.Monad               (forM, forM_, forever, guard,+                                              unless, void, when)+import           Control.Monad.Logger        (MonadLoggerIO, logDebugS,+                                              logErrorS, logInfoS, logWarnS)+import           Control.Monad.Reader        (ReaderT (..), ask, asks)+import           Control.Monad.Trans         (lift)+import           Control.Monad.Trans.Maybe   (MaybeT (..), runMaybeT)+import           Data.Bits                   (shift, (.&.), (.|.))+import           Data.ByteString             (ByteString)+import qualified Data.ByteString.Short       as BSS+import           Data.Default                (def)+import           Data.Either                 (rights)+import           Data.HashMap.Strict         (HashMap)+import qualified Data.HashMap.Strict         as HashMap+import           Data.HashSet                (HashSet)+import qualified Data.HashSet                as HashSet+import qualified Data.IntMap.Strict          as I+import           Data.List                   (sort)+import qualified Data.Map.Strict             as Map+import           Data.Maybe                  (catMaybes, isNothing, mapMaybe)+import           Data.Serialize              (Serialize, decode, encode)+import           Data.String.Conversions     (cs)+import           Data.Text                   (Text)+import           Data.Time.Clock             (NominalDiffTime, diffUTCTime)+import           Data.Time.Clock.System      (getSystemTime, systemSeconds,+                                              systemToUTCTime)+import           Data.Word                   (Word32, Word64)+import           Database.Redis              (Connection, Redis, RedisCtx,+                                              Reply, checkedConnect,+                                              defaultConnectInfo, hgetall,+                                              parseConnectInfo, zadd,+                                              zrangeWithscores,+                                              zrangebyscoreWithscoresLimit,+                                              zrem)+import qualified Database.Redis              as Redis+import           GHC.Generics                (Generic)+import           Haskoin                     (Address, BlockHash,+                                              BlockHeader (..), BlockNode (..),+                                              DerivPathI (..), KeyIndex,+                                              OutPoint (..), Tx (..), TxHash,+                                              TxIn (..), TxOut (..), XPubKey,+                                              blockHashToHex, derivePubPath,+                                              eitherToMaybe, headerHash,+                                              pathToList, scriptToAddressBS,+                                              txHash, xPubAddr,+                                              xPubCompatWitnessAddr, xPubExport,+                                              xPubWitnessAddr)+import           Haskoin.Node                (Chain, chainBlockMain,+                                              chainGetAncestor, chainGetBest,+                                              chainGetBlock) import           Haskoin.Store.Common import           Haskoin.Store.Data-import           NQE                       (Inbox, Listen, Mailbox,-                                            inboxToMailbox, query, receive,-                                            send)-import           System.Random             (randomIO, randomRIO)-import           UnliftIO                  (Exception, MonadIO, MonadUnliftIO,-                                            async, atomically, bracket, liftIO,-                                            link, throwIO, withAsync)-import           UnliftIO.Concurrent       (threadDelay)+import           Haskoin.Store.Stats+import           NQE                         (Inbox, Listen, Mailbox,+                                              inboxToMailbox, query, receive,+                                              send)+import qualified System.Metrics              as Metrics+import qualified System.Metrics.Counter      as Metrics (Counter)+import qualified System.Metrics.Counter      as Metrics.Counter+import qualified System.Metrics.Distribution as Metrics (Distribution)+import qualified System.Metrics.Distribution as Metrics.Distribution+import qualified System.Metrics.Gauge        as Metrics (Gauge)+import qualified System.Metrics.Gauge        as Metrics.Gauge+import           System.Random               (randomIO, randomRIO)+import           UnliftIO                    (Exception, MonadIO, MonadUnliftIO,+                                              TQueue, TVar, async, atomically,+                                              bracket, liftIO, link, readTQueue,+                                              readTVar, throwIO, withAsync,+                                              writeTQueue, writeTVar)+import           UnliftIO.Concurrent         (threadDelay)  runRedis :: MonadLoggerIO m => Redis (Either Reply a) -> CacheX m a runRedis action =     asks cacheConn >>= \conn ->     liftIO (Redis.runRedis conn action) >>= \case-    Right x -> return x-    Left e -> do-        $(logErrorS) "Cache" $ "Got error from Redis: " <> cs (show e)-        throwIO (RedisError e)+        Right x -> return x+        Left e -> do+            $(logErrorS) "Cache" $ "Got error from Redis: " <> cs (show e)+            throwIO (RedisError e)  data CacheConfig = CacheConfig     { cacheConn       :: !Connection@@ -96,10 +114,56 @@     , cacheMax        :: !Integer     , cacheChain      :: !Chain     , cacheRefresh    :: !Int -- millisenconds-    , cacheRetries    :: !Int     , cacheRetryDelay :: !Int -- microseconds+    , cacheMetrics    :: !(Maybe CacheMetrics)     } +data CacheMetrics = CacheMetrics+    { cacheHits      :: !Metrics.Counter+    , cacheMisses    :: !Metrics.Counter+    , cacheIgnore    :: !Metrics.Counter+    , cacheRefreshes :: !Metrics.Counter+    , cacheIndexTime :: !StatDist+    }++newCacheMetrics :: MonadIO m => Metrics.Store -> m CacheMetrics+newCacheMetrics s = liftIO $ do+    cacheHits            <- c "hits"+    cacheMisses          <- c "misses"+    cacheIgnore          <- c "ignore"+    cacheRefreshes       <- c "refreshes"+    cacheIndexTime       <- d "index_time_ms"+    return CacheMetrics{..}+  where+    c x = Metrics.createCounter ("cache." <> x) s+    d x = createStatDist        ("cache." <> x) s++withMetrics :: MonadUnliftIO m+            => (CacheMetrics -> StatDist)+            -> CacheX m a+            -> CacheX m a+withMetrics df go =+    asks cacheMetrics >>= \case+        Nothing -> go+        Just m ->+            bracket+            (systemToUTCTime <$> liftIO getSystemTime)+            (end m)+            (const go)+  where+    end metrics t1 = do+        t2 <- systemToUTCTime <$> liftIO getSystemTime+        let diff = round $ diffUTCTime t2 t1 * 1000+        df metrics `addStatEntry` StatEntry diff 1++incrementCounter :: MonadIO m+                 => (CacheMetrics -> Metrics.Counter)+                 -> CacheX m ()+incrementCounter cf =+    asks cacheMetrics >>= \case+        Nothing -> return ()+        Just m  -> liftIO $ Metrics.Counter.inc (cf m)+ type CacheT = ReaderT (Maybe CacheConfig) type CacheX = ReaderT CacheConfig @@ -139,15 +203,15 @@     getAddressesUnspents addrs = lift . getAddressesUnspents addrs     xPubBals xpub =         ask >>= \case-            Nothing -> lift (xPubBals xpub)+            Nothing  -> lift (xPubBals xpub)             Just cfg -> lift (runReaderT (getXPubBalances xpub) cfg)     xPubUnspents xpub limits =         ask >>= \case-            Nothing -> lift (xPubUnspents xpub limits)+            Nothing  -> lift (xPubUnspents xpub limits)             Just cfg -> lift (runReaderT (getXPubUnspents xpub limits) cfg)     xPubTxs xpub limits =         ask >>= \case-            Nothing -> lift (xPubTxs xpub limits)+            Nothing  -> lift (xPubTxs xpub limits)             Just cfg -> lift (runReaderT (getXPubTxs xpub limits) cfg)     getMaxGap = lift getMaxGap     getInitialGap = lift getInitialGap@@ -171,6 +235,7 @@     xpubtxt <- xpubText xpub     isXPubCached xpub >>= \case         True -> do+            incrementCounter cacheHits             txs <- cacheGetXPubTxs xpub limits             return txs         False -> do@@ -185,6 +250,7 @@     xpubtxt <- xpubText xpub     isXPubCached xpub >>= \case         True -> do+            incrementCounter cacheHits             bals <- cacheGetXPubBalances xpub             process bals         False -> do@@ -221,8 +287,8 @@     xpubtxt <- xpubText xpub     isXPubCached xpub >>= \case         True -> do-            bals <- cacheGetXPubBalances xpub-            return bals+            incrementCounter cacheHits+            cacheGetXPubBalances xpub         False -> do             bals <- lift $ xPubBals xpub             newXPubC xpub bals@@ -231,7 +297,7 @@ isInCache :: MonadLoggerIO m => XPubSpec -> CacheT m Bool isInCache xpub =     ask >>= \case-        Nothing -> return False+        Nothing  -> return False         Just cfg -> runReaderT (isXPubCached xpub) cfg  isXPubCached :: MonadLoggerIO m => XPubSpec -> CacheX m Bool@@ -383,7 +449,7 @@  data CacheWriterMessage     = CacheNewBlock-    | CachePing (Listen ())+    | CachePing !(Listen ())  type CacheWriterInbox = Inbox CacheWriterMessage type CacheWriter = Mailbox CacheWriterMessage@@ -489,7 +555,7 @@     -> CacheX m (Maybe a) withLock f =     bracket lockIt unlockIt $ \case-        Just _ -> Just <$> f+        Just _  -> Just <$> f         Nothing -> return Nothing  smallDelay :: MonadUnliftIO m => CacheX m ()@@ -561,7 +627,9 @@     inSync >>= \s -> when s newBlockC cacheWriterReact (CachePing respond) = do     s <- inSync-    when s $ syncMempoolC >> void pruneDB+    when s $ do+        syncMempoolC+        void pruneDB     atomically (respond ())  lenNotNull :: [XPubBal] -> Int@@ -571,10 +639,12 @@        (MonadUnliftIO m, MonadLoggerIO m, StoreReadExtra m)     => XPubSpec -> [XPubBal] -> CacheX m () newXPubC xpub bals =-    should_index >>= \i -> when i $-    asks cacheRetries >>= \r ->-    void . async . withLockRetry r $-    isXPubCached xpub >>= \c -> when (not c) index+    should_index >>= \i ->+    if i+    then do+        incrementCounter cacheMisses+        withMetrics cacheIndexTime index+    else incrementCounter cacheIgnore   where     op XPubUnspent {xPubUnspent = u} = (unspentPoint u, unspentBlock u)     should_index = do@@ -583,7 +653,6 @@             then inSync             else return False     index = do-        bals <- lift (xPubBals xpub)         xpubtxt <- xpubText xpub         $(logDebugS) "Cache" $             "Caching " <> xpubtxt <> ": " <> cs (show (length bals)) <>@@ -964,13 +1033,14 @@  syncMempoolC :: (MonadUnliftIO m, MonadLoggerIO m, StoreReadExtra m)              => CacheX m ()-syncMempoolC = do-    refresh <- toInteger <$> asks cacheRefresh+syncMempoolC =+    toInteger <$> asks cacheRefresh >>= \refresh ->     void . withLockForever . withCool "cool" (refresh * 9 `div` 10) $ do-        nodepool <- HashSet.fromList . map snd <$> lift getMempool-        cachepool <- HashSet.fromList . map snd <$> cacheGetMempool-        getem (HashSet.difference nodepool cachepool)-        getem (HashSet.difference cachepool nodepool)+    nodepool <- HashSet.fromList . map snd <$> lift getMempool+    cachepool <- HashSet.fromList . map snd <$> cacheGetMempool+    getem (HashSet.difference nodepool cachepool)+    getem (HashSet.difference cachepool nodepool)+    incrementCounter cacheRefreshes   where     getem tset = do         let tids = HashSet.toList tset@@ -1017,7 +1087,7 @@     => [XPubSpec]     -> CacheT m () evictFromCache xpubs = ask >>= \case-    Nothing -> return ()+    Nothing  -> return ()     Just cfg -> void (runReaderT (withLockRetry 100 (delXPubKeys xpubs)) cfg)  delXPubKeys ::
src/Haskoin/Store/Manager.hs view
@@ -36,7 +36,7 @@                                                 blockStoreTxSTM, withBlockStore) import           Haskoin.Store.Cache           (CacheConfig (..), CacheWriter,                                                 cacheNewBlock, cacheWriter,-                                                connectRedis)+                                                connectRedis, newCacheMetrics) import           Haskoin.Store.Common          (StoreEvent (..)) import           Haskoin.Store.Database.Reader (DatabaseReader (..),                                                 DatabaseReaderT,@@ -46,6 +46,7 @@                                                 publishSTM, receive,                                                 withProcess, withPublisher,                                                 withSubscription)+import qualified System.Metrics                as Metrics (Store) import           UnliftIO                      (MonadIO, MonadUnliftIO, STM,                                                 atomically, link, withAsync) import           UnliftIO.Concurrent           (threadDelay)@@ -97,10 +98,10 @@       -- ^ connect to peers using the function 'withConnection'         , storeConfCacheRefresh    :: !Int       -- ^ refresh the cache this often (milliseconds)-        , storeConfCacheRetries    :: !Int-      -- ^ retry count for getting cache lock to index xpub         , storeConfCacheRetryDelay :: !Int       -- ^ delay in microseconds to retry getting cache lock+        , storeConfStats           :: !(Maybe Metrics.Store)+      -- ^ stats store         }  withStore :: (MonadLoggerIO m, MonadUnliftIO m)@@ -146,6 +147,7 @@         , blockConfNoMempool = storeConfNoMempool cfg         , blockConfWipeMempool = storeConfWipeMempool cfg         , blockConfPeerTimeout = storeConfPeerTimeout cfg+        , blockConfStats = storeConfStats cfg         }  nodeCfg :: StoreConfig@@ -182,24 +184,25 @@         Nothing ->             action Nothing         Just redisurl ->+            mapM newCacheMetrics (storeConfStats cfg) >>= \metrics ->             connectRedis redisurl >>= \conn ->             withSubscription pub $ \evts ->-            withProcess (f conn) $ \p ->+            withProcess (f conn metrics) $ \p ->             cacheWriterProcesses crefresh evts (getProcessMailbox p) $-            action (Just (c conn))+            action (Just (c conn metrics))   where     crefresh = storeConfCacheRefresh cfg-    f conn cwinbox =-        runReaderT (cacheWriter (c conn) cwinbox) db-    c conn =+    f conn metrics cwinbox =+        runReaderT (cacheWriter (c conn metrics) cwinbox) db+    c conn metrics =         CacheConfig            { cacheConn = conn            , cacheMin = storeConfCacheMin cfg            , cacheChain = chain            , cacheMax = storeConfMaxKeys cfg            , cacheRefresh = storeConfCacheRefresh cfg-           , cacheRetries = storeConfCacheRetries cfg            , cacheRetryDelay = storeConfCacheRetryDelay cfg+           , cacheMetrics = metrics            }  cacheWriterProcesses :: MonadUnliftIO m
+ src/Haskoin/Store/Stats.hs view
@@ -0,0 +1,141 @@+{-# LANGUAGE OverloadedStrings #-}+module Haskoin.Store.Stats+    ( StatDist+    , StatEntry(..)+    , withStats+    , createStatDist+    , addStatEntry+    ) where++import           Control.Concurrent.STM.TQueue   (TQueue, flushTQueue,+                                                  writeTQueue)+import qualified Control.Foldl                   as L+import           Control.Monad                   (forever)+import           Data.Function                   (on)+import           Data.HashMap.Strict             (HashMap)+import qualified Data.HashMap.Strict             as HashMap+import           Data.Int                        (Int64)+import           Data.List                       (sort, sortBy)+import           Data.Maybe                      (fromMaybe)+import           Data.Ord                        (Down (..), comparing)+import           Data.String.Conversions         (cs)+import           Data.Text                       (Text)+import           System.Metrics                  (Store, Value (..), newStore,+                                                  registerGcMetrics,+                                                  registerGroup, sampleAll)+import           System.Remote.Monitoring.Statsd (defaultStatsdOptions,+                                                  flushInterval, forkStatsd,+                                                  host, port, prefix)+import           UnliftIO                        (MonadIO, atomically, liftIO,+                                                  newTQueueIO, withAsync)+import           UnliftIO.Concurrent             (threadDelay)++withStats :: MonadIO m => Text -> Int -> Text -> (Store -> m a) -> m a+withStats h p pfx go = do+    store <- liftIO newStore+    _statsd <- liftIO $+        forkStatsd+        defaultStatsdOptions+            { prefix = pfx+            , host = h+            , port = p+            } store+    liftIO $ registerGcMetrics store+    go store++data StatEntry = StatEntry+    { statValue :: !Int64+    , statCount :: !Int64+    }+type StatDist = TQueue StatEntry++createStatDist :: MonadIO m => Text -> Store -> m StatDist+createStatDist t store = liftIO $ do+    q <- newTQueueIO+    let metrics = HashMap.fromList+            [ (t <> ".query_count",      Gauge . fromIntegral . length)+            , (t <> ".item_count",       Gauge . sum . map count)+            , (t <> ".per_query.mean",   Gauge . mean . map value)+            , (t <> ".per_query.avg",    Gauge . avg . map value)+            , (t <> ".per_query.max",    Gauge . maxi . map value)+            , (t <> ".per_query.min",    Gauge . mini . map value)+            , (t <> ".per_query.p90max", Gauge . p90max . map value)+            , (t <> ".per_query.p90min", Gauge . p90min . map value)+            , (t <> ".per_query.p90avg", Gauge . p90avg . map value)+            , (t <> ".per_query.var",    Gauge . var . map value)+            , (t <> ".per_item.mean",    Gauge . mean . normalize)+            , (t <> ".per_item.avg",     Gauge . avg . normalize)+            , (t <> ".per_item.max",     Gauge . maxi . normalize)+            , (t <> ".per_item.min",     Gauge . mini . normalize)+            , (t <> ".per_item.p90max",  Gauge . p90max . normalize)+            , (t <> ".per_item.p90min",  Gauge . p90min . normalize)+            , (t <> ".per_item.p90avg",  Gauge . p90avg . normalize)+            , (t <> ".per_item.var",     Gauge . var . normalize)+            ]+    registerGroup metrics (flush q) store+    return q+  where+    count = statCount+    value = statValue++toDouble :: Int64 -> Double+toDouble = fromIntegral++addStatEntry :: MonadIO m => StatDist -> StatEntry -> m ()+addStatEntry q = liftIO . atomically . writeTQueue q++flush :: MonadIO m => StatDist -> m [StatEntry]+flush = atomically . flushTQueue++average :: Fractional a => L.Fold a a+average = (/) <$> L.sum <*> L.genericLength++avg :: [Int64] -> Int64+avg = round . L.fold average . map toDouble++mean :: [Int64] -> Int64+mean = round . L.fold L.mean . map toDouble++maxi :: [Int64] -> Int64+maxi = fromMaybe 0 . L.fold L.maximum++mini :: [Int64] -> Int64+mini = fromMaybe 0 . L.fold L.minimum++var :: [Int64] -> Int64+var = round . L.fold L.variance . map toDouble++p90max :: [Int64] -> Int64+p90max ls =+    case chopped of+        []  -> 0+        h:_ -> h+  where+    sorted = sortBy (comparing Down) ls+    len = length sorted+    chopped = drop (length sorted * 1 `div` 10) sorted++p90min :: [Int64] -> Int64+p90min ls =+    case chopped of+        []  -> 0+        h:_ -> h+  where+    sorted = sort ls+    len = length sorted+    chopped = drop (length sorted * 1 `div` 10) sorted++p90avg :: [Int64] -> Int64+p90avg ls =+    avg chopped+  where+    sorted = sortBy (comparing Down) ls+    len = length sorted+    chopped = drop (length sorted * 1 `div` 10) sorted++normalize :: [StatEntry] -> [Int64]+normalize =+    concatMap $+    \(StatEntry x i) ->+        replicate (fromIntegral i) (round (toDouble x / toDouble i))+
src/Haskoin/Store/Web.hs view
@@ -28,6 +28,7 @@ import           Control.Monad.Reader          (ReaderT, ask, asks, local,                                                 runReaderT) import           Control.Monad.Trans           (lift)+import           Control.Monad.Trans.Control   (liftWith, restoreT) import           Control.Monad.Trans.Maybe     (MaybeT (..), runMaybeT) import           Data.Aeson                    (Encoding, ToJSON (..), Value) import           Data.Aeson.Encode.Pretty      (Config (..), defConfig,@@ -43,6 +44,7 @@ import           Data.HashMap.Strict           (HashMap) import qualified Data.HashMap.Strict           as HashMap import qualified Data.HashSet                  as HashSet+import           Data.Int                      (Int64) import           Data.List                     (nub) import qualified Data.Map.Strict               as Map import           Data.Maybe                    (catMaybes, fromJust, fromMaybe,@@ -57,9 +59,9 @@ import qualified Data.Text.Encoding            as T import           Data.Text.Lazy                (toStrict) import qualified Data.Text.Lazy                as TL-import           Data.Time.Clock               (NominalDiffTime, diffUTCTime,-                                                getCurrentTime)-import           Data.Time.Clock.System        (getSystemTime, systemSeconds)+import           Data.Time.Clock               (NominalDiffTime, diffUTCTime)+import           Data.Time.Clock.System        (getSystemTime, systemSeconds,+                                                systemToUTCTime) import           Data.Word                     (Word32, Word64) import           Database.RocksDB              (Property (..), getProperty) import           Haskoin.Address@@ -76,31 +78,39 @@ import           Haskoin.Store.Data import           Haskoin.Store.Database.Reader import           Haskoin.Store.Manager+import           Haskoin.Store.Stats import           Haskoin.Store.WebCommon import           Haskoin.Transaction import           Haskoin.Util+import           NQE                           (Inbox, receive,+                                                withSubscription) import           Network.HTTP.Types            (Status (..), status400,                                                 status403, status404, status500,-                                                status503, statusIsSuccessful)+                                                status503, statusIsClientError,+                                                statusIsServerError,+                                                statusIsSuccessful) import           Network.Wai                   (Middleware, Request (..),                                                 responseStatus) import           Network.Wai.Handler.Warp      (defaultSettings, setHost,                                                 setPort) import qualified Network.Wreq                  as Wreq-import           NQE                           (Inbox, receive,-                                                withSubscription)+import qualified System.Metrics                as Metrics+import qualified System.Metrics.Counter        as Metrics (Counter)+import qualified System.Metrics.Counter        as Metrics.Counter+import qualified System.Metrics.Gauge          as Metrics (Gauge)+import qualified System.Metrics.Gauge          as Metrics.Gauge import           Text.Printf                   (printf) import           UnliftIO                      (MonadIO, MonadUnliftIO, TVar,-                                                askRunInIO, atomically,-                                                handleAny, liftIO, newTVarIO,-                                                readTVarIO, timeout, withAsync,-                                                writeTVar)+                                                askRunInIO, atomically, bracket,+                                                bracket_, handleAny, liftIO,+                                                newTVarIO, readTVarIO, timeout,+                                                withAsync, writeTVar) import           UnliftIO.Concurrent           (threadDelay) import           Web.Scotty.Internal.Types     (ActionT) import           Web.Scotty.Trans              (Parsable) import qualified Web.Scotty.Trans              as S -type WebT m = ActionT Except (ReaderT WebConfig m)+type WebT m = ActionT Except (ReaderT WebState m)  data WebLimits = WebLimits     { maxLimitCount      :: !Word32@@ -134,8 +144,139 @@     , webVersion    :: !String     , webNoMempool  :: !Bool     , webNumTxId    :: !Bool+    , webStats      :: !(Maybe Metrics.Store)     } +data WebState = WebState+    { webConfig  :: !WebConfig+    , webTicker  :: !(TVar (HashMap Text BinfoTicker))+    , webMetrics :: !(Maybe WebMetrics)+    }++data ErrorCounter = ErrorCounter+    { clientErrors :: Metrics.Counter+    , serverErrors :: Metrics.Counter+    }++createErrorCounter :: MonadIO m+                   => Text+                   -> Metrics.Store+                   -> m ErrorCounter+createErrorCounter t s = liftIO $ do+    clientErrors <- Metrics.createCounter (t <> ".client_errors") s+    serverErrors <- Metrics.createCounter (t <> ".server_errors") s+    return ErrorCounter{..}++data WebMetrics = WebMetrics+    { everyResponseTime       :: !StatDist+    , everyError              :: !ErrorCounter+    , blockResponseTime       :: !StatDist+    , rawBlockResponseTime    :: !StatDist+    , blockErrors             :: !ErrorCounter+    , txResponseTime          :: !StatDist+    , txsBlockResponseTime    :: !StatDist+    , txErrors                :: !ErrorCounter+    , txAfterResponseTime     :: !StatDist+    , postTxResponseTime      :: !StatDist+    , postTxErrors            :: !ErrorCounter+    , mempoolResponseTime     :: !StatDist+    , addrTxResponseTime      :: !StatDist+    , addrTxFullResponseTime  :: !StatDist+    , addrBalanceResponseTime :: !StatDist+    , addrUnspentResponseTime :: !StatDist+    , xPubResponseTime        :: !StatDist+    , xPubTxResponseTime      :: !StatDist+    , xPubTxFullResponseTime  :: !StatDist+    , xPubUnspentResponseTime :: !StatDist+    , xPubEvictResponseTime   :: !StatDist+    , multiaddrResponseTime   :: !StatDist+    , multiaddrErrors         :: !ErrorCounter+    , rawtxResponseTime       :: !StatDist+    , rawtxErrors             :: !ErrorCounter+    , peerResponseTime        :: !StatDist+    , healthResponseTime      :: !StatDist+    , dbStatsResponseTime     :: !StatDist+    , eventsConnected         :: !Metrics.Gauge+    }++createMetrics :: MonadIO m => Metrics.Store -> m WebMetrics+createMetrics s = liftIO $ do+    everyResponseTime         <- d "all.response_time_ms"+    everyError                <- e "all.errors"+    blockResponseTime         <- d "block.response_time_ms"+    blockErrors               <- e "block.errors"+    rawBlockResponseTime      <- d "block_raw.response_time_ms"+    txResponseTime            <- d "tx.response_time"+    txsBlockResponseTime      <- d "txs_block.response_time"+    txErrors                  <- e "tx.errors"+    txAfterResponseTime       <- d "tx_after.response_time_ms"+    postTxResponseTime        <- d "tx_post.response_time_ms"+    postTxErrors              <- e "tx_post.errors"+    mempoolResponseTime       <- d "mempool.response_time_ms"+    addrBalanceResponseTime   <- d "addr_balance.response_time_ms"+    addrTxResponseTime        <- d "addr_tx.response_time_ms"+    addrTxFullResponseTime    <- d "addr_tx_full.response_time_ms"+    addrUnspentResponseTime   <- d "addr_unspent.response_time_ms"+    xPubResponseTime          <- d "xpub_balance.response_time_ms"+    xPubTxResponseTime        <- d "xpub_tx.response_time_ms"+    xPubTxFullResponseTime    <- d "xpub_tx_full.response_time_ms"+    xPubUnspentResponseTime   <- d "xpub_unspent.response_time_ms"+    xPubEvictResponseTime     <- d "xpub_evict.response_time_ms"+    multiaddrResponseTime     <- d "multiaddr.response_time_ms"+    multiaddrErrors           <- e "multiaddr.errors"+    rawtxResponseTime         <- d "rawtx.response_time_ms"+    rawtxErrors               <- e "rawtx.errors"+    peerResponseTime          <- d "peer.response_time_ms"+    healthResponseTime        <- d "health.response_time_ms"+    dbStatsResponseTime       <- d "dbstats.response_time_ms"+    eventsConnected           <- g "events.connected"+    return WebMetrics{..}+  where+    d x = createStatDist       ("web." <> x) s+    g x = Metrics.createGauge  ("web." <> x) s+    e x = createErrorCounter   ("web." <> x) s++withGaugeIncrease :: MonadUnliftIO m+                  => (WebMetrics -> Metrics.Gauge)+                  -> WebT m a+                  -> WebT m a+withGaugeIncrease gf go =+    lift (asks webMetrics) >>= \case+        Nothing -> go+        Just m -> do+            s <- liftWith $ \run ->+                bracket_+                (start m)+                (end m)+                (run go)+            restoreT $ return s+  where+    start m = liftIO $ Metrics.Gauge.inc (gf m)+    end m = liftIO $ Metrics.Gauge.dec (gf m)++withMetrics :: MonadUnliftIO m+            => (WebMetrics -> StatDist)+            -> Int+            -> WebT m a+            -> WebT m a+withMetrics df i go =+    lift (asks webMetrics) >>= \case+        Nothing -> go+        Just m -> do+            x <-+                liftWith $ \run ->+                bracket+                (systemToUTCTime <$> liftIO getSystemTime)+                (end m)+                (const (run go))+            restoreT $ return x+  where+    end metrics t1 = do+        t2 <- systemToUTCTime <$> liftIO getSystemTime+        let diff = round $ diffUTCTime t2 t1 * 1000+        df metrics `addStatEntry` StatEntry diff (fromIntegral i)+        everyResponseTime metrics `addStatEntry` StatEntry diff (fromIntegral i)+ data WebTimeouts = WebTimeouts     { txTimeout    :: !Word64     , blockTimeout :: !Word64@@ -149,7 +290,7 @@     def = WebTimeouts {txTimeout = 300, blockTimeout = 7200}  instance (MonadUnliftIO m, MonadLoggerIO m) =>-         StoreReadBase (ReaderT WebConfig m) where+         StoreReadBase (ReaderT WebState m) where     getNetwork = runInWebReader getNetwork     getBestBlock = runInWebReader getBestBlock     getBlocksAtHeight height = runInWebReader (getBlocksAtHeight height)@@ -161,7 +302,7 @@     getMempool = runInWebReader getMempool  instance (MonadUnliftIO m, MonadLoggerIO m) =>-         StoreReadExtra (ReaderT WebConfig m) where+         StoreReadExtra (ReaderT WebState m) where     getMaxGap = runInWebReader getMaxGap     getInitialGap = runInWebReader getInitialGap     getBalances as = runInWebReader (getBalances as)@@ -201,15 +342,19 @@ runWeb :: (MonadUnliftIO m, MonadLoggerIO m) => WebConfig -> m () runWeb cfg@WebConfig{ webHost = host                     , webPort = port-                    , webStore = store } = do+                    , webStore = store+                    , webStats = stats+                    } = do     ticker <- newTVarIO HashMap.empty+    metrics <- mapM createMetrics stats+    let st = WebState { webConfig = cfg, webTicker = ticker, webMetrics = metrics }     withAsync (price (storeNetwork store) ticker) $ \_ -> do         reqLogger <- logIt         runner <- askRunInIO-        S.scottyOptsT opts (runner . (`runReaderT` cfg)) $ do+        S.scottyOptsT opts (runner . (`runReaderT` st)) $ do             S.middleware reqLogger             S.defaultHandler defHandler-            handlePaths ticker+            handlePaths             S.notFound $ S.raise ThingNotFound   where     opts = def {S.settings = settings defaultSettings}@@ -236,24 +381,53 @@             atomically . writeTVar v $ r ^. Wreq.responseBody         threadDelay $ 5 * 60 * 1000 * 1000 -- five minutes +raise_ :: MonadIO m => Except -> WebT m a+raise_ err =+    lift (asks webMetrics) >>= \case+    Nothing -> S.raise err+    Just metrics -> do+        let status = errStatus err+        if | statusIsClientError status -> liftIO $+                 Metrics.Counter.inc (clientErrors (everyError metrics))+           | statusIsServerError status -> liftIO $+                 Metrics.Counter.inc (serverErrors (everyError metrics))+           | otherwise ->+                 return ()+        S.raise err +raise :: MonadIO m => (WebMetrics -> ErrorCounter) -> Except -> WebT m a+raise metric err =+    lift (asks webMetrics) >>= \case+    Nothing -> S.raise err+    Just metrics -> do+        let status = errStatus err+        if | statusIsClientError status -> liftIO $ do+                 Metrics.Counter.inc (clientErrors (everyError metrics))+                 Metrics.Counter.inc (clientErrors (metric metrics))+           | statusIsServerError status -> liftIO $ do+                 Metrics.Counter.inc (serverErrors (everyError metrics))+                 Metrics.Counter.inc (serverErrors (metric metrics))+           | otherwise ->+                 return ()+        S.raise err++errStatus :: Except -> Status+errStatus ThingNotFound = status404+errStatus BadRequest    = status400+errStatus UserError{}   = status400+errStatus StringError{} = status400+errStatus ServerError   = status500+errStatus BlockTooLarge = status403+ defHandler :: Monad m => Except -> WebT m () defHandler e = do     proto <- setupContentType False-    case e of-        ThingNotFound -> S.status status404-        BadRequest    -> S.status status400-        UserError _   -> S.status status400-        StringError _ -> S.status status400-        ServerError   -> S.status status500-        BlockTooLarge -> S.status status403+    S.status $ errStatus e     S.raw $ protoSerial proto toEncoding toJSON e -handlePaths ::-       (MonadUnliftIO m, MonadLoggerIO m)-    => TVar (HashMap Text BinfoTicker)-    -> S.ScottyT Except (ReaderT WebConfig m) ()-handlePaths ticker = do+handlePaths :: (MonadUnliftIO m, MonadLoggerIO m)+            => S.ScottyT Except (ReaderT WebState m) ()+handlePaths = do     -- Block Paths     pathPretty         (GetBlock <$> paramLazy <*> paramDef)@@ -452,7 +626,7 @@     S.get "/events" scottyEvents     S.get "/dbstats" scottyDbStats     -- Blockchain.info-    S.post "/blockchain/multiaddr" (scottyMultiAddr ticker)+    S.post "/blockchain/multiaddr" scottyMultiAddr     S.get "/blockchain/rawtx/:txid" scottyBinfoTx   where     json_list f net = toJSONList . map (f net)@@ -463,7 +637,7 @@     -> (a -> WebT m b)     -> (Network -> b -> Encoding)     -> (Network -> b -> Value)-    -> S.ScottyT Except (ReaderT WebConfig m) ()+    -> S.ScottyT Except (ReaderT WebState m) () pathPretty parser action encJson encValue =     pathCommon parser action encJson encValue True @@ -473,7 +647,7 @@     -> (a -> WebT m b)     -> (Network -> b -> Encoding)     -> (Network -> b -> Value)-    -> S.ScottyT Except (ReaderT WebConfig m) ()+    -> S.ScottyT Except (ReaderT WebState m) () pathCompact parser action encJson encValue =     pathCommon parser action encJson encValue False @@ -484,12 +658,12 @@     -> (Network -> b -> Encoding)     -> (Network -> b -> Value)     -> Bool-    -> S.ScottyT Except (ReaderT WebConfig m) ()+    -> S.ScottyT Except (ReaderT WebState m) () pathCommon parser action encJson encValue pretty =     S.addroute (resourceMethod proxy) (capturePath proxy) $ do         setHeaders         proto <- setupContentType pretty-        net <- lift $ asks (storeNetwork . webStore)+        net <- lift $ asks (storeNetwork . webStore . webConfig)         apiRes <- parser         res <- action apiRes         S.raw $ protoSerial proto (encJson net) (encValue net) res@@ -533,12 +707,21 @@ scottyBlock ::        (MonadUnliftIO m, MonadLoggerIO m) => GetBlock -> WebT m BlockData scottyBlock (GetBlock h (NoTx noTx)) =-    maybe (S.raise ThingNotFound) (return . pruneTx noTx) =<< getBlock h+    withMetrics blockResponseTime 1 $+    maybe (raise blockErrors ThingNotFound) (return . pruneTx noTx) =<<+    getBlock h +getBlocks :: (MonadUnliftIO m, MonadLoggerIO m)+          => [H.BlockHash]+          -> Bool+          -> WebT m [BlockData]+getBlocks hs notx =+    (pruneTx notx <$>) . catMaybes <$> mapM getBlock (nub hs)+ scottyBlocks ::        (MonadUnliftIO m, MonadLoggerIO m) => GetBlocks -> WebT m [BlockData]-scottyBlocks (GetBlocks hs (NoTx noTx)) =-    (pruneTx noTx <$>) . catMaybes <$> mapM getBlock (nub hs)+scottyBlocks (GetBlocks hs (NoTx notx)) =+    withMetrics blockResponseTime (length hs) $ getBlocks hs notx  pruneTx :: Bool -> BlockData -> BlockData pruneTx False b = b@@ -550,13 +733,16 @@        (MonadUnliftIO m, MonadLoggerIO m)     => GetBlockRaw     -> WebT m (RawResult H.Block)-scottyBlockRaw (GetBlockRaw h) = RawResult <$> getRawBlock h+scottyBlockRaw (GetBlockRaw h) =+    withMetrics rawBlockResponseTime 1 $+    RawResult <$> getRawBlock h  getRawBlock ::        (MonadUnliftIO m, MonadLoggerIO m) => H.BlockHash -> WebT m H.Block-getRawBlock h = do-    b <- maybe (S.raise ThingNotFound) return =<< getBlock h-    refuseLargeBlock b+getRawBlock h =+    withMetrics rawBlockResponseTime 1 $ do+    b <- maybe (raise blockErrors ThingNotFound) return =<< getBlock h+    refuseLargeBlock blockErrors b     toRawBlock b  toRawBlock :: (Monad m, StoreReadBase m) => BlockData -> m H.Block@@ -565,25 +751,34 @@     txs <- map transactionData . catMaybes <$> mapM getTransaction ths     return H.Block {H.blockHeader = blockDataHeader b, H.blockTxns = txs} -refuseLargeBlock :: Monad m => BlockData -> WebT m ()-refuseLargeBlock BlockData {blockDataTxs = txs} = do-    WebLimits {maxLimitFull = f} <- lift $ asks webMaxLimits-    when (length txs > fromIntegral f) $ S.raise BlockTooLarge+refuseLargeBlock :: MonadIO m+                 => (WebMetrics -> ErrorCounter)+                 -> BlockData+                 -> WebT m ()+refuseLargeBlock metric BlockData {blockDataTxs = txs} = do+    WebLimits {maxLimitFull = f} <- lift $ asks (webMaxLimits . webConfig)+    when (length txs > fromIntegral f) $ raise metric BlockTooLarge  -- GET BlockBest / BlockBestRaw --  scottyBlockBest ::        (MonadUnliftIO m, MonadLoggerIO m) => GetBlockBest -> WebT m BlockData-scottyBlockBest (GetBlockBest noTx) = do-    bestM <- getBestBlock-    maybe (S.raise ThingNotFound) (scottyBlock . (`GetBlock` noTx)) bestM+scottyBlockBest (GetBlockBest (NoTx notx)) =+    withMetrics blockResponseTime 1 $+    getBestBlock >>= \case+        Nothing -> raise blockErrors ThingNotFound+        Just bb -> getBlock bb >>= \case+            Nothing -> raise blockErrors ThingNotFound+            Just b -> return $ pruneTx notx b  scottyBlockBestRaw ::        (MonadUnliftIO m, MonadLoggerIO m)     => GetBlockBestRaw     -> WebT m (RawResult H.Block) scottyBlockBestRaw _ =-    RawResult <$> (maybe (S.raise ThingNotFound) getRawBlock =<< getBestBlock)+    withMetrics rawBlockResponseTime 1 $+    fmap RawResult $+    maybe (raise blockErrors ThingNotFound) getRawBlock =<< getBestBlock  -- GET BlockLatest -- @@ -592,7 +787,8 @@     => GetBlockLatest     -> WebT m [BlockData] scottyBlockLatest (GetBlockLatest (NoTx noTx)) =-    maybe (S.raise ThingNotFound) (go [] <=< getBlock) =<< getBestBlock+    withMetrics blockResponseTime 100 $+    maybe (raise blockErrors ThingNotFound) (go [] <=< getBlock) =<< getBestBlock   where     go acc Nothing = return acc     go acc (Just b)@@ -606,95 +802,114 @@  scottyBlockHeight ::        (MonadUnliftIO m, MonadLoggerIO m) => GetBlockHeight -> WebT m [BlockData]-scottyBlockHeight (GetBlockHeight h noTx) =-    scottyBlocks . (`GetBlocks` noTx) =<< getBlocksAtHeight (fromIntegral h)+scottyBlockHeight (GetBlockHeight h (NoTx notx)) =+    withMetrics blockResponseTime 1 $+    (`getBlocks` notx) =<< getBlocksAtHeight (fromIntegral h)  scottyBlockHeights ::        (MonadUnliftIO m, MonadLoggerIO m)     => GetBlockHeights     -> WebT m [BlockData]-scottyBlockHeights (GetBlockHeights (HeightsParam heights) noTx) = do+scottyBlockHeights (GetBlockHeights (HeightsParam heights) (NoTx notx)) =+    withMetrics blockResponseTime (length heights) $ do     bhs <- concat <$> mapM getBlocksAtHeight (fromIntegral <$> heights)-    scottyBlocks (GetBlocks bhs noTx)+    getBlocks bhs notx  scottyBlockHeightRaw ::        (MonadUnliftIO m, MonadLoggerIO m)     => GetBlockHeightRaw     -> WebT m (RawResultList H.Block) scottyBlockHeightRaw (GetBlockHeightRaw h) =+    withMetrics rawBlockResponseTime 1 $     RawResultList <$> (mapM getRawBlock =<< getBlocksAtHeight (fromIntegral h))  -- GET BlockTime / BlockTimeRaw --  scottyBlockTime :: (MonadUnliftIO m, MonadLoggerIO m)                 => GetBlockTime -> WebT m BlockData-scottyBlockTime (GetBlockTime (TimeParam t) (NoTx noTx)) = do-    ch <- lift $ asks (storeChain . webStore)+scottyBlockTime (GetBlockTime (TimeParam t) (NoTx notx)) =+    withMetrics blockResponseTime 1 $ do+    ch <- lift $ asks (storeChain . webStore . webConfig)     m <- blockAtOrBefore ch t-    maybe (S.raise ThingNotFound) (return . pruneTx noTx) m+    maybe (raise blockErrors ThingNotFound) (return . pruneTx notx) m  scottyBlockMTP :: (MonadUnliftIO m, MonadLoggerIO m)                => GetBlockMTP -> WebT m BlockData-scottyBlockMTP (GetBlockMTP (TimeParam t) (NoTx noTx)) = do-    ch <- lift $ asks (storeChain . webStore)+scottyBlockMTP (GetBlockMTP (TimeParam t) (NoTx noTx)) =+    withMetrics blockResponseTime 1 $ do+    ch <- lift $ asks (storeChain . webStore . webConfig)     m <- blockAtOrAfterMTP ch t-    maybe (S.raise ThingNotFound) (return . pruneTx noTx) m+    maybe (raise blockErrors ThingNotFound) (return . pruneTx noTx) m  scottyBlockTimeRaw :: (MonadUnliftIO m, MonadLoggerIO m)                    => GetBlockTimeRaw -> WebT m (RawResult H.Block)-scottyBlockTimeRaw (GetBlockTimeRaw (TimeParam t)) = do-    ch <- lift $ asks (storeChain . webStore)+scottyBlockTimeRaw (GetBlockTimeRaw (TimeParam t)) =+    withMetrics rawBlockResponseTime 1 $ do+    ch <- lift $ asks (storeChain . webStore . webConfig)     m <- blockAtOrBefore ch t-    b <- maybe (S.raise ThingNotFound) return m-    refuseLargeBlock b+    b <- maybe (raise blockErrors ThingNotFound) return m+    refuseLargeBlock blockErrors b     RawResult <$> toRawBlock b  scottyBlockMTPRaw :: (MonadUnliftIO m, MonadLoggerIO m)                   => GetBlockMTPRaw -> WebT m (RawResult H.Block)-scottyBlockMTPRaw (GetBlockMTPRaw (TimeParam t)) = do-    ch <- lift $ asks (storeChain . webStore)+scottyBlockMTPRaw (GetBlockMTPRaw (TimeParam t)) =+    withMetrics rawBlockResponseTime 1 $ do+    ch <- lift $ asks (storeChain . webStore . webConfig)     m <- blockAtOrAfterMTP ch t-    b <- maybe (S.raise ThingNotFound) return m-    refuseLargeBlock b+    b <- maybe (raise blockErrors ThingNotFound) return m+    refuseLargeBlock blockErrors b     RawResult <$> toRawBlock b  -- GET Transactions --  scottyTx :: (MonadUnliftIO m, MonadLoggerIO m) => GetTx -> WebT m Transaction scottyTx (GetTx txid) =-    maybe (S.raise ThingNotFound) return =<< getTransaction txid+    withMetrics txResponseTime 1 $+    maybe (raise txErrors ThingNotFound) return =<< getTransaction txid  scottyTxs ::        (MonadUnliftIO m, MonadLoggerIO m) => GetTxs -> WebT m [Transaction]-scottyTxs (GetTxs txids) = catMaybes <$> mapM getTransaction (nub txids)+scottyTxs (GetTxs txids) =+    withMetrics txResponseTime (length txids) $+    catMaybes <$> mapM getTransaction (nub txids)  scottyTxRaw ::        (MonadUnliftIO m, MonadLoggerIO m) => GetTxRaw -> WebT m (RawResult Tx)-scottyTxRaw (GetTxRaw txid) = do-    tx <- maybe (S.raise ThingNotFound) return =<< getTransaction txid+scottyTxRaw (GetTxRaw txid) =+    withMetrics txResponseTime 1 $ do+    tx <- maybe (raise txErrors ThingNotFound) return =<< getTransaction txid     return $ RawResult $ transactionData tx  scottyTxsRaw ::        (MonadUnliftIO m, MonadLoggerIO m)     => GetTxsRaw     -> WebT m (RawResultList Tx)-scottyTxsRaw (GetTxsRaw txids) = do+scottyTxsRaw (GetTxsRaw txids) =+    withMetrics txResponseTime (length txids) $ do     txs <- catMaybes <$> mapM getTransaction (nub txids)     return $ RawResultList $ transactionData <$> txs +getTxsBlock :: (MonadUnliftIO m, MonadLoggerIO m)+            => H.BlockHash+            -> WebT m [Transaction]+getTxsBlock h = do+    b <- maybe (raise txErrors ThingNotFound) return =<< getBlock h+    refuseLargeBlock txErrors b+    catMaybes <$> mapM getTransaction (blockDataTxs b)+ scottyTxsBlock ::        (MonadUnliftIO m, MonadLoggerIO m) => GetTxsBlock -> WebT m [Transaction]-scottyTxsBlock (GetTxsBlock h) = do-    b <- maybe (S.raise ThingNotFound) return =<< getBlock h-    refuseLargeBlock b-    catMaybes <$> mapM getTransaction (blockDataTxs b)+scottyTxsBlock (GetTxsBlock h) =+    withMetrics txsBlockResponseTime 1 $ getTxsBlock h  scottyTxsBlockRaw ::        (MonadUnliftIO m, MonadLoggerIO m)     => GetTxsBlockRaw     -> WebT m (RawResultList Tx) scottyTxsBlockRaw (GetTxsBlockRaw h) =-    RawResultList . fmap transactionData <$> scottyTxsBlock (GetTxsBlock h)+    withMetrics txsBlockResponseTime 1 $+    RawResultList . fmap transactionData <$> getTxsBlock h  -- GET TransactionAfterHeight -- @@ -703,6 +918,7 @@     => GetTxAfter     -> WebT m (GenericResult (Maybe Bool)) scottyTxAfter (GetTxAfter txid height) =+    withMetrics txAfterResponseTime 1 $     GenericResult <$> cbAfterHeight (fromIntegral height) txid  -- | Check if any of the ancestors of this transaction is a coinbase after the@@ -743,10 +959,11 @@  scottyPostTx :: (MonadUnliftIO m, MonadLoggerIO m) => PostTx -> WebT m TxId scottyPostTx (PostTx tx) =-    lift ask >>= \cfg -> lift (publishTx cfg tx) >>= \case-        Right () -> return $ TxId (txHash tx)-        Left e@(PubReject _) -> S.raise $ UserError $ show e-        _ -> S.raise ServerError+    withMetrics postTxResponseTime 1 $+    lift (asks webConfig) >>= \cfg -> lift (publishTx cfg tx) >>= \case+        Right ()             -> return $ TxId (txHash tx)+        Left e@(PubReject _) -> raise postTxErrors $ UserError $ show e+        _                    -> raise postTxErrors ServerError  -- | Publish a new transaction to the network. publishTx ::@@ -757,7 +974,7 @@ publishTx cfg tx =     withSubscription pub $ \s ->         getTransaction (txHash tx) >>= \case-            Just _ -> return $ Right ()+            Just _  -> return $ Right ()             Nothing -> go s   where     pub = storePublisher (webStore cfg)@@ -781,8 +998,8 @@       | webNoMempool cfg = return $ Right ()       | otherwise =         liftIO (timeout t (g p s)) >>= \case-            Nothing -> return $ Left PubTimeout-            Just (Left e) -> return $ Left e+            Nothing         -> return $ Left PubTimeout+            Just (Left e)   -> return $ Left e             Just (Right ()) -> return $ Right ()     g p s =         receive s >>= \case@@ -798,17 +1015,19 @@  scottyMempool ::        (MonadUnliftIO m, MonadLoggerIO m) => GetMempool -> WebT m [TxHash]-scottyMempool (GetMempool limitM (OffsetParam o)) = do-    wl <- lift $ asks webMaxLimits+scottyMempool (GetMempool limitM (OffsetParam o)) =+    withMetrics mempoolResponseTime 1 $ do+    wl <- lift $ asks (webMaxLimits . webConfig)     let wl' = wl { maxLimitCount = 0 }         l = Limits (validateLimit wl' False limitM) (fromIntegral o) Nothing     map snd . applyLimits l <$> getMempool -scottyEvents :: MonadLoggerIO m => WebT m ()-scottyEvents = do+scottyEvents :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()+scottyEvents =+    withGaugeIncrease eventsConnected $ do     setHeaders     proto <- setupContentType False-    pub <- lift $ asks (storePublisher . webStore)+    pub <- lift $ asks (storePublisher . webStore . webConfig)     S.stream $ \io flush' ->         withSubscription pub $ \sub ->             forever $@@ -834,43 +1053,53 @@ scottyAddrTxs ::        (MonadUnliftIO m, MonadLoggerIO m) => GetAddrTxs -> WebT m [TxRef] scottyAddrTxs (GetAddrTxs addr pLimits) =+    withMetrics addrTxResponseTime 1 $     getAddressTxs addr =<< paramToLimits False pLimits  scottyAddrsTxs ::        (MonadUnliftIO m, MonadLoggerIO m) => GetAddrsTxs -> WebT m [TxRef] scottyAddrsTxs (GetAddrsTxs addrs pLimits) =+    withMetrics addrTxResponseTime (length addrs) $     getAddressesTxs addrs =<< paramToLimits False pLimits  scottyAddrTxsFull ::        (MonadUnliftIO m, MonadLoggerIO m)     => GetAddrTxsFull     -> WebT m [Transaction]-scottyAddrTxsFull (GetAddrTxsFull addr pLimits) = do+scottyAddrTxsFull (GetAddrTxsFull addr pLimits) =+    withMetrics addrTxFullResponseTime 1 $ do     txs <- getAddressTxs addr =<< paramToLimits True pLimits     catMaybes <$> mapM (getTransaction . txRefHash) txs  scottyAddrsTxsFull :: (MonadUnliftIO m, MonadLoggerIO m)                    => GetAddrsTxsFull -> WebT m [Transaction]-scottyAddrsTxsFull (GetAddrsTxsFull addrs pLimits) = do+scottyAddrsTxsFull (GetAddrsTxsFull addrs pLimits) =+    withMetrics addrTxFullResponseTime (length addrs) $ do     txs <- getAddressesTxs addrs =<< paramToLimits True pLimits     catMaybes <$> mapM (getTransaction . txRefHash) txs  scottyAddrBalance :: (MonadUnliftIO m, MonadLoggerIO m)                   => GetAddrBalance -> WebT m Balance-scottyAddrBalance (GetAddrBalance addr) = getDefaultBalance addr+scottyAddrBalance (GetAddrBalance addr) =+    withMetrics addrBalanceResponseTime 1 $+    getDefaultBalance addr  scottyAddrsBalance ::        (MonadUnliftIO m, MonadLoggerIO m) => GetAddrsBalance -> WebT m [Balance]-scottyAddrsBalance (GetAddrsBalance addrs) = getBalances addrs+scottyAddrsBalance (GetAddrsBalance addrs) =+    withMetrics addrBalanceResponseTime (length addrs) $+    getBalances addrs  scottyAddrUnspent ::        (MonadUnliftIO m, MonadLoggerIO m) => GetAddrUnspent -> WebT m [Unspent] scottyAddrUnspent (GetAddrUnspent addr pLimits) =+    withMetrics addrUnspentResponseTime 1 $     getAddressUnspents addr =<< paramToLimits False pLimits  scottyAddrsUnspent ::        (MonadUnliftIO m, MonadLoggerIO m) => GetAddrsUnspent -> WebT m [Unspent] scottyAddrsUnspent (GetAddrsUnspent addrs pLimits) =+    withMetrics addrUnspentResponseTime (length addrs) $     getAddressesUnspents addrs =<< paramToLimits False pLimits  -- GET XPubs --@@ -878,26 +1107,35 @@ scottyXPub ::        (MonadUnliftIO m, MonadLoggerIO m) => GetXPub -> WebT m XPubSummary scottyXPub (GetXPub xpub deriv (NoCache noCache)) =+    withMetrics xPubResponseTime 1 $     lift . runNoCache noCache $ xPubSummary $ XPubSpec xpub deriv +getXPubTxs :: (MonadUnliftIO m, MonadLoggerIO m)+           => XPubKey -> DeriveType -> LimitsParam -> Bool -> WebT m [TxRef]+getXPubTxs xpub deriv plimits nocache = do+    limits <- paramToLimits False plimits+    lift . runNoCache nocache $ xPubTxs (XPubSpec xpub deriv) limits+ scottyXPubTxs ::        (MonadUnliftIO m, MonadLoggerIO m) => GetXPubTxs -> WebT m [TxRef]-scottyXPubTxs (GetXPubTxs xpub deriv pLimits (NoCache noCache)) = do-    limits <- paramToLimits False pLimits-    lift . runNoCache noCache $ xPubTxs (XPubSpec xpub deriv) limits+scottyXPubTxs (GetXPubTxs xpub deriv plimits (NoCache nocache)) =+    withMetrics xPubTxResponseTime 1 $+    getXPubTxs xpub deriv plimits nocache  scottyXPubTxsFull ::        (MonadUnliftIO m, MonadLoggerIO m)     => GetXPubTxsFull     -> WebT m [Transaction]-scottyXPubTxsFull (GetXPubTxsFull xpub deriv pLimits n@(NoCache noCache)) = do-    refs <- scottyXPubTxs (GetXPubTxs xpub deriv pLimits n)-    txs <- lift . runNoCache noCache $ mapM (getTransaction . txRefHash) refs+scottyXPubTxsFull (GetXPubTxsFull xpub deriv plimits (NoCache nocache)) =+    withMetrics xPubTxFullResponseTime 1 $ do+    refs <- getXPubTxs xpub deriv plimits nocache+    txs <- lift . runNoCache nocache $ mapM (getTransaction . txRefHash) refs     return $ catMaybes txs  scottyXPubBalances ::        (MonadUnliftIO m, MonadLoggerIO m) => GetXPubBalances -> WebT m [XPubBal] scottyXPubBalances (GetXPubBalances xpub deriv (NoCache noCache)) =+    withMetrics xPubResponseTime 1 $     filter f <$> lift (runNoCache noCache (xPubBals spec))   where     spec = XPubSpec xpub deriv@@ -907,7 +1145,8 @@        (MonadUnliftIO m, MonadLoggerIO m)     => GetXPubUnspent     -> WebT m [XPubUnspent]-scottyXPubUnspent (GetXPubUnspent xpub deriv pLimits (NoCache noCache)) = do+scottyXPubUnspent (GetXPubUnspent xpub deriv pLimits (NoCache noCache)) =+    withMetrics xPubUnspentResponseTime 1 $ do     limits <- paramToLimits False pLimits     lift . runNoCache noCache $ xPubUnspents (XPubSpec xpub deriv) limits @@ -915,8 +1154,9 @@        (MonadUnliftIO m, MonadLoggerIO m)     => GetXPubEvict     -> WebT m (GenericResult Bool)-scottyXPubEvict (GetXPubEvict xpub deriv) = do-    cache <- lift $ asks (storeCache . webStore)+scottyXPubEvict (GetXPubEvict xpub deriv) =+    withMetrics xPubEvictResponseTime 1 $ do+    cache <- lift $ asks (storeCache . webStore . webConfig)     lift . withCache cache $ evictFromCache [XPubSpec xpub deriv]     return $ GenericResult True @@ -969,20 +1209,22 @@         x     -> x  scottyMultiAddr :: (MonadUnliftIO m, MonadLoggerIO m)-                => TVar (HashMap Text BinfoTicker)-                -> WebT m ()-scottyMultiAddr ticker = do+                => WebT m ()+scottyMultiAddr =+    get_addrs >>= \(addrs, xpubs, saddrs, sxpubs, xspecs) ->+    let len = HashSet.size addrs + HashSet.size xpubs+    in withMetrics multiaddrResponseTime len $ do     prune <- get_prune     n <- get_count     offset <- get_offset     cashaddr <- get_cashaddr-    numtxid <- lift $ asks webNumTxId-    (addrs, xpubs, saddrs, sxpubs, xspecs) <- get_addrs+    numtxid <- lift $ asks (webNumTxId . webConfig)     xbals <- get_xbals xspecs     let sxbals = compute_sxbals sxpubs xbals     sxtrs <- get_sxtrs sxpubs xspecs     sabals <- get_abals saddrs     satrs <- get_atrs n offset saddrs+    ticker <- lift $ asks webTicker     local <- get_price ticker     let sxtns = HashMap.map length sxtrs         sxtrset = Set.fromList . concat $ HashMap.elems sxtrs@@ -996,14 +1238,14 @@         stxids = compute_txids n offset salltrs     stxs <- get_txs stxids     let etxids = if numtxid-                 then compute_etxids prune abook stxs-                 else []+                then compute_etxids prune abook stxs+                else []     etxs <- if numtxid             then Just <$> get_etxs etxids             else return Nothing-    best <- scottyBlockBest (GetBlockBest (NoTx True))+    best <- get_best_block     peers <- get_peers-    net <- lift $ asks (storeNetwork . webStore)+    net <- lift $ asks (storeNetwork . webStore . webConfig)     let ibal = fromIntegral bal         btxs = binfo_txs etxs abook salladdrs prune ibal stxs         ftxs = take n $ drop offset btxs@@ -1046,6 +1288,12 @@         , getBinfoMultiAddrCashAddr = cashaddr         }   where+    get_best_block =+        getBestBlock >>= \case+        Nothing -> raise multiaddrErrors ThingNotFound+        Just bh -> getBlock bh >>= \case+            Nothing -> raise multiaddrErrors ThingNotFound+            Just b -> return b     get_price ticker = do         code <- T.toUpper <$> S.param "local" `S.rescue` const (return "USD")         prices <- readTVarIO ticker@@ -1057,19 +1305,19 @@     get_cashaddr = S.param "cashaddr"         `S.rescue` const (return False)     get_count = do-        d <- lift (asks (maxLimitDefault . webMaxLimits))-        x <- lift (asks (maxLimitFull . webMaxLimits))+        d <- lift (asks (maxLimitDefault . webMaxLimits . webConfig))+        x <- lift (asks (maxLimitFull . webMaxLimits . webConfig))         i <- min x <$> (S.param "n" `S.rescue` const (return d))         return (fromIntegral i :: Int)     get_offset = do-        x <- lift (asks (maxLimitOffset . webMaxLimits))+        x <- lift (asks (maxLimitOffset . webMaxLimits . webConfig))         o <- min x <$> (S.param "offset" `S.rescue` const (return 0))         return (fromIntegral o :: Int)     get_addrs_param name = do-        net <- lift (asks (storeNetwork . webStore))+        net <- lift (asks (storeNetwork . webStore . webConfig))         p <- S.param name `S.rescue` const (return "")         case parseBinfoAddr net p of-            Nothing -> S.raise (UserError "invalid active address")+            Nothing -> raise multiaddrErrors (UserError "invalid active address")             Just xs -> return $ HashSet.fromList xs     addr (BinfoAddr a) = Just a     addr (BinfoXpub x) = Nothing@@ -1125,7 +1373,8 @@             g = HashMap.fromList . map f . catMaybes         in fmap g . mapM getTransaction     get_peers = do-        ps <- lift $ getPeersInformation =<< asks (storeManager . webStore)+        ps <- lift $ getPeersInformation =<<+            asks (storeManager . webStore . webConfig)         return (fromIntegral (length ps))     compute_txids n offset = map txRefHash . take (n + offset) . Set.toDescList     compute_etxids prune abook =@@ -1168,12 +1417,13 @@  scottyBinfoTx :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m () scottyBinfoTx =-    lift (asks webNumTxId) >>= \num -> S.param "txid" >>= \case+    withMetrics rawtxResponseTime 1 $+    lift (asks (webNumTxId . webConfig)) >>= \num -> S.param "txid" >>= \case         BinfoTxIdHash h -> go num h         BinfoTxIdIndex i ->-            if | not num || isBinfoTxIndexNull i -> S.raise ThingNotFound-               | isBinfoTxIndexBlock i -> block i-               | isBinfoTxIndexHash i -> hash i+            if | not num || isBinfoTxIndexNull i -> raise rawtxErrors ThingNotFound+               | isBinfoTxIndexBlock i           -> block i+               | isBinfoTxIndexHash i            -> hash i   where     get_format = S.param "format" `S.rescue` const (return ("json" :: Text))     js num t = do@@ -1185,27 +1435,27 @@                  else return Nothing         let f t = (txHash (transactionData t), t)             etxs = HashMap.fromList . map f <$> etxs'-        net <- lift $ asks (storeNetwork . webStore)+        net <- lift $ asks (storeNetwork . webStore . webConfig)         setHeaders         S.json . binfoTxToJSON net $ toBinfoTxSimple etxs t     hex t = do         setHeaders         S.text . TL.fromStrict . encodeHex . encode $ transactionData t     go num h = getTransaction h >>= \case-        Nothing -> S.raise ThingNotFound+        Nothing -> raise rawtxErrors ThingNotFound         Just t -> get_format >>= \case             "hex" -> hex t-            _ -> js num t+            _     -> js num t     block i =         let Just (height, pos) = binfoTxIndexBlock i         in getBlocksAtHeight height >>= \case-            [] -> S.raise ThingNotFound+            [] -> raise rawtxErrors ThingNotFound             h:_ -> getBlock h >>= \case-                Nothing -> S.raise ThingNotFound+                Nothing -> raise rawtxErrors ThingNotFound                 Just BlockData{..} ->                     if length blockDataTxs > fromIntegral pos                     then go True (blockDataTxs !! fromIntegral pos)-                    else S.raise ThingNotFound+                    else raise rawtxErrors ThingNotFound     hmatch h = listToMaybe . filter (matchBinfoTxHash h)     hmem h = hmatch h . map snd <$> getMempool     hblock h =@@ -1219,18 +1469,22 @@                         Just x  -> return (Just x)         in getBestBlock >>= \case             Nothing -> return Nothing-            Just k -> g 10 k+            Just k  -> g 10 k     hash h = hmem h >>= \case         Nothing -> hblock h >>= \case-            Nothing -> S.raise ThingNotFound-            Just x -> go True x+            Nothing -> raise rawtxErrors ThingNotFound+            Just x  -> go True x         Just x -> go True x  -- GET Network Information -- -scottyPeers :: MonadLoggerIO m => GetPeers -> WebT m [PeerInformation]-scottyPeers _ = lift $-    getPeersInformation =<< asks (storeManager . webStore)+scottyPeers :: (MonadUnliftIO m, MonadLoggerIO m)+            => GetPeers+            -> WebT m [PeerInformation]+scottyPeers _ =+    withMetrics peerResponseTime 1 $+    lift $+    getPeersInformation =<< asks (storeManager . webStore . webConfig)  -- | Obtain information about connected peers from peer manager process. getPeersInformation@@ -1256,8 +1510,9 @@  scottyHealth ::        (MonadUnliftIO m, MonadLoggerIO m) => GetHealth -> WebT m HealthCheck-scottyHealth _ = do-    h <- lift $ ask >>= healthCheck+scottyHealth _ =+    withMetrics healthResponseTime 1 $ do+    h <- lift $ asks webConfig >>= healthCheck     unless (isOK h) $ S.status status503     return h @@ -1328,10 +1583,11 @@         $(logErrorS) "Web" $ "Health check failed: " <> t     return hc -scottyDbStats :: MonadLoggerIO m => WebT m ()-scottyDbStats = do+scottyDbStats :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()+scottyDbStats =+    withMetrics dbStatsResponseTime 1 $ do     setHeaders-    db <- lift $ asks (databaseHandle . storeDB . webStore)+    db <- lift $ asks (databaseHandle . storeDB . webStore . webConfig)     statsM <- lift (getProperty db Stats)     S.text $ maybe "Could not get stats" cs statsM @@ -1341,42 +1597,42 @@  -- | Returns @Nothing@ if the parameter is not supplied. Raises an exception on -- parse failure.-paramOptional :: (Param a, Monad m) => WebT m (Maybe a)+paramOptional :: (Param a, MonadIO m) => WebT m (Maybe a) paramOptional = go Proxy   where-    go :: (Param a, Monad m) => Proxy a -> WebT m (Maybe a)+    go :: (Param a, MonadIO m) => Proxy a -> WebT m (Maybe a)     go proxy = do-        net <- lift $ asks (storeNetwork . webStore)+        net <- lift $ asks (storeNetwork . webStore . webConfig)         tsM :: Maybe [Text] <- p `S.rescue` const (return Nothing)         case tsM of             Nothing -> return Nothing -- Parameter was not supplied-            Just ts -> maybe (S.raise err) (return . Just) $ parseParam net ts+            Just ts -> maybe (raise_ err) (return . Just) $ parseParam net ts       where         l = proxyLabel proxy         p = Just <$> S.param (cs l)         err = UserError $ "Unable to parse param " <> cs l  -- | Raises an exception if the parameter is not supplied-param :: (Param a, Monad m) => WebT m a+param :: (Param a, MonadIO m) => WebT m a param = go Proxy   where-    go :: (Param a, Monad m) => Proxy a -> WebT m a+    go :: (Param a, MonadIO m) => Proxy a -> WebT m a     go proxy = do         resM <- paramOptional         case resM of             Just res -> return res             _ ->-                S.raise . UserError $+                raise_ . UserError $                 "The param " <> cs (proxyLabel proxy) <> " was not defined"  -- | Returns the default value of a parameter if it is not supplied. Raises an -- exception on parse failure.-paramDef :: (Default a, Param a, Monad m) => WebT m a+paramDef :: (Default a, Param a, MonadIO m) => WebT m a paramDef = fromMaybe def <$> paramOptional  -- | Does not raise exceptions. Will call @Scotty.next@ if the parameter is -- not supplied or if parsing fails.-paramLazy :: (Param a, Monad m) => WebT m a+paramLazy :: (Param a, MonadIO m) => WebT m a paramLazy = do     resM <- paramOptional `S.rescue` const (return Nothing)     maybe S.next return resM@@ -1385,18 +1641,18 @@ parseBody = do     b <- S.body     case hex b <|> bin (L.toStrict b) of-        Nothing -> S.raise $ UserError "Failed to parse request body"+        Nothing -> raise_ $ UserError "Failed to parse request body"         Just x  -> return x   where     bin = eitherToMaybe . Serialize.decode     hex = bin <=< decodeHex . cs . C.filter (not . isSpace) -parseOffset :: Monad m => WebT m OffsetParam+parseOffset :: MonadIO m => WebT m OffsetParam parseOffset = do     res@(OffsetParam o) <- paramDef-    limits <- lift $ asks webMaxLimits+    limits <- lift $ asks (webMaxLimits . webConfig)     when (maxLimitOffset limits > 0 && fromIntegral o > maxLimitOffset limits) $-        S.raise . UserError $+        raise_ . UserError $         "offset exceeded: " <> show o <> " > " <> show (maxLimitOffset limits)     return res @@ -1420,12 +1676,12 @@         _ <- MaybeT $ getTxData (TxHash h)         return $ AtTx (TxHash h)     start_time q = do-        ch <- lift $ asks (storeChain . webStore)+        ch <- lift $ asks (storeChain . webStore . webConfig)         b <- MaybeT $ blockAtOrBefore ch q         let g = blockDataHeight b         return $ AtBlock g -parseLimits :: Monad m => WebT m LimitsParam+parseLimits :: MonadIO m => WebT m LimitsParam parseLimits = LimitsParam <$> paramOptional <*> parseOffset <*> paramOptional  paramToLimits ::@@ -1434,7 +1690,7 @@     -> LimitsParam     -> WebT m Limits paramToLimits full (LimitsParam limitM o startM) = do-    wl <- lift $ asks webMaxLimits+    wl <- lift $ asks (webMaxLimits . webConfig)     Limits (validateLimit wl full limitM) (fromIntegral o) <$> parseStart startM  validateLimit :: WebLimits -> Bool -> Maybe LimitParam -> Word32@@ -1455,24 +1711,27 @@ runInWebReader ::        MonadIO m     => CacheT (DatabaseReaderT m) a-    -> ReaderT WebConfig m a+    -> ReaderT WebState m a runInWebReader f = do-    bdb <- asks (storeDB . webStore)-    mc <- asks (storeCache . webStore)+    bdb <- asks (storeDB . webStore . webConfig)+    mc <- asks (storeCache . webStore . webConfig)     lift $ runReaderT (withCache mc f) bdb -runNoCache :: MonadIO m => Bool -> ReaderT WebConfig m a -> ReaderT WebConfig m a+runNoCache :: MonadIO m => Bool -> ReaderT WebState m a -> ReaderT WebState m a runNoCache False f = f-runNoCache True f =-    local (\s -> s {webStore = (webStore s) {storeCache = Nothing}}) f+runNoCache True f = local g f+  where+    g s = s { webConfig = h (webConfig s) }+    h c = c { webStore = i (webStore c) }+    i s = s { storeCache = Nothing }  logIt :: (MonadUnliftIO m, MonadLoggerIO m) => m Middleware logIt = do     runner <- askRunInIO     return $ \app req respond -> do-        t1 <- getCurrentTime+        t1 <- systemToUTCTime <$> liftIO getSystemTime         app req $ \res -> do-            t2 <- getCurrentTime+            t2 <- systemToUTCTime <$> liftIO getSystemTime             let d = diffUTCTime t2 t1                 s = responseStatus res                 msg = fmtReq req <> " [" <> fmtStatus s <> " / " <> fmtDiff d <> "]"
test/Haskoin/StoreSpec.hs view
@@ -97,8 +97,8 @@               , storeConfPeerMaxLife = 48 * 3600               , storeConfConnect = dummyPeerConnect net ad               , storeConfCacheRefresh = 750-              , storeConfCacheRetries = 100               , storeConfCacheRetryDelay = 100000+              , storeConfStats = Nothing               }     withStore cfg $ \Store {..} ->         withSubscription storePublisher $ \sub ->