|
@@ -0,0 +1,309 @@
|
|
|
+{-# LANGUAGE EmptyDataDecls #-}
|
|
|
+{-# LANGUAGE FlexibleContexts #-}
|
|
|
+{-# LANGUAGE FlexibleInstances #-}
|
|
|
+{-# LANGUAGE GADTs #-}
|
|
|
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
|
|
+{-# LANGUAGE MultiParamTypeClasses #-}
|
|
|
+{-# LANGUAGE OverloadedStrings #-}
|
|
|
+{-# LANGUAGE QuasiQuotes #-}
|
|
|
+{-# LANGUAGE RankNTypes #-}
|
|
|
+{-# LANGUAGE RecordWildCards #-}
|
|
|
+{-# LANGUAGE TemplateHaskell #-}
|
|
|
+{-# LANGUAGE TypeFamilies #-}
|
|
|
+{-# LANGUAGE ViewPatterns #-}
|
|
|
+{-# OPTIONS_GHC -fno-warn-orphans #-}
|
|
|
+module Main (main, resourcesApp, Widget, WorldId) where
|
|
|
+import Blaze.ByteString.Builder
|
|
|
+import Control.Applicative (liftA2)
|
|
|
+import Control.Concurrent (runInUnboundThread)
|
|
|
+import Control.Monad (replicateM)
|
|
|
+import Control.Monad.Logger (runNoLoggingT)
|
|
|
+import Control.Monad.Primitive (PrimState)
|
|
|
+import Control.Monad.Reader (ReaderT)
|
|
|
+import Control.Monad.Trans.Resource (InternalState)
|
|
|
+import Data.Aeson (encode)
|
|
|
+import qualified Data.ByteString.Lazy as L
|
|
|
+import Data.Pool (Pool, createPool)
|
|
|
+import Data.Int (Int64)
|
|
|
+import Data.IORef (newIORef)
|
|
|
+import Data.Function (on)
|
|
|
+import Data.List (sortBy)
|
|
|
+import Data.Pool (withResource)
|
|
|
+import Data.Text (Text)
|
|
|
+import Database.MongoDB (Field ((:=)), (=:))
|
|
|
+import qualified Database.MongoDB as Mongo
|
|
|
+import Database.Persist (Key, PersistEntity,
|
|
|
+ PersistEntityBackend,
|
|
|
+ PersistStore, get, update,
|
|
|
+ (=.))
|
|
|
+import qualified Database.Persist.MySQL as My
|
|
|
+import Database.Persist.TH (mkPersist, mpsGeneric,
|
|
|
+ persistLowerCase, sqlSettings)
|
|
|
+import Network (PortID (PortNumber))
|
|
|
+import Network.HTTP.Types
|
|
|
+import Network.Wai
|
|
|
+import qualified Network.Wai.Handler.Warp as Warp
|
|
|
+import System.Environment (getArgs)
|
|
|
+import System.IO.Unsafe (unsafePerformIO)
|
|
|
+import qualified System.Random.MWC as R
|
|
|
+import Text.Blaze.Html.Renderer.Utf8 (renderHtmlBuilder)
|
|
|
+import Yesod.Core
|
|
|
+
|
|
|
+mkPersist sqlSettings { mpsGeneric = True } [persistLowerCase|
|
|
|
+World sql=World
|
|
|
+ randomNumber Int sql=randomNumber
|
|
|
+|]
|
|
|
+
|
|
|
+mkPersist sqlSettings { mpsGeneric = True } [persistLowerCase|
|
|
|
+Fortune sql=Fortune
|
|
|
+ message Text sql=message
|
|
|
+|]
|
|
|
+
|
|
|
+data App = App
|
|
|
+ { appGen :: !(R.Gen (PrimState IO))
|
|
|
+ , mySqlPool :: !(Pool My.SqlBackend)
|
|
|
+ , mongoDBPool :: !(Pool Mongo.Pipe)
|
|
|
+ }
|
|
|
+
|
|
|
+-- | Not actually using the non-raw mongoDB.
|
|
|
+-- persistent-mongoDB expects a field of '_id', not 'id'
|
|
|
+mkYesod "App" [parseRoutes|
|
|
|
+/json JsonR GET
|
|
|
+
|
|
|
+/db DbR GET
|
|
|
+/dbs/#Int DbsR GET
|
|
|
+!/dbs/#Text DbsDefaultR GET
|
|
|
+
|
|
|
+/mongo/raw/db MongoRawDbR GET
|
|
|
+/mongo/raw/dbs/#Int MongoRawDbsR GET
|
|
|
+!/mongo/raw/dbs/#Text MongoRawDbsDefaultR GET
|
|
|
+
|
|
|
+/updates/#Int UpdatesR GET
|
|
|
+!/updates/#Text UpdatesDefaultR GET
|
|
|
+
|
|
|
+/fortunes FortunesR GET
|
|
|
+
|
|
|
+/plaintext PlaintextR GET
|
|
|
+|]
|
|
|
+
|
|
|
+fakeInternalState :: InternalState
|
|
|
+fakeInternalState = unsafePerformIO $ newIORef $ error "fakeInternalState forced"
|
|
|
+{-# NOINLINE fakeInternalState #-}
|
|
|
+
|
|
|
+instance Yesod App where
|
|
|
+ makeSessionBackend _ = return Nothing
|
|
|
+ {-# INLINE makeSessionBackend #-}
|
|
|
+ shouldLog _ _ _ = False
|
|
|
+ {-# INLINE shouldLog #-}
|
|
|
+ yesodMiddleware = id
|
|
|
+ {-# INLINE yesodMiddleware #-}
|
|
|
+ cleanPath _ = Right
|
|
|
+ {-# INLINE cleanPath #-}
|
|
|
+ yesodWithInternalState _ _ = ($ fakeInternalState)
|
|
|
+ {-# INLINE yesodWithInternalState #-}
|
|
|
+ maximumContentLength _ _ = Nothing
|
|
|
+ {-# INLINE maximumContentLength #-}
|
|
|
+
|
|
|
+getJsonR :: Handler ()
|
|
|
+getJsonR = sendWaiResponse
|
|
|
+ $ responseBuilder
|
|
|
+ status200
|
|
|
+ [("Content-Type", simpleContentType typeJson)]
|
|
|
+ $ copyByteString
|
|
|
+ $ L.toStrict
|
|
|
+ $ encode
|
|
|
+ $ object ["message" .= ("Hello, World!" :: Text)]
|
|
|
+
|
|
|
+
|
|
|
+getDbR :: Handler Value
|
|
|
+getDbR = getDb (intQuery runMySQL My.toSqlKey)
|
|
|
+
|
|
|
+getMongoRawDbR :: Handler Value
|
|
|
+getMongoRawDbR = getDb rawMongoIntQuery
|
|
|
+
|
|
|
+getDbsR :: Int -> Handler Value
|
|
|
+getDbsR cnt = do
|
|
|
+ App {..} <- getYesod
|
|
|
+ multiRandomHandler randomNumber (intQuery runMySQL My.toSqlKey) cnt'
|
|
|
+ where
|
|
|
+ cnt' | cnt < 1 = 1
|
|
|
+ | cnt > 500 = 500
|
|
|
+ | otherwise = cnt
|
|
|
+
|
|
|
+getDbsDefaultR :: Text -> Handler Value
|
|
|
+getDbsDefaultR _ = getDbsR 1
|
|
|
+
|
|
|
+getMongoRawDbsR :: Int -> Handler Value
|
|
|
+getMongoRawDbsR cnt = multiRandomHandler randomNumber rawMongoIntQuery cnt'
|
|
|
+ where
|
|
|
+ cnt' | cnt < 1 = 1
|
|
|
+ | cnt > 500 = 500
|
|
|
+ | otherwise = cnt
|
|
|
+
|
|
|
+getMongoRawDbsDefaultR :: Text -> Handler Value
|
|
|
+getMongoRawDbsDefaultR _ = getMongoRawDbsR 1
|
|
|
+
|
|
|
+getUpdatesR :: Int -> Handler Value
|
|
|
+getUpdatesR cnt = multiRandomHandler randomPair go cnt'
|
|
|
+ where
|
|
|
+ cnt' | cnt < 1 = 1
|
|
|
+ | cnt > 500 = 500
|
|
|
+ | otherwise = cnt
|
|
|
+ go = uncurry (intUpdate runMySQL My.toSqlKey)
|
|
|
+
|
|
|
+getUpdatesDefaultR :: Text -> Handler Value
|
|
|
+getUpdatesDefaultR _ = getUpdatesR 1
|
|
|
+
|
|
|
+randomNumber :: R.Gen (PrimState IO) -> IO Int64
|
|
|
+randomNumber appGen = R.uniformR (1, 10000) appGen
|
|
|
+
|
|
|
+randomPair :: R.Gen (PrimState IO) -> IO (Int64, Int64)
|
|
|
+randomPair appGen = liftA2 (,) (randomNumber appGen) (randomNumber appGen)
|
|
|
+
|
|
|
+getDb :: (Int64 -> Handler Value) -> Handler Value
|
|
|
+getDb query = do
|
|
|
+ app <- getYesod
|
|
|
+ i <- liftIO (randomNumber (appGen app))
|
|
|
+ value <- query i
|
|
|
+ sendWaiResponse
|
|
|
+ $ responseBuilder
|
|
|
+ status200
|
|
|
+ [("Content-Type", simpleContentType typeJson)]
|
|
|
+ $ copyByteString
|
|
|
+ $ L.toStrict
|
|
|
+ $ encode value
|
|
|
+
|
|
|
+
|
|
|
+runMongoDB :: Mongo.Action Handler b -> Handler b
|
|
|
+runMongoDB f = do
|
|
|
+ App {..} <- getYesod
|
|
|
+ withResource mongoDBPool $ \pipe ->
|
|
|
+ Mongo.access pipe Mongo.ReadStaleOk "hello_world" f
|
|
|
+
|
|
|
+runMySQL :: My.SqlPersistT Handler b -> Handler b
|
|
|
+runMySQL f = do
|
|
|
+ App {..} <- getYesod
|
|
|
+ My.runSqlPool f mySqlPool
|
|
|
+
|
|
|
+intQuery :: (MonadIO m, PersistEntity val, PersistStore backend
|
|
|
+ , backend ~ PersistEntityBackend val
|
|
|
+ ) =>
|
|
|
+ (ReaderT backend m (Maybe val) -> m (Maybe (WorldGeneric backend)))
|
|
|
+ -> (Int64 -> Key val)
|
|
|
+ -> Int64 -> m Value
|
|
|
+intQuery db toKey i = do
|
|
|
+ Just x <- db $ get $ toKey i
|
|
|
+ return $ jsonResult (worldRandomNumber x)
|
|
|
+ where
|
|
|
+ jsonResult :: Int -> Value
|
|
|
+ jsonResult n = object ["id" .= i, "randomNumber" .= n]
|
|
|
+
|
|
|
+rawMongoIntQuery :: Mongo.Val v => v -> Handler Value
|
|
|
+rawMongoIntQuery i = do
|
|
|
+ Just x <- runMongoDB $ Mongo.findOne (Mongo.select ["id" =: i] "World")
|
|
|
+ return $ documentToJson x
|
|
|
+
|
|
|
+intUpdate :: (Functor m, Monad m, MonadIO m
|
|
|
+ , PersistStore backend) =>
|
|
|
+ (ReaderT backend m (Maybe (WorldGeneric backend))
|
|
|
+ -> m (Maybe (WorldGeneric backend)))
|
|
|
+ -> (Int64 -> Key (WorldGeneric backend))
|
|
|
+ -> Int64 -> Int64 -> m Value
|
|
|
+intUpdate db toKey i v = do
|
|
|
+ Just x <- db $ get k
|
|
|
+ _ <- db $ fmap (const Nothing) $
|
|
|
+ update k [WorldRandomNumber =. fromIntegral v]
|
|
|
+ return $ object ["id" .= i, "randomNumber" .= v]
|
|
|
+ where
|
|
|
+ k = toKey i
|
|
|
+
|
|
|
+multiRandomHandler :: ToJSON a
|
|
|
+ => (R.Gen (PrimState IO) -> IO b)
|
|
|
+ -> (b -> Handler a)
|
|
|
+ -> Int
|
|
|
+ -> Handler Value
|
|
|
+multiRandomHandler rand operation cnt = do
|
|
|
+ App {..} <- getYesod
|
|
|
+ nums <- liftIO $ replicateM cnt (rand appGen)
|
|
|
+ return . array =<< mapM operation nums
|
|
|
+
|
|
|
+documentToJson :: [Field] -> Value
|
|
|
+documentToJson = object . map toAssoc
|
|
|
+ where
|
|
|
+ toAssoc :: Field -> (Text, Value)
|
|
|
+ toAssoc ("_id" := v) = ("id", toJSON v)
|
|
|
+ toAssoc (l := v) = (l, toJSON v)
|
|
|
+
|
|
|
+instance ToJSON Mongo.Value where
|
|
|
+ toJSON (Mongo.Int32 i) = toJSON i
|
|
|
+ toJSON (Mongo.Int64 i) = toJSON i
|
|
|
+ toJSON (Mongo.Float f) = toJSON f
|
|
|
+ toJSON (Mongo.Doc d) = documentToJson d
|
|
|
+ toJSON s = error $ "no convert for: " ++ show s
|
|
|
+
|
|
|
+getFortunesR :: Handler ()
|
|
|
+getFortunesR = do
|
|
|
+ es <- runMySQL $ My.selectList [] []
|
|
|
+ sendWaiResponse
|
|
|
+ $ responseBuilder status200 [("Content-type", typeHtml)]
|
|
|
+ $ fortuneTemplate (messages es)
|
|
|
+ where
|
|
|
+ messages es = sortBy (compare `on` snd)
|
|
|
+ ((0, "Additional fortune added at request time.") : map stripEntity es)
|
|
|
+ stripEntity e =
|
|
|
+ (My.fromSqlKey (My.entityKey e), fortuneMessage . My.entityVal $ e)
|
|
|
+
|
|
|
+getPlaintextR :: Handler ()
|
|
|
+getPlaintextR = sendWaiResponse
|
|
|
+ $ responseBuilder
|
|
|
+ status200
|
|
|
+ [("Content-Type", simpleContentType typePlain)]
|
|
|
+ $ copyByteString "Hello, World!"
|
|
|
+
|
|
|
+fortuneTemplate :: [(Int64, Text)] -> Builder
|
|
|
+fortuneTemplate messages = renderHtmlBuilder $ [shamlet|
|
|
|
+$doctype 5
|
|
|
+<html>
|
|
|
+ <head>
|
|
|
+ <title>Fortunes
|
|
|
+ <body>
|
|
|
+ <table>
|
|
|
+ <tr>
|
|
|
+ <th>id
|
|
|
+ <th>message
|
|
|
+ $forall message <- messages
|
|
|
+ <tr>
|
|
|
+ <td>#{fst message}
|
|
|
+ <td>#{snd message}
|
|
|
+|]
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+main :: IO ()
|
|
|
+main = R.withSystemRandom $ \gen -> do
|
|
|
+ [cores, host] <- getArgs
|
|
|
+ myPool <- runNoLoggingT $ My.createMySQLPool My.defaultConnectInfo
|
|
|
+ { My.connectUser = "benchmarkdbuser"
|
|
|
+ , My.connectPassword = "benchmarkdbpass"
|
|
|
+ , My.connectDatabase = "hello_world"
|
|
|
+ , My.connectHost = host
|
|
|
+ } 1000
|
|
|
+
|
|
|
+ mongoPool <- createPool
|
|
|
+ (Mongo.connect $ Mongo.Host host $ PortNumber 27017)
|
|
|
+ Mongo.close
|
|
|
+ (read cores) -- what is the optimal stripe count? 1 is said to be a good default
|
|
|
+ 3 -- 3 second timeout
|
|
|
+ 1000
|
|
|
+
|
|
|
+ app <- toWaiAppPlain App
|
|
|
+ { appGen = gen
|
|
|
+ , mySqlPool = myPool
|
|
|
+ , mongoDBPool = mongoPool
|
|
|
+ }
|
|
|
+ runInUnboundThread $ Warp.runSettings
|
|
|
+ ( Warp.setPort 8000
|
|
|
+ $ Warp.setHost "*"
|
|
|
+ $ Warp.setOnException (\_ _ -> return ())
|
|
|
+ Warp.defaultSettings
|
|
|
+ ) app
|