packages feed

haskoin-store-1.15.0: src/Haskoin/Store/Web/Pieces.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}

-- |
-- Module      : Haskoin.Store.Web.Pieces
-- Copyright   : (c) Haskoin Developers
-- License     : MIT
-- Maintainer  : jprupp@protonmail.ch
-- Stability   : experimental
-- Portability : POSIX
--
-- A response that is written while it is read.
--
-- A handler usually reads everything it answers with and then encodes it.
-- For a long list of large items, such as thousands of whole transactions,
-- that makes the client wait for the last item before it gets the first,
-- and makes the server hold the whole list for as long as the client takes
-- to read it. Here the list comes in pieces, each an action that reads some
-- of the items, and the response is written piece by piece.
--
-- = What it gives and what it does not
--
-- Measured on 5,000 transactions of one address, 15 MB of JSON: the first
-- byte left after 11 ms where it took 96 ms, and sixteen such requests at
-- once took 197 MB of memory where they took 1,014 MB. The whole response
-- took as long as before: most of that time goes into encoding JSON, which
-- reading in pieces does not change, but encoding them on several cores
-- does (see below). A short answer gains nothing, and should stay with the
-- routes that read first.
--
-- = Why it is safe
--
-- The order of things makes it so. All that can refuse the request, and the
-- first piece, come before the status line, so an error there is answered
-- as an error. A read that fails later can only cut the response short,
-- which a client sees as a response that does not end. So the pieces are
-- for what the database itself names, such as the transactions listed in a
-- block or under an address: those are there, and their reads are not
-- expected to fail. What the client names, such as a list of transaction
-- hashes, may not be there, and is read whole before anything is answered.
--
-- While a piece is written the next ones are read, so that neither the
-- database nor the client waits for the other. Nothing of the database is
-- held between pieces, so a slow client costs a few pieces of memory and no
-- more. The price is that the pieces are not read from one view of the
-- database: an item may change, or go, between two of them.
--
-- = Encoding on several cores
--
-- Most of the time of a long answer goes into encoding its JSON, and one
-- thread doing it for a whole answer leaves the other cores idle. So each
-- piece is encoded, down to its bytes, in the thread that reads it, and
-- several pieces are read and encoded at once, as many as 'piecesResponse'
-- is told. What has to be done in order, such as the comma between the
-- items of two pieces, which depends on whether the pieces before had any,
-- is left for the writer, and is cheap: a 'Piece' is an action in two
-- steps, the first one that may run beside the others, and the second one
-- that runs in order. A response holds at most that many pieces, and one
-- more being written.
--
-- Measured with @scripts/bench-web.sh@ on 5,000 transactions of one
-- address, eight cores, eight pieces at once: one request at a time took
-- 77 ms where it took 176 ms, for 16% more CPU time. With eight requests at
-- once, enough to keep every core busy without it, as many were answered,
-- 5% fewer. So it is for a server with cores to spare, which is the usual
-- case: it shortens the wait, and does not make more answers out of the
-- same cores.
--
-- = The alternative
--
-- Lazy input and output, a lazy 'Data.ByteString.Lazy.ByteString' whose
-- chunks read the database when the server asks for them, would do the same
-- with less code. It was rejected: an error would come out of pure code,
-- and nothing would say when a read happens or ends.
module Haskoin.Store.Web.Pieces
  ( -- * Responses
    piecesResponse,
    ahead,

    -- * Pieces
    Piece,
    piece,
    pieced,
    listPieces,
    around,
    hexOf,
  )
where

import Control.Exception (evaluate)
import Data.Aeson.Encoding (Encoding, fromEncoding)
import Data.ByteString.Builder (Builder, lazyByteString, toLazyByteString)
import Data.ByteString.Lazy qualified as L
import Data.Bytes.Put (runPutL)
import Data.Bytes.Serial (Serial (..))
import Haskoin.Util (hexBuilder)
import Data.IORef (modifyIORef', newIORef, readIORef, writeIORef)
import Data.List (intersperse)
import Data.Sequence (Seq (..), (|>))
import Network.HTTP.Types (ResponseHeaders, status200)
import Network.Wai (Response, responseStream)
import UnliftIO (asyncWithUnmask, cancel, finally, mask_, wait)

-- | A piece of a response. Its outer action reads what the piece is made of
-- and encodes it to bytes, and may run at the same time as those of other
-- pieces. The action that it gives runs in order, right before the piece is
-- written, and gives the bytes to write. It should be cheap: it is there
-- for what depends on the pieces before, such as a comma.
newtype Piece = Piece (IO (IO Builder))

-- | A piece from an action that reads its bytes. They are encoded in the
-- thread that reads them, so the action that it gives only hands them over.
piece :: IO Builder -> Piece
piece act = Piece $ do
  bytes <- rendered =<< act
  return (return (lazyByteString bytes))

-- | The bytes of a builder, all of them made, so that the work of making
-- them is done where this is run.
rendered :: Builder -> IO L.ByteString
rendered b = do
  let bytes = toLazyByteString b
  _ <- evaluate (L.length bytes)
  return bytes

-- | The response made of pieces: the bytes of each, in order. The first
-- piece is read before the response is made, and its errors are thrown
-- here. The others are read while the response is written, up to the
-- number given at once, as 'ahead' does. The action given runs when the
-- response ends, however it ends. The response has no @Content-Length@.
piecesResponse :: Int -> ResponseHeaders -> IO () -> [Piece] -> IO Response
piecesResponse n headers done pieces = do
  (first, rest) <- case pieces of
    [] -> return (mempty, [])
    Piece p : more -> do
      bytes <- p >>= id
      return (bytes, more)
  return . responseStream status200 headers $ \write flush ->
    let send (Piece p) = p
        out next = next >>= write >> flush
     in (write first >> flush >> ahead n (map send rest) out) `finally` done

-- | Run actions, up to the number given at once, and hand their results to
-- the last action in order, as each comes. A failure of one is thrown here,
-- in order too: the results before it are handed over first. Actions still
-- running when this ends, however it ends, are cancelled, and a result is
-- let go once it is handed over.
ahead :: Int -> [IO a] -> (a -> IO ()) -> IO ()
ahead n acts consume = do
  running <- newIORef Empty
  let start act = mask_ $ do
        a <- asyncWithUnmask (\unmask -> unmask act)
        modifyIORef' running (|> a)
      go pending =
        readIORef running >>= \case
          Empty -> return ()
          a :<| others -> do
            x <- wait a
            writeIORef running others
            case pending of
              [] -> consume x >> go []
              act : more -> start act >> consume x >> go more
      (first, later) = splitAt (max 1 n) acts
  (mapM_ start first >> go later) `finally` (readIORef running >>= mapM_ cancel)

-- | Cut a list of keys into pieces of a size, each read with the action
-- given: the pieces of items that are read by key.
pieced :: Int -> ([k] -> IO b) -> [k] -> [IO b]
pieced size fetch = map fetch . cut
  where
    cut [] = []
    cut ks = let (part, more) = splitAt (max 1 size) ks in part : cut more

-- | The pieces of a JSON array from pieces of its items: the same bytes as
-- the list of all the items encoded at once. A piece may have no items.
-- The items of each piece are encoded where it is read; whether a comma
-- comes before them, which depends on whether an item came in the pieces
-- before, is settled in order, as 'piecesResponse' runs the pieces.
listPieces :: (b -> Encoding) -> [IO [b]] -> IO [Piece]
listPieces enc pieces = do
  begun <- newIORef False
  let encoded part = Piece $ do
        items <- part
        bytes <- rendered (mconcat (intersperse "," (map (fromEncoding . enc) items)))
        return $ do
          was <- readIORef begun
          let has = not (null items)
          writeIORef begun (was || has)
          return ((if was && has then "," else mempty) <> lazyByteString bytes)
  return (around "[" "]" (map encoded pieces))

-- | Pieces with bytes before the first and after the last, without a piece
-- more: the first piece stays the one that is read before the response.
around :: Builder -> Builder -> [Piece] -> [Piece]
around open close = \case
  [] -> [piece (return (open <> close))]
  [only] -> [wrap open close only]
  first : more -> wrap open mempty first : ending more
  where
    ending [] = []
    ending [final] = [wrap mempty close final]
    ending (p : more) = p : ending more
    wrap before after (Piece p) = Piece (fmap (fmap (\b -> before <> b <> after)) p)

-- | The serialization of a value, in hexadecimal.
hexOf :: (Serial a) => a -> Builder
hexOf = hexBuilder . runPutL . serialize