packages feed

time-warp-1.1.1.0: examples/playground/Main.hs

{-# LANGUAGE DeriveAnyClass        #-}
{-# LANGUAGE DeriveGeneric         #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TupleSections         #-}
{-# LANGUAGE TypeFamilies          #-}

module Main
    ( main
    , yohohoScenario
--    , rpcScenario
    , transferScenario
    , proxyScenario
    , slowpokeScenario
    , closingServerScenario
    , pendingForkStrategy
--    , runEmulation
--    , runReal
    ) where

import           Control.Concurrent.MVar           (newEmptyMVar, putMVar, takeMVar)
import           Control.Concurrent.STM.TVar       (modifyTVar, newTVar, readTVar)
import           Control.Monad                     (forM_, replicateM_, when)
import           Control.Monad.STM                 (atomically)
import           Control.Monad.Trans               (MonadIO (liftIO))
import           Data.Binary                       (Binary, Get, Put, get, put)
import           Data.Conduit                      (yield, (=$=))
import qualified Data.Conduit.List                 as CL
import           Data.Conduit.Serialization.Binary (conduitGet, conduitPut)
import           Data.Data                         (Data)
import           Data.Default                      (def)
import           Data.MessagePack                  (MessagePack (..))
import           Data.Monoid                       ((<>))
import           Data.Text.Buildable               (Buildable (..))
import           Data.Word                         (Word16)
import           Formatting                        (sformat, shown, string, (%))
import           GHC.Generics                      (Generic)

import           System.Wlog                       (LoggerName, Severity (Debug),
                                                    initLogging, logDebug, logInfo,
                                                    usingLoggerName)

import           Control.TimeWarp.Rpc              (BinaryP (..), Binding (..),
                                                    ForkStrategy (..), Listener (..),
                                                    ListenerH (..), Message,
                                                    MonadTransfer (..), NetworkAddress,
                                                    Port, listen, listenH, listenR,
                                                    localhost, messageName', plainBinaryP,
                                                    reconnectPolicy, reply, replyRaw,
                                                    runDialog, runTransfer, runTransferS,
                                                    send, sendH, sendR, setForkStrategy)
import           Control.TimeWarp.Timed            (MonadTimed (wait), Second, after, for,
                                                    fork_, interval, ms, runTimedIO,
                                                    schedule, sec, sec', till)

-- use ghci; this is only for logger debugging
main :: IO ()
main = return ()

{-
runReal :: MsgPackRpc a -> IO a
runReal = runMsgPackRpc

runEmulation :: PureRpc IO a -> IO a
runEmulation scenario = do
    gen <- newStdGen
    runPureRpc delays gen scenario
  where
    delays :: Microsecond
    delays = interval 50 ms
-}

-- * data types

data Ping = Ping
    deriving (Generic, Data, Binary, MessagePack, Message)

instance Buildable Ping where
    build _ = "Ping"

data Pong = Pong
    deriving (Generic, Data, Binary, MessagePack, Message)

instance Buildable Pong where
    build _ = "Pong"

data EpicRequest = EpicRequest
    { num :: Int
    , msg :: String
    } deriving (Generic, Data, Binary, MessagePack)

instance Buildable EpicRequest where
    build EpicRequest{..} = "EpicRequest " <> build num <> " " <> build msg

instance Message EpicRequest

-- * scenarios

-- | Examples management info.
narrator :: LoggerName
narrator = "*"

-- Emulates dialog of two guys, maybe several times, in parallel:
-- 1: Ping
-- 2: Pong
-- 1: EpicRequest ...
-- 2: <prints result>
yohohoScenario :: IO ()
yohohoScenario = runTimedIO $ do
    initLogging Debug
    (saveWorker, killWorkers) <- newNode narrator workersManager

    -- guy 1
    newNode "guy.1" . fork_ $ do
        saveWorker $ listen (AtPort $ guysPort 1)
            [ Listener $ \Pong ->
              do logDebug "Got Pong!"
                 reply $ EpicRequest 14 " men on the dead man's chest"
            ]
        -- guy 1 initiates dialog
        wait (for 100 ms)
        replicateM_ 2 $ do
            send (guy 2) Ping
            logInfo "Sent"


    -- guy 2
    newNode "guy.2" . fork_ $ do
        saveWorker $ listen (AtPort $ guysPort 2)
            [ Listener $ \Ping ->
              do logDebug "Got Ping!"
                 send (guy 1) Pong
            ]
        saveWorker $ listen (AtConnTo $ guy 1)
            [ Listener $ \EpicRequest{..} ->
              do logDebug "Got EpicRequest!"
                 wait (for 0.1 sec')
                 logInfo $ sformat (shown%string) (num + 1) msg
            ]

    wait (till finish)
    newNode narrator killWorkers
    wait (for 100 ms)
  where
    finish :: Second
    finish = 5

    newNode name = usingLoggerName name . runTransfer (pure ()) . runDialog plainBinaryP

    guy :: Word16 -> NetworkAddress
    guy = (localhost, ) . guysPort

    guysPort :: Word16 -> Port
    guysPort = (+10000)


-- | Example of `Transfer` usage
transferScenario :: IO ()
transferScenario = runTimedIO $ do
    initLogging Debug
    (saveWorker, killWorkers) <- newNode narrator workersManager

    newNode "node.server" $
        let listener req = do
                logInfo $ sformat ("Got "%shown) req
                replyRaw $ yield (put $ sformat "Ok!") =$= conduitPut
        in  saveWorker $ listenRaw (AtPort 1234) $
                conduitGet decoder =$= CL.mapM_ listener

    wait (for 100 ms)

    newNode "node.client-1" $
        schedule (after 200 ms) $ do
            saveWorker $ listenRaw (AtConnTo (localhost, 1234)) $
                    conduitGet get =$= CL.mapM_ logInfo
            forM_ ([1..5] :: [Int]) $ \i ->
                sendRaw (localhost, 1234) $ yield i
                                         =$= CL.map Left
                                         =$= CL.map encoder
                                         =$= conduitPut
--                                     =$= awaitForever (\m -> yield "trash" >> yield m)

    newNode "node.client-2" $
        schedule (after 200 ms) $ do
            sendRaw (localhost, 1234) $  CL.sourceList ([1..5] :: [Int])
                                     =$= CL.map (, -1)
                                     =$= CL.map Right
                                     =$= CL.map encoder
                                     =$= conduitPut
            saveWorker $ listenRaw (AtConnTo (localhost, 1234)) $
                conduitGet get =$= CL.mapM_ logInfo

    wait (for 1000 ms)
    newNode narrator killWorkers
    wait (for 100 ms)
  where
    decoder :: Get (Either Int (Int, Int))
    decoder = do
        magic <- get
        when (magic /= magicVal) $
            fail "Missed magic constant!"
        get

    encoder :: Either Int (Int, Int) -> Put
    encoder d = put magicVal >> put d

    magicVal :: Int
    magicVal = 234

    newNode name = usingLoggerName name . runTransfer (pure ())

{-
rpcScenario :: IO ()
rpcScenario = runTimedIO $ do
    liftIO $ initLogging ["server", "cli"] Debug
    usingLoggerName "server" . runTransfer . runBinaryDialog . runRpc $
        work (till finish) $
            serve 1234
                [ Method $ \Ping -> do
                  do logInfo "Got Ping! Wait a sec..."
                     wait (for 1000 ms)
                     logInfo "Replying"
                     return Pong
                ]

    wait (for 100 ms)
    usingLoggerName "client" . runTransfer . runBinaryDialog . runRpc $ do
        Pong <- call (localhost, 1234) Ping
        logInfo "Got Pong!"
    return ()
  where
    finish :: Second
    finish = 5

-}

-- * Blind proxy scenario, illustrates work with headers and raw data.
proxyScenario :: IO ()
proxyScenario = runTimedIO $ do
    liftIO $ initLogging Debug
    (saveWorker, killWorkers) <- newNode narrator workersManager

    lock <- liftIO newEmptyMVar
    let sync act = liftIO (putMVar lock ()) >> act >> liftIO (takeMVar lock)

    -- server
    newNode "server" . fork_ $
        saveWorker $ listenH (AtPort 5678)
            [ ListenerH $ \(h, EpicRequest{..}) -> sync . logInfo $
                sformat ("Got request!: "%shown%" "%shown%"; h = "%shown)
                num msg (int h)
            ]

    -- proxy
    newNode "proxy" . fork_ $
        saveWorker $ listenR (AtPort 1234)
            [ ListenerH $ \(h, EpicRequest _ _) -> sync . logInfo $
                sformat ("Proxy! h = "%shown) h
            ]
            $ \(h, raw) -> do
                when (h < 5) $ do
                    sendR (localhost, 5678) h raw
                    sync $ logInfo $ sformat ("Resend "%shown) h
                return $ even (int h)


    wait (for 100 ms)

    -- client
    newNode "client" . fork_  $
        forM_ [1..10] $
            \i -> sendH (localhost, 1234) (int i) $ EpicRequest 34 "lol"

    wait (till finish)
    newNode narrator killWorkers
    wait (for 100 ms)
  where
    finish :: Second
    finish = 1

    newNode name = usingLoggerName name . runTransfer (pure ()) . runDialog packing

    packing :: BinaryP Int
    packing = BinaryP

    int :: Int -> Int
    int = id

-- | Slowpoke server scenario
slowpokeScenario :: IO ()
slowpokeScenario = runTimedIO $ do
    initLogging Debug
    (saveWorker, killWorkers) <- newNode narrator workersManager

    newNode "server" . fork_ $ do
        wait (for 3 sec)
        saveWorker $ listen (AtPort 1234)
            [ Listener $ \Ping -> logDebug "Got Ping!"
            ]

    newNode "client" . fork_ $ do
        wait (for 100 ms)
        replicateM_ 3 $ send (localhost, 1234) Ping

    wait (till finish)
    newNode narrator killWorkers
    wait (for 100 ms)
  where
    finish :: Second
    finish = 5

    newNode name = usingLoggerName name . runTransferS settings (pure ())
                 . runDialog plainBinaryP

    settings = def
        { reconnectPolicy = \failsInRow -> return $
            if failsInRow < 5 then Just (interval 1 sec) else Nothing
        }

closingServerScenario :: IO ()
closingServerScenario = runTimedIO $ do
    initLogging Debug
    (saveWorker, killWorkers) <- newNode narrator workersManager

    newNode "server" . fork_ $
        saveWorker $ listen (AtPort 1234) []

    wait (for 100 ms)

    newNode "client" $
        replicateM_ 3 $ do
            closer <- listen (AtConnTo (localhost, 1234)) []
            wait (for 500 ms)
            closer

    wait (till finish)
    newNode narrator killWorkers
    wait (for 100 ms)
  where
    finish :: Second
    finish = 3

    newNode name = usingLoggerName name . runTransfer (pure ()) . runDialog plainBinaryP

pendingForkStrategy :: IO ()
pendingForkStrategy = runTimedIO $ do
    initLogging Debug
    (saveWorker, killWorkers) <- newNode narrator workersManager

    newNode "server" . fork_ $
        saveWorker $ setForkStrategy forkStrategy $
            listen (AtPort 1234)
                [ Listener $ \Ping -> do
                    logInfo "Got Ping, wait 1 sec"
                    wait (for 1 sec)
                ]

    wait (for 100 ms)

    newNode "client" . fork_ $ do
        wait (for 100 ms)
        replicateM_ 5 $ send (localhost, 1234) Ping

    wait (till finish)
    newNode narrator killWorkers
    wait (for 100 ms)
  where
    finish :: Second
    finish = 6

    forkStrategy = ForkStrategy $ \msgName act ->
        if msgName == messageName' Ping
        then act        -- execute in-place
        else fork_ act  -- execute in another thread

    newNode name = usingLoggerName name . runTransfer (pure ()) . runDialog plainBinaryP

workersManager :: MonadIO m => m (m (m ()) -> m (), m ())
workersManager = do
    t <- liftIO . atomically $ newTVar []
    let saveWorker action = do
            closer <- action
            liftIO . atomically $ modifyTVar t (closer:)
        killWorkers = sequence_ =<< liftIO (atomically $ readTVar t)

    return (saveWorker, killWorkers)