{-# LANGUAGE BangPatterns #-}

{-| RPC test program.

-}

{-

Copyright (C) 2011, 2012, 2013 Google Inc.
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

-}

import Control.Concurrent
import Control.Monad
import System.Console.GetOpt
import System.Environment
import System.IO
import Text.JSON (decode)
import Text.Printf

import Ganeti.BasicTypes
import Ganeti.Common
import Ganeti.Config
import Ganeti.Errors
import Ganeti.JSON
import Ganeti.Objects
import qualified Ganeti.Path as P
import Ganeti.Rpc
import Ganeti.Utils


-- | Command line options structure.
data Options = Options
  { optRpc      :: String   -- ^ RPC to execute
  , optDataFile :: FilePath -- ^ Path to the RPC serialised form
  , optVerbose  :: Bool     -- ^ Verbosity level
  , optStats    :: Bool     -- ^ Whether to show timing stats
  , optCount    :: Int      -- ^ Count of (multi) RPCs to do
  , optBatch    :: Int      -- ^ How many (multi) RPCs to run in parallel
  , optShowHelp :: Bool     -- ^ Just show the help
  , optShowComp :: Bool     -- ^ Just show the completion info
  , optShowVer  :: Bool     -- ^ Just show the program version
  } deriving Show

-- | Default values for the command line options.
defaultOptions :: Options
defaultOptions = Options
  { optRpc      = "version"
  , optDataFile = "rpc.json"
  , optVerbose  = False
  , optStats    = False
  , optCount    = 1
  , optBatch    = 1
  , optShowHelp = False
  , optShowComp = False
  , optShowVer  = False
  }

instance StandardOptions Options where
  helpRequested = optShowHelp
  verRequested  = optShowVer
  compRequested = optShowComp
  requestHelp o = o { optShowHelp = True }
  requestVer  o = o { optShowVer  = True }
  requestComp o = o { optShowComp = True }

-- | The rpcs we support. Sadly this duplicates the RPC list.
data KnownRpc = KRInstanceInfo        RpcCallInstanceInfo
              | KRAllInstancesInfo    RpcCallAllInstancesInfo
              | KRInstanceConsoleInfo RpcCallInstanceConsoleInfo
              | KRInstanceList        RpcCallInstanceList
              | KRNodeInfo            RpcCallNodeInfo
              | KRVersion             RpcCallVersion
              | KRStorageList         RpcCallStorageList
              | KRTestDelay           RpcCallTestDelay
              | KRExportList          RpcCallExportList
                deriving (Show)

-- | The command line options.
options :: [GenericOptType Options]
options =
  [ (Option "r" ["rpc"]
     (ReqArg (\ r o -> Ok o { optRpc = r }) "RPC")
     "the rpc to use [version]",
     OptComplChoices [])
  , (Option "f" ["data-file"]
     (ReqArg (\ f o -> Ok o { optDataFile = f }) "FILE")
     "the rpc serialised form [\"rpc.json\"]",
     OptComplFile)
  , (Option "v" ["verbose"]
     (NoArg (\ opts -> Ok opts { optVerbose = True}))
     "show more information when executing RPCs",
     OptComplNone)
  , (Option "t" ["stats"]
     (NoArg (\ opts -> Ok opts { optStats = True}))
     "show timing information summary",
     OptComplNone)
  , (Option "c" ["count"]
     (reqWithConversion (tryRead "reading count")
      (\count opts -> Ok opts { optCount = count }) "NUMBER")
     "Count of (multi) RPCs to execute [1]",
     OptComplInteger)
  , (Option "b" ["batch"]
     (reqWithConversion (tryRead "reading batch size")
      (\batch opts -> Ok opts { optBatch = batch }) "NUMBER")
     "Parallelisation factor for RPCs [1]",
     OptComplInteger)
  , oShowHelp
  , oShowComp
  , oShowVer
  ]

-- | Arguments we expect
arguments :: [ArgCompletion]
arguments = [ArgCompletion OptComplOneNode 1 Nothing]

-- | Log a message.
logMsg :: MVar () -> String -> IO ()
logMsg outmvar text =
  withMVar outmvar $ \_ -> do
    let p = if null text || last text /= '\n'
              then putStrLn
              else putStr
    p text
    hFlush stdout

-- | Parses a RPC.
parseRpc :: String -> String -> Result KnownRpc
parseRpc "instance_info"      f =
  fromJResult "parsing rpc" (decode f) >>= Ok . KRInstanceInfo
parseRpc "all_instances_info" f =
  fromJResult "parsing rpc" (decode f) >>= Ok . KRAllInstancesInfo
parseRpc "console_instance_info" f =
  fromJResult "parsing rpc" (decode f) >>= Ok . KRInstanceConsoleInfo
parseRpc "instance_list"      f =
  fromJResult "parsing rpc" (decode f) >>= Ok . KRInstanceList
parseRpc "node_info"          f =
  fromJResult "parsing rpc" (decode f) >>= Ok . KRNodeInfo
parseRpc "version"            f =
  fromJResult "parsing rpc" (decode f) >>= Ok . KRVersion
parseRpc "storage_list"       f =
  fromJResult "parsing rpc" (decode f) >>= Ok . KRStorageList
parseRpc "test_delay"         f =
  fromJResult "parsing rpc" (decode f) >>= Ok . KRTestDelay
parseRpc "export_list"        f =
  fromJResult "parsing rpc" (decode f) >>= Ok . KRExportList
parseRpc s _                  = Bad $ "Unknown rpc '" ++ s ++ "'"

-- | Executes a RPC. These duplicate definitions are needed due to the
-- polymorphism of 'executeRpcCall', and the binding of the result
-- based on the input rpc call.
execRpc :: [Node] -> KnownRpc -> IO [[String]]
execRpc n (KRInstanceInfo        v) = formatRpcRes `fmap` executeRpcCall n v
execRpc n (KRAllInstancesInfo    v) = formatRpcRes `fmap` executeRpcCall n v
execRpc n (KRInstanceConsoleInfo v) = formatRpcRes `fmap` executeRpcCall n v
execRpc n (KRInstanceList        v) = formatRpcRes `fmap` executeRpcCall n v
execRpc n (KRNodeInfo            v) = formatRpcRes `fmap` executeRpcCall n v
execRpc n (KRVersion             v) = formatRpcRes `fmap` executeRpcCall n v
execRpc n (KRStorageList         v) = formatRpcRes `fmap` executeRpcCall n v
execRpc n (KRTestDelay           v) = formatRpcRes `fmap` executeRpcCall n v
execRpc n (KRExportList          v) = formatRpcRes `fmap` executeRpcCall n v

-- | Helper to format the RPC result such that it can be printed by
-- 'printTable'.
formatRpcRes :: (Show b) => [(Node, ERpcError b)] -> [[String]]
formatRpcRes = map (\(n, r) -> [nodeName n, either explainRpcError show r])

-- | Main function.
main :: IO ()
main = do
  cmd_args <- getArgs
  (opts, args) <-
    parseOpts defaultOptions cmd_args "rpc-test" options arguments
  rpc <- parseRpc (optRpc opts) `liftM` readFile (optDataFile opts) >>=
         exitIfBad "parsing RPC"
  cfg_file <- P.clusterConfFile
  cfg <- loadConfig  cfg_file>>= exitIfBad "Can't load configuration"
  nodes <- exitIfBad "Can't find node" . errToResult $
            mapM (getNode cfg) args
  token <- newEmptyMVar -- semaphore for batch calls
  outmvar <- newMVar () -- token for stdout non-interleaving
  let logger = if optVerbose opts
                 then logMsg outmvar
                 else const $ return ()
  let batch = [1..optBatch opts]
      count = optCount opts
      rpcs = count * length nodes
  logger $ printf "Will execute %s multi-ops and %s RPCs"
           (show count) (show rpcs)
  tstart <- getCurrentTimeUSec
  _ <- forkIO $ mapM_ (\_ -> putMVar token ()) batch
  mapM_ (\idx -> do
           let str_idx = show idx
           logger $ "Acquiring token for run " ++ str_idx
           _ <- takeMVar token
           forkIO $ do
             start <- getCurrentTimeUSec
             logger $ "Start run " ++ str_idx
             !results <- execRpc nodes rpc
             stop <- getCurrentTimeUSec
             let delta = (fromIntegral (stop - start)::Double) / 1000
             putMVar token ()
             let stats = if optVerbose opts
                           then printf "Done run %d in %7.3fmsec\n" idx delta
                           else ""
                 table = printTable "" ["Node", "Result"]
                           results [False, False]
             logMsg outmvar $ stats ++ table
        ) [1..count]
  mapM_ (\_ -> takeMVar token) batch
  _ <- takeMVar outmvar
  when (optStats opts) $ do
    tstop <- getCurrentTimeUSec
    let delta = (fromIntegral (tstop - tstart) / 1000000)::Double
    printf "Total runtime:     %9.3fs\n" delta :: IO ()
    printf "Total mult-ops:    %9d\n" count :: IO ()
    printf "Total single RPCs: %9d\n" rpcs  :: IO ()
    printf "Multi-ops/sec:     %9.3f\n" (fromIntegral count / delta) :: IO ()
    printf "RPCs/sec:          %9.3f\n" (fromIntegral rpcs / delta) :: IO ()
