packages feed

grapesy-1.0.0: kvstore/KVStore/Client.hs

module KVStore.Client (runKeyValueClient) where

import Control.Concurrent
import Control.Exception
import Control.Monad
import Data.ByteString (ByteString)
import Data.ByteString qualified as BS
import Data.IORef
import Data.Maybe
import System.Timeout
import Text.Printf

import Network.GRPC.Client
import Network.GRPC.Common

import KVStore.API
import KVStore.API.JSON qualified as JSON
import KVStore.API.Protobuf qualified as Protobuf
import KVStore.Cmdline
import KVStore.Util.Profiling
import KVStore.Util.RandomAccessSet (RandomAccessSet)
import KVStore.Util.RandomAccessSet qualified as RandomAccessSet
import KVStore.Util.RandomGen qualified as RandomGen

{-------------------------------------------------------------------------------
  Top-level
-------------------------------------------------------------------------------}

-- | Run the client for the specified time in seconds
--
-- Print the number of RPC calls.
runKeyValueClient :: Cmdline -> IO ()
runKeyValueClient cmdline@Cmdline{cmdDuration} = do
    statsVar <- newIORef zeroStats
    void $ timeout (cmdDuration * 1_000_000) $ client cmdline statsVar
    putStr . showStats cmdline =<< readIORef statsVar

{-------------------------------------------------------------------------------
  Stats
-------------------------------------------------------------------------------}

data Stats = Stats {
      statsNumCreate   :: !Int
    , statsNumUpdate   :: !Int
    , statsNumRetrieve :: !Int
    , statsNumDelete   :: !Int
    }

zeroStats :: Stats
zeroStats = Stats {
      statsNumCreate   = 0
    , statsNumUpdate   = 0
    , statsNumRetrieve = 0
    , statsNumDelete   = 0
    }

statsTotal :: Stats -> Int
statsTotal stats = sum [
      statsNumCreate   stats
    , statsNumUpdate   stats
    , statsNumRetrieve stats
    , statsNumDelete   stats
    ]

incNumCreate, incNumUpdate, incNumRetrieve, incNumDelete :: Stats -> (Stats, ())
incNumCreate   stats = (stats{statsNumCreate   = statsNumCreate   stats + 1}, ())
incNumUpdate   stats = (stats{statsNumUpdate   = statsNumUpdate   stats + 1}, ())
incNumRetrieve stats = (stats{statsNumRetrieve = statsNumRetrieve stats + 1}, ())
incNumDelete   stats = (stats{statsNumDelete   = statsNumDelete   stats + 1}, ())

showStats :: Cmdline -> Stats -> String
showStats Cmdline{cmdDuration} stats = unlines [
      printf "Did %.3f RPCs/s" countPerSec
    , "Totals:"
    , printf "  %d CREATE"   (statsNumCreate   stats)
    , printf "  %d UPDATE"   (statsNumUpdate   stats)
    , printf "  %d RETRIEVE" (statsNumRetrieve stats)
    , printf "  %d DELETE"   (statsNumDelete   stats)
    ]
  where
    countPerSec :: Double
    countPerSec = fromIntegral (statsTotal stats) / fromIntegral cmdDuration

{-------------------------------------------------------------------------------
  Main client
-------------------------------------------------------------------------------}

-- | Connect to the server and keep calling random RPCs
--
-- Caller should create an 'IORef' initialized to 0, then call 'client' in
-- separate thread, and kill the thread after some amount of time. The number
-- of RPC calls made can then be read off from the 'IORef'.
client :: Cmdline -> IORef Stats -> IO ()
client Cmdline{
           cmdJSON
         , cmdSecure
         , cmdDisableTcpNoDelay
         , cmdPingRateLimit
         , cmdSemaphoreLimit
         } statsVar = do
    knownKeys <- RandomAccessSet.new
    random    <- RandomGen.new
    limiter   <- newQSem (fromMaybe 1 cmdSemaphoreLimit)

    withConnection params server $ \conn -> do
      let kvstore :: KVStore
          kvstore
            | cmdJSON   = JSON.client     conn
            | otherwise = Protobuf.client conn

      forever $ do
        waitQSem limiter
        forkIO $
          doRandomCRUD random knownKeys kvstore `finally` signalQSem limiter
  where
    params :: ConnParams
    params = def {
          connHTTP2Settings = def {
              http2TcpNoDelay            = not cmdDisableTcpNoDelay
            , http2OverridePingRateLimit = cmdPingRateLimit
            }
        }

    server :: Server
    server
      | cmdSecure
      = ServerSecure
          NoServerValidation
          SslKeyLogNone -- Let the server write the log
          Address {
              addressHost      = "127.0.0.1"
            , addressPort      = defaultSecurePort
            , addressAuthority = Nothing
            }

      | otherwise
      = ServerInsecure $ Address {
             addressHost      = "127.0.0.1"
           , addressPort      = defaultInsecurePort
           , addressAuthority = Nothing
           }

    doRandomCRUD ::
         RandomGen.RandomGen
      -> RandomAccessSet Key
      -> KVStore
      -> IO ()
    doRandomCRUD random knownKeys kvstore = do
        -- Pick a random CRUD action to take
        command <- RandomGen.nextInt random 4

        if command == 0 then do
          doCreate kvstore knownKeys
          atomicModifyIORef' statsVar incNumCreate
        else do
          -- If we don't know about any keys, retry with a new random action
          noKnownKeys <- RandomAccessSet.isEmpty knownKeys
          unless noKnownKeys $ do
            case command of
              1 -> do doRetrieve kvstore knownKeys
                      atomicModifyIORef' statsVar incNumRetrieve
              2 -> do doUpdate kvstore knownKeys
                      atomicModifyIORef' statsVar incNumUpdate
              3 -> do doDelete kvstore knownKeys
                      atomicModifyIORef' statsVar incNumDelete
              _ -> error "impossible"

{-------------------------------------------------------------------------------
  Access the various server features
-------------------------------------------------------------------------------}

-- | Create a random key and value
doCreate :: KVStore -> RandomAccessSet Key -> IO ()
doCreate kvstore knownKeys = markRequest "CREATE" $ do
    key   <- createRandomKey knownKeys
    value <- Value <$> randomBytes meanValueSize

    let handleGrpcException :: GrpcException -> IO ()
        handleGrpcException e =
            if grpcError e == GrpcAlreadyExists
              then putStrLn "Key already existed"
              else throwIO e

    handle handleGrpcException $
      create kvstore (key, value)

-- | Retrieve the value of a random key
doRetrieve :: KVStore -> RandomAccessSet Key -> IO ()
doRetrieve kvstore knownKeys = markRequest "RETRIEVE" $ do
    key <- RandomAccessSet.getRandomKey knownKeys

    let handleGrpcException :: GrpcException -> IO ()
        handleGrpcException e =
            if grpcError e == GrpcNotFound then do
              RandomAccessSet.remove knownKeys key
              putStrLn "Key not found"
            else
              throwIO e

    handle handleGrpcException $ do
      Value res <- retrieve kvstore key
      when (BS.length res < 1) $
        error "Invalid response"

-- | Update a random key with a random value
doUpdate :: KVStore -> RandomAccessSet Key -> IO ()
doUpdate kvstore knownKeys = markRequest "UPDATE" $ do
    key   <- RandomAccessSet.getRandomKey knownKeys
    value <- Value <$> randomBytes meanValueSize

    let handleGrpcException :: GrpcException -> IO ()
        handleGrpcException e =
            if grpcError e == GrpcNotFound then do
              RandomAccessSet.remove knownKeys key
              putStrLn "Key not found"
            else
              throwIO e

    handle handleGrpcException $
      update kvstore (key, value)

-- | Delete the value of a random key
doDelete :: KVStore -> RandomAccessSet Key -> IO ()
doDelete kvstore knownKeys = markRequest "DELETE" $ do
    key <- RandomAccessSet.getRandomKey knownKeys
    delete kvstore key
    RandomAccessSet.remove knownKeys key

{-------------------------------------------------------------------------------
  Creating random keys and values
-------------------------------------------------------------------------------}

meanKeySize, meanValueSize :: Int
meanKeySize   = 64
meanValueSize = 65536

-- | Create and add a key to the set of known keys
createRandomKey :: RandomAccessSet Key -> IO Key
createRandomKey knownKeys = loop
  where
    loop :: IO Key
    loop = do
      key   <- Key <$> randomBytes meanKeySize
      added <- RandomAccessSet.add knownKeys key
      if added
        then return key
        else loop

-- | Create an exponentially sized byte string with a mean size
randomBytes :: Int -> IO ByteString
randomBytes mean = do
    random <- RandomGen.new
    d      <- RandomGen.nextDouble random
    let size :: Int
        size = round (fromIntegral mean * (-1 * log (1 - d)))
    RandomGen.getRandomBytes <$> RandomGen.nextBytes random (1 + size)

{-------------------------------------------------------------------------------
  Debugging
-------------------------------------------------------------------------------}

markRequest :: String -> IO a -> IO a
markRequest label =
    bracket_ (markEvent $ "CLIENT start " ++ label)
             (markEvent $ "CLIENT stop "  ++ label)