haskoin-store-1.9.0: src/Haskoin/Store/Database/Query.hs
-- |
-- Module : Haskoin.Store.Database.Query
-- Copyright : (c) Haskoin Developers
-- License : MIT
-- Maintainer : jprupp@protonmail.ch
-- Stability : experimental
-- Portability : POSIX
--
-- Queries against the RocksDB database, as plain functions in 'IO' that take
-- the 'DatabaseReader' as their first argument.
--
-- These read one layer. On a database of version 20 that is everything,
-- the mempool included, and the tests use them so. On the layout that the
-- store runs on, they give what the chain has and nothing of the mempool,
-- but for its list: "Haskoin.Store.Database.Query.Layered" calls them for
-- the chain's part of its answers, and is what the store answers with.
--
-- = Why plain functions
--
-- The queries used to be the methods of two classes, run in
-- @ReaderT DatabaseReader@, and reached the web server through more layers of
-- @ReaderT@ for the cache. Every caller then
-- had to live in a stack of monad transformers to ask the database anything,
-- and every layer had to lift or re-implement every method. A function that
-- takes the reader and runs in 'IO' can be called from anywhere, needs no
-- instance, and says in its type exactly what it reads from.
--
-- Streams are still conduits, run in 'IO': they are a good fit for merging the
-- sorted indices of many addresses and reading only as far as a page needs.
module Haskoin.Store.Database.Query
( -- * All of them
databaseRead,
-- * Blocks
getBestBlock,
getBlocksAtHeight,
getBlock,
-- * Transactions
getTxData,
getTxRecord,
getManyTxData,
getChainSpender,
getChainSpenders,
withChainSpenders,
getNumTxData,
getSpender,
-- * Addresses
getBalance,
getBalances,
getUnspent,
getAddressTxs,
getAddressesTxs,
getAddressUnspents,
getAddressesUnspents,
-- * Mempool
getMempool,
getMempoolPage,
getMempoolBefore,
-- * Extended public keys
xPubBals,
xPubUnspents,
xPubTxs,
xPubTxCount,
getAddressesTxCount,
-- * Building blocks
-- | What the queries above are made of, for the queries of the layered
-- layout in "Haskoin.Store.Database.Query.Layered", which read the same
-- column families for the part of their answers that is in the chain.
count,
withManyIters,
retrieveManyCF,
joinConduits,
usedAddresses,
)
where
import Conduit
( ConduitT,
concatC,
concatMapC,
dropWhileC,
lift,
mapC,
runConduit,
sinkList,
takeC,
takeWhileC,
yield,
(.|),
)
import Control.Monad (forM, (>=>))
import Data.Bits ((.&.))
import Data.ByteString qualified as BS
import Data.Default (def)
import Data.Functor ((<&>))
import Data.IntMap.Strict qualified as IntMap
import Data.Map.Strict qualified as Map
import Data.Maybe (catMaybes, listToMaybe)
import Data.Ord (Down (..))
import Data.Serialize (Serialize, decode, encode)
import Data.Word (Word32, Word64)
import Database.RocksDB
( ColumnFamily,
DB,
IterConfig (..),
Iterator,
createIteratorConfig,
getCF,
iterKey,
iterLast,
iterNext,
iterPrev,
iterSeek,
iterValid,
multiGetCF,
prefixUpperBound,
withIterCF,
withIterConfig,
)
import Database.RocksDB.Query
( matching,
matchingAsListCF,
matchingSkip,
retrieve,
retrieveCF,
)
import Haskoin
( Address,
BlockHash,
BlockHeight,
OutPoint (..),
Tx (..),
TxHash,
)
import Haskoin.Store.Common
( Limits (..),
Start (..),
StoreRead (..),
applyLimitsC,
deOffset,
joinDescStreams,
mergeDescGroups,
notDeleted,
scanXPubBals,
txRefKey,
unspentKey,
)
import Haskoin.Store.Data
import Haskoin.Store.Database.Reader
import Haskoin.Store.Database.Types
import System.Metrics.StatsD (decrementGauge, incrementCounter, incrementGauge)
import UnliftIO
( bracket,
bracket_,
mask_,
modifyIORef',
newIORef,
readIORef,
)
-- | The queries of a database as a 'StoreRead', for code that takes its
-- queries as a value, such as the web server, which puts the Redis cache in
-- front of some of them. Transactions that were deleted are left out
-- ('notDeleted'); the functions of this module still return them, for the
-- code that imports transactions.
databaseRead :: DatabaseReader -> StoreRead
databaseRead r =
StoreRead
{ net = r.net,
ctx = r.ctx,
getBestBlock = getBestBlock r,
getBlocksAtHeight = getBlocksAtHeight r,
getBlock = getBlock r,
getTxData = fmap notDeleted . getTxData r,
getManyTxData = fmap (map notDeleted) . getManyTxData r,
getSpender = getSpender r,
getBalance = getBalance r,
getUnspent = getUnspent r,
getMempool = getMempool r,
getMempoolPage = getMempoolPage r,
getAddressesTxs = getAddressesTxs r,
getAddressesUnspents = getAddressesUnspents r,
initialGap = r.initGap,
maxGap = r.maxGap,
getNumTxData = fmap (filter (not . (.deleted))) . getNumTxData r,
getBalances = getBalances r,
getAddressTxs = getAddressTxs r,
getAddressUnspents = getAddressUnspents r,
xPubBals = xPubBals r,
xPubUnspents = \_ -> xPubUnspents r,
xPubTxs = \_ -> xPubTxs r,
xPubTxCount = \_ -> xPubTxCount r,
getAddressesTxCount = getAddressesTxCount r
}
-- | Count something in the metrics, if they are enabled.
count :: DatabaseReader -> (DataMetrics -> IO ()) -> IO ()
count r f = mapM_ f r.metrics
-------------------------------------------------------------------------------
-- Blocks
-------------------------------------------------------------------------------
-- | Hash of the best block, if the database has any.
getBestBlock :: DatabaseReader -> IO (Maybe BlockHash)
getBestBlock r = do
count r $ \s -> incrementCounter s.dataBestCount 1
retrieve r.db BestKey
-- | Hashes of the blocks at a height, of the main chain and of side chains.
getBlocksAtHeight :: DatabaseReader -> BlockHeight -> IO [BlockHash]
getBlocksAtHeight r h =
retrieveCF r.db (heightCF r.db) (HeightKey h) >>= \case
Nothing -> return []
Just ls -> do
count r $ \s -> incrementCounter s.dataBlockCount (length ls)
return ls
-- | Data of a block.
getBlock :: DatabaseReader -> BlockHash -> IO (Maybe BlockData)
getBlock r h =
retrieveCF r.db (blockCF r.db) (BlockKey h) >>= \case
Nothing -> return Nothing
Just b -> do
count r $ \s -> incrementCounter s.dataBlockCount 1
return (Just b)
-------------------------------------------------------------------------------
-- Transactions
-------------------------------------------------------------------------------
-- | Data of a transaction, including deleted ones, with the inputs of
-- blocks that spend its outputs.
getTxData :: DatabaseReader -> TxHash -> IO (Maybe TxData)
getTxData r th =
getTxRecord r th >>= \case
Nothing -> return Nothing
Just t -> do
count r $ \s -> incrementCounter s.dataTxCount 1
listToMaybe <$> withChainSpenders r [(th, t)]
-- | The record of a transaction as the database has it, including deleted
-- ones. Since version 22 the spenders of its outputs are not in it
-- ('getChainSpenders'); a database of version 20 or 21 has them there.
-- This is what the rules that write the database read: they must not
-- write back what was read from somewhere else.
getTxRecord :: DatabaseReader -> TxHash -> IO (Maybe TxData)
getTxRecord r th = retrieveCF r.db (txCF r.db) (TxKey th)
-- | Data of many transactions, in the order of the hashes, read with one
-- multi-get, with the inputs of blocks that spend their outputs.
getManyTxData :: DatabaseReader -> [TxHash] -> IO [Maybe TxData]
getManyTxData r ths = do
found <- retrieveManyCF r.db (txCF r.db) (map TxKey ths)
count r $ \s -> incrementCounter s.dataTxCount (length (catMaybes found))
spent <- withChainSpenders r [(h, td) | (h, Just td) <- zip ths found]
return (fill found spent)
where
fill (Just _ : more) (td : tds) = Just td : fill more tds
fill (Nothing : more) tds = Nothing : fill more tds
fill _ _ = []
-- | The input of a block that spends an output, if one does: one read by
-- key, of the group of outputs that it is in, in a database of version 22.
getChainSpender :: DatabaseReader -> OutPoint -> IO (Maybe Spender)
getChainSpender r p =
retrieveCF r.db (spenderCF r.db) (spenderKey p) <&> \case
Just (SpenderVal m) -> IntMap.lookup (fromIntegral p.index) m
Nothing -> Nothing
-- | The inputs of blocks that spend the outputs of a transaction, by
-- output, read with one scan of the keys of the transaction.
getChainSpenders :: DatabaseReader -> TxHash -> IO (IntMap.IntMap Spender)
getChainSpenders r h = do
rows <- matchingAsListCF r.db (spenderCF r.db) (SpenderKeyS h)
return (IntMap.unions [m | (_ :: SpenderKey, SpenderVal m) <- rows])
-- | Records of transactions, with the inputs of blocks that spend their
-- outputs added to whatever spenders the records have themselves, which
-- those of a database older than version 22 do.
--
-- The spenders are read by the keys of the groups of outputs, all the
-- groups of all the transactions in one multi-get. A group is sixteen
-- outputs, so nearly every transaction is one key, and a page of a
-- hundred transactions is one more read of a hundred keys. A scan would
-- need an iterator for each transaction, at ten microseconds apiece. Only
-- a transaction with more groups than 'wideTx' is scanned, so that a
-- payout to thousands is not hundreds of keys to ask for.
--
-- Each record comes with the hash of its transaction, which whoever read
-- it by its key has: working it out again from the record is two rounds
-- of SHA-256 over the whole transaction, and cost more than everything
-- else here together when this was first written without it.
withChainSpenders :: DatabaseReader -> [(TxHash, TxData)] -> IO [TxData]
withChainSpenders _ [] = return []
withChainSpenders r tds = do
let groups td = (fromIntegral (length td.tx.outputs) + spenderGroup - 1) `div` spenderGroup
keys (h, td) = [SpenderKey h g | g <- take (fromIntegral (groups td)) [0 ..]]
narrow (_, td) = groups td <= wideTx
asked = concatMap keys (filter narrow tds)
found <- case asked of
[] -> return []
_ -> retrieveManyCF r.db (spenderCF r.db) asked
let byKey = Map.fromList [(k, m) | (k, Just (SpenderVal m)) <- zip asked found]
keyed x = IntMap.unions [m | k <- keys x, Just m <- [Map.lookup k byKey]]
forM tds $ \x@(h, td) -> do
chain <- if narrow x then return (keyed x) else getChainSpenders r h
return (td :: TxData) {spenders = IntMap.union td.spenders chain}
-- | The number of groups of outputs above which the spenders of a
-- transaction are read with a scan instead of by key.
wideTx :: Word32
wideTx = 16
-- | Transactions whose hash, read as a number, matches a short identifier of
-- the Blockchain.info API. There may be more than one.
getNumTxData :: DatabaseReader -> Word64 -> IO [TxData]
getNumTxData r i = do
let (sk, w) = decodeTxKey i
ls <- matchingAsListCF r.db (txCF r.db) (TxKeyS sk)
let f h =
let bs = encode h
b = BS.head (BS.drop 6 bs)
w' = b .&. 0xf8
in w == w'
txs = [(h, td) | (TxKey h, td) <- ls, f h]
count r $ \s -> incrementCounter s.dataTxCount (length txs)
withChainSpenders r txs
-- | Transaction that spends an output, if any: by its key, or, in a
-- database older than version 22, from the record of the transaction that
-- made the output.
getSpender :: DatabaseReader -> OutPoint -> IO (Maybe Spender)
getSpender r op =
getChainSpender r op >>= \case
Just s -> return (Just s)
Nothing ->
getTxRecord r op.hash <&> \case
Nothing -> Nothing
Just td -> IntMap.lookup (fromIntegral op.index) td.spenders
-------------------------------------------------------------------------------
-- Addresses
-------------------------------------------------------------------------------
-- | Balance of an address, if it was ever used.
getBalance :: DatabaseReader -> Address -> IO (Maybe Balance)
getBalance r a = do
count r $ \s -> incrementCounter s.dataBalanceCount 1
fmap (valToBalance a) <$> retrieveCF r.db (balanceCF r.db) (BalKey a)
-- | Balances of addresses, in the same order, read with one multi-get.
-- Addresses never used get a zero balance.
getBalances :: DatabaseReader -> [Address] -> IO [Balance]
getBalances _ [] = return []
getBalances r as = do
count r $ \s -> incrementCounter s.dataBalanceCount (length as)
vals <- retrieveManyCF r.db (balanceCF r.db) (map BalKey as)
return (zipWith f as vals)
where
f a Nothing = zeroBalance a
f a (Just v) = valToBalance a v
-- | Unspent output, if it exists and is unspent.
getUnspent :: DatabaseReader -> OutPoint -> IO (Maybe Unspent)
getUnspent r p =
retrieveCF r.db (unspentCF r.db) (UnspentKey p) >>= \case
Nothing -> return Nothing
Just val -> do
count r $ \s -> incrementCounter s.dataUnspentCount 1
return (Just (valToUnspent r.net r.ctx p val))
-- | Transactions of an address, newest first.
getAddressTxs :: DatabaseReader -> Address -> Limits -> IO [TxRef]
getAddressTxs r a limits = do
txs <- withIterCFStat r (addrTxCF r.db) (AddrTxKeyA a) $ \it ->
runConduit $
addressConduit r a limits.start it
.| applyLimitsC limits
.| sinkList
count r $ \s -> incrementCounter s.dataAddrTxCount (length txs)
return txs
-- | Transactions of any of the addresses, newest first, each once.
getAddressesTxs :: DatabaseReader -> [Address] -> Limits -> IO [TxRef]
getAddressesTxs r addrs limits =
withManyIters r (addrTxCF r.db) (map AddrTxKeyA addrs) $ \its -> do
txs <- joinConduits txRefKey (zipWith (\a -> addressConduit r a limits.start) addrs its) limits
count r $ \s -> incrementCounter s.dataAddrTxCount (length txs)
return txs
-- | Unspent outputs of an address, newest first.
getAddressUnspents :: DatabaseReader -> Address -> Limits -> IO [Unspent]
getAddressUnspents r a limits = do
us <- withIterCFStat r (addrOutCF r.db) (AddrOutKeyA a) $ \it ->
runConduit $
unspentConduit r a limits.start it
.| applyLimitsC limits
.| sinkList
count r $ \s -> incrementCounter s.dataUnspentCount (length us)
return us
-- | Unspent outputs of any of the addresses, newest first.
getAddressesUnspents :: DatabaseReader -> [Address] -> Limits -> IO [Unspent]
getAddressesUnspents r addrs limits =
withManyIters r (addrOutCF r.db) (map AddrOutKeyA addrs) $ \its -> do
uns <- joinConduits unspentKey (zipWith (\a -> unspentConduit r a limits.start) addrs its) limits
count r $ \s -> incrementCounter s.dataUnspentCount (length uns)
return uns
-- | Stream the transactions of an address, newest first, from a starting point.
-- If the start is a transaction that is not found, nothing is streamed.
addressConduit ::
DatabaseReader ->
Address ->
Maybe Start ->
Iterator ->
ConduitT i TxRef IO ()
addressConduit r a s it =
pipe .| mapC (uncurry f)
where
f (AddrTxKey _ t) () = t
f _ _ = undefined
pipe = case s of
Nothing ->
matching it (AddrTxKeyA a)
Just (AtBlock bh) ->
matchingSkip it (AddrTxKeyA a) (AddrTxKeyB a (BlockRef bh maxBound))
Just (AtTx txh) ->
lift (getTxRecord r txh) >>= \case
Just TxData {block = b@BlockRef {}} ->
matchingSkip it (AddrTxKeyA a) (AddrTxKeyB a b)
Just TxData {block = b@MemRef {}} ->
-- Mempool transactions that arrived in the same second are
-- keyed by hash, so seek to the time and drop those before it.
let start = txRefKey (TxRef b txh)
before (AddrTxKey _ t, ()) = txRefKey t > start
before _ = undefined
in matchingSkip it (AddrTxKeyA a) (AddrTxKeyB a b)
.| (dropWhileC before >> mapC id)
Nothing -> return ()
-- | Stream the unspent outputs of an address, newest first, from a starting
-- point. If the start is a transaction that is not found, nothing is streamed.
unspentConduit ::
DatabaseReader ->
Address ->
Maybe Start ->
Iterator ->
ConduitT i Unspent IO ()
unspentConduit r a s it =
x .| mapC (uncurry (toUnspent r.net r.ctx)) .| concatC
where
x = case s of
Nothing ->
matching it (AddrOutKeyA a)
Just (AtBlock h) ->
matchingSkip it (AddrOutKeyA a) (AddrOutKeyB a (BlockRef h maxBound))
Just (AtTx txh) ->
lift (getTxRecord r txh) >>= \case
Just TxData {block = b@BlockRef {}} ->
matchingSkip it (AddrOutKeyA a) (AddrOutKeyB a b)
Just TxData {block = b@MemRef {}} ->
-- The outputs of the start transaction come first among those
-- of the same second whose hashes are not smaller.
let start = (b, Down (encode (OutPoint txh 0)))
before (AddrOutKey _ ref p, _) = (ref, Down (encode p)) > start
before _ = undefined
in matchingSkip it (AddrOutKeyA a) (AddrOutKeyB a b)
.| (dropWhileC before >> mapC id)
Nothing -> return ()
-------------------------------------------------------------------------------
-- Mempool
-------------------------------------------------------------------------------
-- | The whole mempool, newest first.
getMempool :: DatabaseReader -> IO [(UnixTime, TxHash)]
getMempool r = getMempoolPage r def
-- | Page of the mempool, newest first, from the offset and limit of the
-- 'Limits'; its start is ignored. Reads only the entries of the page from
-- the index by time.
getMempoolPage :: DatabaseReader -> Limits -> IO [(UnixTime, TxHash)]
getMempoolPage r limits = do
count r $ \s -> incrementCounter s.dataMempoolCount 1
withIterCF r.db (mempoolCF r.db) $ \it ->
runConduit $
mempoolNewestC it
.| applyLimitsC limits
.| sinkList
-- | Mempool transactions that entered it before the given time, oldest
-- first, at most as many as given. Reads only those entries of the index.
getMempoolBefore :: DatabaseReader -> UnixTime -> Int -> IO [(UnixTime, TxHash)]
getMempoolBefore r time n =
withIterCF r.db (mempoolCF r.db) $ \it ->
runConduit $
matching it MemTimeKeyS
.| concatMapC entry
.| takeWhileC ((< time) . fst)
.| takeC n
.| sinkList
where
entry (MemTimeKey t h, ()) = Just (t, h)
entry (MemTimeKeyS, ()) = Nothing
-- | Mempool from the index by time, newest first. The index sorts last in
-- the mempool column family, so seek past its prefix and walk backwards
-- until a key is no longer in it.
mempoolNewestC :: Iterator -> ConduitT i (UnixTime, TxHash) IO ()
mempoolNewestC it = do
lift $ do
-- 0x0c is the first prefix after the 0x0b of 'MemTimeKey'
iterSeek it (BS.singleton 0x0c)
iterValid it >>= \case
True -> iterPrev it
False -> iterLast it
go
where
go =
lift (iterKey it) >>= \case
Just k | Right (MemTimeKey t h) <- decode k -> do
yield (t, h)
lift (iterPrev it)
go
_ -> return ()
-------------------------------------------------------------------------------
-- Extended public keys
-------------------------------------------------------------------------------
-- | Balances of the addresses of an extended public key, derived up to the
-- exact gap (see 'scanXPubBals').
xPubBals :: DatabaseReader -> XPubSpec -> IO [XPubBal]
xPubBals r xpub = do
Derived ext chg <- derivedAddrs r xpub
bals <- scanXPubBals (getBalances r) r.initGap r.maxGap ext chg
count r $ \s -> incrementCounter s.dataXPubBals (length bals)
return bals
-- | Unspent outputs of an extended public key, newest first, given its
-- balances from 'xPubBals'.
--
-- The first unspent outputs of each address, at most offset plus limit of
-- them, sorted together. The addresses are read in step, so that each is
-- read only as far as the page needs, rather than decoding the whole quota
-- of every address to keep one page of it.
xPubUnspents :: DatabaseReader -> [XPubBal] -> Limits -> IO [XPubUnspent]
xPubUnspents r xbals limits = do
let quota = case (deOffset limits).limit of
0 -> Nothing
l -> Just l
stream b it =
unspentConduit r b.balance.address limits.start it
.| mapC (\u -> XPubUnspent {path = b.path, unspent = u})
us <- withManyIters r (addrOutCF r.db) (map (AddrOutKeyA . (.balance.address)) cs) $ \its ->
runConduit $
mergeDescGroups (.unspent.block) (unspentKey . (.unspent)) quota (zipWith stream cs its)
.| applyLimitsC limits
.| sinkList
count r $ \s -> incrementCounter s.dataXPubUnspents (length us)
return us
where
cs = filter ((> 0) . (.balance.utxo)) xbals
-- | Transactions of an extended public key, newest first, given its balances
-- from 'xPubBals'.
xPubTxs :: DatabaseReader -> [XPubBal] -> Limits -> IO [TxRef]
xPubTxs r xbals limits = do
txs <- getAddressesTxs r (usedAddresses xbals) limits
count r $ \s -> incrementCounter s.dataXPubTxs (length txs)
return txs
-- | Number of transactions of an extended public key, given its balances
-- from 'xPubBals'.
--
-- Each transaction is counted once even when it touches several addresses.
-- The entries of an address are sorted, and the same transaction has the same
-- key after the address prefix in every address, so the addresses are merged
-- by those raw key bytes and the distinct ones counted, without decoding any
-- key or reading any value.
xPubTxCount :: DatabaseReader -> [XPubBal] -> IO Word32
xPubTxCount r xbals = do
count r $ \s -> incrementCounter s.dataXPubTxCount 1
getAddressesTxCount r (usedAddresses xbals)
-- | Number of distinct transactions of a set of addresses: each transaction
-- is counted once even when it touches several of them. It reads only the
-- keys of the index, once each, as 'xPubTxCount' does, so it costs about as
-- much as reading the references of the transactions once, and much less
-- than paging through them.
getAddressesTxCount :: DatabaseReader -> [Address] -> IO Word32
getAddressesTxCount _ [] = return 0
getAddressesTxCount r as =
withManyIters r (addrTxCF r.db) (map AddrTxKeyA as) $ \its ->
fromIntegral <$> countDistinctSuffixes (zip (map (encode . AddrTxKeyA) as) its)
-- | Addresses of the balances that were ever used.
usedAddresses :: [XPubBal] -> [Address]
usedAddresses = map (.address) . filter (not . nullBalance) . map (.balance)
-- | Number of distinct keys, after their prefix, in a set of iterators that
-- each walk the keys with one prefix, in order.
countDistinctSuffixes :: [(BS.ByteString, Iterator)] -> IO Int
countDistinctSuffixes streams = do
heads <- mapM start streams
go 0 (Map.fromListWith (<>) [(k, [x]) | (k, x) <- catMaybes heads])
where
start x@(pfx, it) = iterSeek it pfx >> current x
current x@(pfx, it) =
iterKey it <&> \case
Just k | pfx `BS.isPrefixOf` k -> Just (BS.drop (BS.length pfx) k, x)
_ -> Nothing
advance x@(_, it) = iterNext it >> current x
go !n m = case Map.minViewWithKey m of
Nothing -> return n
Just ((_, xs), m') -> do
nexts <- catMaybes <$> mapM advance xs
go (n + 1) (foldr (\(k, x) -> Map.insertWith (<>) k [x]) m' nexts)
-------------------------------------------------------------------------------
-- Iterators and multi-gets
-------------------------------------------------------------------------------
-- | Run an action with an iterator on a column family, counting it in the
-- metrics.
--
-- The iterator is for the keys that start with a prefix, and RocksDB
-- stops it where they end ('scanOf').
withIterCFStat :: (Serialize k) => DatabaseReader -> ColumnFamily -> k -> (Iterator -> IO a) -> IO a
withIterCFStat r cf prefix f =
bracket_ (gauge r 1) (gauge r (-1)) $ withIterConfig r.db (Just cf) (scanOf prefix) f
-- | An iterator for the keys that start with a prefix, such as those of one
-- address: one that RocksDB itself stops at the end of them.
--
-- A scan that stops by looking at the key that it has reached makes
-- RocksDB find that key first, and if the keys after the prefix were
-- deleted, it steps over the marker of every one of them to get there.
-- With an upper bound it stops at the bound. The summary of the mempool
-- area and the layout of the chain keep such runs of deleted keys short as
-- things are, so this is for the day that they do not: after a
-- reorganisation that reverts many blocks, or a change of layout.
scanOf :: (Serialize k) => k -> IterConfig
scanOf prefix = def {upperBound = prefixUpperBound (encode prefix)}
-- | Run an action with this many iterators on a column family. They are
-- opened and closed in a single bracket rather than one bracket each: each
-- bracket leaves a frame on the stack, and every call into RocksDB through
-- an iterator walks the stack, so with one bracket per address an xpub of a
-- hundred addresses made each step of each iterator a hundred times slower.
--
-- There is one iterator for each of the prefixes given, in their order,
-- and each is bounded to the keys of its prefix ('scanOf').
withManyIters :: (Serialize k) => DatabaseReader -> ColumnFamily -> [k] -> ([Iterator] -> IO a) -> IO a
withManyIters r cf prefixes f =
bracket_ (gauge r i) (gauge r (negate i)) $
bracket (newIORef []) (readIORef >=> sequence_) $ \ref -> do
its <- forM prefixes $ \prefix -> mask_ $ do
(it, destroy) <- createIteratorConfig r.db (Just cf) (scanOf prefix)
modifyIORef' ref (destroy :)
return it
f its
where
i = length prefixes
-- | Change the gauge of open iterators.
gauge :: DatabaseReader -> Int -> IO ()
gauge r n = count r $ \s ->
if n >= 0 then incrementGauge s.dataIters n else decrementGauge s.dataIters (negate n)
-- | Values of many keys of a column family, read with one multi-get, in
-- the order of the keys.
--
-- One key is read by itself. A multi-get costs about as much as a read to
-- set up and a third of one for each key, as measured, so it is the slower
-- way to read one key, and most requests about one transaction or one
-- address ask for one.
retrieveManyCF :: (Serialize key, Serialize value) => DB -> ColumnFamily -> [key] -> IO [Maybe value]
retrieveManyCF _ _ [] = return []
retrieveManyCF db cf [key] = pure . fmap (either error id . decode) <$> getCF db cf (encode key)
retrieveManyCF db cf keys =
map (fmap (either error id . decode)) <$> multiGetCF db cf (map encode keys)
-- | Merge streams sorted newest first, drop duplicates, and apply the offset
-- and limit.
joinConduits :: (Ord k) => (o -> k) -> [ConduitT () o IO ()] -> Limits -> IO [o]
joinConduits key cs l =
runConduit $ joinDescStreams key cs .| applyLimitsC l .| sinkList