Main.hs 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. {-# LANGUAGE DataKinds #-}
  2. {-# LANGUAGE DeriveGeneric #-}
  3. {-# LANGUAGE MultiParamTypeClasses #-}
  4. {-# LANGUAGE OverloadedStrings #-}
  5. {-# LANGUAGE TemplateHaskell #-}
  6. {-# LANGUAGE TypeOperators #-}
  7. module Main (main) where
  8. import Control.Exception (bracket)
  9. import Control.Monad (replicateM)
  10. import Control.Monad.IO.Class (liftIO)
  11. import Data.Aeson ((.=))
  12. import qualified Data.Aeson as Aeson
  13. import qualified Data.ByteString.Lazy as LBS
  14. import Data.Int (Int32)
  15. import Data.List (sortOn)
  16. import Data.Either (fromRight, partitionEithers)
  17. import Data.Monoid ((<>))
  18. import Data.Text (Text)
  19. import qualified Data.Text as Text
  20. import qualified Data.Text.Encoding as TextEnc
  21. import GHC.Exts (IsList (fromList))
  22. import GHC.Generics (Generic)
  23. import qualified Data.Pool as Pool
  24. import qualified Database.MySQL.Base as MySQL
  25. import qualified System.IO.Streams as Streams
  26. import qualified Lucid
  27. import qualified Network.Wai.Handler.Warp as Warp
  28. import Network.HTTP.Media ((//))
  29. import Servant
  30. import Servant.HTML.Lucid (HTML)
  31. import System.Random.MWC (GenIO, createSystemRandom,
  32. uniformR)
  33. import qualified GHC.Conc
  34. import System.Environment (getArgs)
  35. type API =
  36. "json" :> Get '[JSON] Aeson.Value
  37. :<|> "db" :> Get '[JSON] World
  38. :<|> "queries" :> QueryParam "queries" Count :> Get '[JSON] [World]
  39. :<|> "fortune" :> Get '[HTML] (Lucid.Html ())
  40. :<|> "updates" :> QueryParam "queries" Count :> Get '[JSON] [World]
  41. :<|> "plaintext" :> Get '[Plain] LBS.ByteString
  42. api :: Proxy API
  43. api = Proxy
  44. server :: DbPool -> GenIO -> Server API
  45. server pool gen =
  46. json
  47. :<|> singleDb pool gen
  48. :<|> multipleDb pool gen
  49. :<|> fortunes pool
  50. :<|> updates pool gen
  51. :<|> plaintext
  52. run :: Warp.Port -> MySQL.ConnectInfo -> IO ()
  53. run port dbSettings = do
  54. gen <- createSystemRandom
  55. numCaps <- GHC.Conc.getNumCapabilities
  56. let mkPool = Pool.createPool (MySQL.connect dbSettings) MySQL.close numCaps 10 512
  57. bracket mkPool Pool.destroyAllResources $ \pool ->
  58. Warp.run port $ serve api $ server pool gen
  59. main :: IO ()
  60. main = do
  61. [host] <- getArgs
  62. run 7041 $ MySQL.defaultConnectInfoMB4 {
  63. MySQL.ciHost = host,
  64. MySQL.ciDatabase = "hello_world",
  65. MySQL.ciUser = "benchmarkdbuser",
  66. MySQL.ciPassword = "benchmarkdbpass"
  67. }
  68. type DbPool = Pool.Pool MySQL.MySQLConn
  69. type DbRow = [MySQL.MySQLValue]
  70. newtype Count = Count Int
  71. instance FromHttpApiData Count where
  72. parseQueryParam
  73. = pure . Count . fromRight 1 . parseQueryParam
  74. getCount :: Maybe Count -> Int
  75. getCount Nothing = 1
  76. getCount (Just (Count c)) = max 1 (min c 500)
  77. data World = World { wId :: !Int32 , wRandomNumber :: !Int32 }
  78. deriving (Show, Generic)
  79. instance Aeson.ToJSON World where
  80. toEncoding w
  81. = Aeson.pairs
  82. ( "id" .= wId w
  83. <> "randomNumber" .= wRandomNumber w
  84. )
  85. data Fortune = Fortune { fId :: !Int32 , fMessage :: Text }
  86. deriving (Show, Generic)
  87. instance Aeson.ToJSON Fortune where
  88. toEncoding f
  89. = Aeson.pairs
  90. ( "id" .= fId f
  91. <> "message" .= fMessage f
  92. )
  93. intValEnc :: Int32 -> MySQL.MySQLValue
  94. intValEnc = MySQL.MySQLInt32 . fromIntegral
  95. intValDec :: MySQL.MySQLValue -> Either Text Int32
  96. intValDec (MySQL.MySQLInt8U i) = pure . fromIntegral $ i
  97. intValDec (MySQL.MySQLInt8 i) = pure . fromIntegral $ i
  98. intValDec (MySQL.MySQLInt16U i) = pure . fromIntegral $ i
  99. intValDec (MySQL.MySQLInt16 i) = pure . fromIntegral $ i
  100. intValDec (MySQL.MySQLInt32U i) = pure . fromIntegral $ i
  101. intValDec (MySQL.MySQLInt32 i) = pure . fromIntegral $ i
  102. intValDec (MySQL.MySQLInt64U i) = pure . fromIntegral $ i
  103. intValDec (MySQL.MySQLInt64 i) = pure . fromIntegral $ i
  104. intValDec x = Left $ "Expected MySQLInt*, received" <> (Text.pack $ show x)
  105. textValDec :: MySQL.MySQLValue -> Either Text Text
  106. textValDec (MySQL.MySQLText t) = pure t
  107. textValDec x = Left $ "Expected Text, received" <> (Text.pack $ show x)
  108. -- * PlainText without charset
  109. data Plain
  110. instance Accept Plain where contentType _ = "text" // "plain"
  111. instance MimeRender Plain LBS.ByteString where
  112. mimeRender _ = id
  113. {-# INLINE mimeRender #-}
  114. ------------------------------------------------------------------------------
  115. -- * Test 1: JSON serialization
  116. json :: Handler Aeson.Value
  117. json = return . Aeson.Object $ fromList [("message", "Hello, World!")]
  118. {-# INLINE json #-}
  119. -- * Test 2: Single database query
  120. decodeWorld :: DbRow -> Either Text World
  121. decodeWorld [] = Left "MarshalError: Expected 2 columns for World, found 0"
  122. decodeWorld (_:[]) = Left "MarshalError: Expected 2 columns for World, found 1"
  123. decodeWorld (c1:c2:_) = World <$> intValDec c1 <*> intValDec c2
  124. {-# INLINE decodeWorld #-}
  125. extractWorld :: Streams.InputStream DbRow -> IO (Either Text World)
  126. extractWorld rowsS = do
  127. rows <- Streams.toList rowsS
  128. return $ case rows of
  129. [] -> Left "No rows found!"
  130. (row:_) -> decodeWorld row
  131. singleDb :: DbPool -> GenIO -> Handler World
  132. singleDb pool gen = do
  133. v <- liftIO $ uniformR (1, 10000) gen
  134. r <- liftIO $ Pool.withResource pool $ \conn -> do
  135. (_, rowsS) <- MySQL.query conn "SELECT * FROM World WHERE id = ?" [intValEnc v]
  136. extractWorld rowsS
  137. case r of
  138. Left e -> throwError err500 { errBody = LBS.fromStrict $ TextEnc.encodeUtf8 e }
  139. Right world -> return world
  140. {-# INLINE singleDb #-}
  141. -- * Test 3: Multiple database query
  142. multipleDb :: DbPool -> GenIO -> Maybe Count -> Handler [World]
  143. multipleDb pool gen mcount = do
  144. res <- liftIO $ Pool.withResource pool $ \conn -> do
  145. sId <- MySQL.prepareStmt conn "SELECT * FROM World WHERE id = ?"
  146. res <- replicateM (getCount mcount) $ do
  147. v <- uniformR (1, 10000) gen
  148. (_, rowsS) <- MySQL.queryStmt conn sId [intValEnc v]
  149. extractWorld rowsS
  150. MySQL.closeStmt conn sId
  151. return res
  152. let (errs, oks) = partitionEithers res
  153. case errs of
  154. [] -> return oks
  155. e -> throwError err500 { errBody = LBS.fromStrict . TextEnc.encodeUtf8 . Text.pack . show $ e }
  156. {-# INLINE multipleDb #-}
  157. -- * Test 4: Fortunes
  158. decodeFortune :: DbRow -> Either Text Fortune
  159. decodeFortune [] = Left "MarshalError: Expected 2 columns for Fortune, found 0"
  160. decodeFortune (_:[]) = Left "MarshalError: Expected 2 columns for Fortune, found 1"
  161. decodeFortune (c1:c2:_) = Fortune <$> intValDec c1 <*> textValDec c2
  162. {-# INLINE decodeFortune #-}
  163. selectFortunes :: MySQL.MySQLConn -> IO (Either [Text] [Fortune])
  164. selectFortunes conn = do
  165. (_, rowsS) <- MySQL.query_ conn "SELECT * FROM Fortune"
  166. rows <- Streams.toList rowsS
  167. let eFortunes = fmap decodeFortune rows
  168. let (err, oks) = partitionEithers eFortunes
  169. return $ case err of
  170. [] -> pure oks
  171. _ -> Left err
  172. {-# INLINE selectFortunes #-}
  173. fortunes :: DbPool -> Handler (Lucid.Html ())
  174. fortunes pool = do
  175. r <- liftIO $ Pool.withResource pool selectFortunes
  176. case r of
  177. Left e -> throwError err500 { errBody = LBS.fromStrict . TextEnc.encodeUtf8 . Text.pack . show $ e }
  178. Right fs -> return $ do
  179. let new = Fortune 0 "Additional fortune added at request time."
  180. Lucid.doctypehtml_ $ do
  181. Lucid.head_ $ Lucid.title_ "Fortunes"
  182. Lucid.body_ $ do
  183. Lucid.table_ $ do
  184. Lucid.tr_ $ do
  185. Lucid.th_ "id"
  186. Lucid.th_ "message"
  187. mapM_ (\f -> Lucid.tr_ $ do
  188. Lucid.td_ (Lucid.toHtml . show $ fId f)
  189. Lucid.td_ (Lucid.toHtml $ fMessage f)) (sortOn fMessage (new : fs))
  190. {-# INLINE fortunes #-}
  191. -- * Test 5: Updates
  192. updates :: DbPool -> GenIO -> Maybe Count -> Handler [World]
  193. updates pool gen mcount = do
  194. res <- liftIO $ Pool.withResource pool $ \conn -> do
  195. sIdGet <- MySQL.prepareStmt conn "SELECT * FROM World WHERE id = ?"
  196. sIdPut <- MySQL.prepareStmt conn "UPDATE World SET randomNumber = ? WHERE id = ?"
  197. res <- replicateM (getCount mcount) $ do
  198. vGet <- uniformR (1, 10000) gen
  199. vPut <- uniformR (1, 10000) gen
  200. (_, rowsS) <- MySQL.queryStmt conn sIdGet [intValEnc vGet]
  201. eWorld <- extractWorld rowsS
  202. case eWorld of
  203. Left e -> return $ Left e
  204. Right world -> do
  205. _ <- MySQL.executeStmt conn sIdPut [intValEnc vPut, intValEnc vGet]
  206. return . pure $ world { wRandomNumber = vPut }
  207. MySQL.closeStmt conn sIdGet
  208. MySQL.closeStmt conn sIdPut
  209. return res
  210. let (errs, oks) = partitionEithers res
  211. case errs of
  212. [] -> return oks
  213. e -> throwError err500 { errBody = LBS.fromStrict . TextEnc.encodeUtf8 . Text.pack . show $ e }
  214. {-# INLINE updates #-}
  215. -- * Test 6: Plaintext endpoint
  216. plaintext :: Handler LBS.ByteString
  217. plaintext = return "Hello, World!"
  218. {-# INLINE plaintext #-}