packages feed

instana-haskell-trace-sdk-0.2.0.0: test/integration/Instana/SDK/IntegrationTest/HttpHelper.hs

{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Instana.SDK.IntegrationTest.HttpHelper
  ( agentStubHost
  , agentStubPort
  , agentStubUrl
  , appUrl
  , defaultHeaders
  , doAgentStubRequest
  , doAppRequest
  , parseResponse
  , requestAgentStubAndParse
  , requestAppAndParse
  , retryRequest
  , retryRequestRecovering
  ) where


import           Control.Monad.Catch              (Handler)
import qualified Control.Retry                    as Retry
import qualified Data.Aeson                       as Aeson
import           Data.ByteString                  (ByteString)
import qualified Data.ByteString.Lazy             as LBS
import qualified Network.HTTP.Client              as HTTP
import           Network.HTTP.Types.Header        (Header, HeaderName)
import           Network.HTTP.Types.Method        (Method)

import           Instana.SDK.IntegrationTest.Util (putStrFlush)


-- 100 milliseconds
retryDelay :: Int
retryDelay = 100 * 1000


-- Time to wait for a particular HTTP response: 7.5 seconds.
--
-- We need > 5 seconds, in particular on Travis. Reason:
-- 1) The agent stub and the app under test are started more or less
--    simultaneously
-- 2) The agent stub might not yet be up when the Instana SDK in the app under
--    test starts to look for an agent (which is a perfectly normal situation
--    and should shouldn't not be a problem as the SDK will retry with to find
--    an agent to connect to with fibonacci backoff).
-- 3) When this happens, the first attempt to talk to 127.0.0.1:1302 fails.
-- 4) Next, the SDK attempts to talk to an agent over the default gateway.
-- 5) This never happens locally (at least not on MacOS), as there is no
--    /sbin/ip executable present
-- 6) On Travis (or more generally on most Linux systems), the SDK actually
--    tries the default gateway because that executable _is_ present. It will
--    also find a default gateway (let's say, for example, 10.20.0.1).
-- 7) That host is reachable so an HTTP request to 10.20.0.1:1302 is attempted
--    (1302 is the configured agent port in the integration tests).
-- 8) If something exists at 10.20.0.1:1302 it never sends a response. Or there
--    is nothing there but somehow the request does not fail immediately.
--    Either way, trying this HTTP request eats up around 5 seconds. Why 5
--    seconds? 5 seconds is:
--    a) the timeout the SDK sets itself (see Instana/SDK/SDK.hs, value for
--       HTTP.managerResponseTimeout),
--    b) also the standard HTTP client timeout (see
--    https://hackage.haskell.org/package/http-client-0.1.0.0/docs/Network-HTTP-Client-Manager.html - managerResponseTimeout).
-- 9) When the HTTP client timeout is up and the HTTP call to 10.20.0.1 has
--    finally failed, the SDK realizes it can't talk to the default gateway. It
--    would probably try the configured host (127.0.0.1:1302) again soon, and
--    then find the agent stub there, unless this timeout here kicks in earlier.
--
-- Thus, we need more than 5 seconds.
maxRetryDelay :: Int
maxRetryDelay = 7500 * 1000


agentStubHost :: String
agentStubHost = "127.0.0.1"


agentStubPort :: Int
agentStubPort = 1302


agentStubBaseUrl :: String
agentStubBaseUrl =
  "http://" ++ agentStubHost ++ ":" ++ (show agentStubPort) ++ "/"


agentStubUrl :: String -> String
agentStubUrl path =
  agentStubBaseUrl ++ path


appHost :: String
appHost = "127.0.0.1"


appPort :: Int
appPort = 1207


appBaseUrl :: String
appBaseUrl =
  "http://" ++ appHost ++ ":" ++ (show appPort) ++ "/"


appUrl :: String -> String
appUrl path =
  appBaseUrl ++ path


defaultHeaders :: [(HeaderName, ByteString)]
defaultHeaders =
  [ ("Accept", "application/json")
  , ("Content-Type", "application/json; charset=UTF-8")
  ]


requestAgentStubAndParse ::
  Aeson.FromJSON a =>
  String
  -> ByteString
  -> IO (Either String a)
requestAgentStubAndParse path method =
  requestAndParse path method [] agentStubUrl


requestAppAndParse ::
  Aeson.FromJSON a =>
  String
  -> ByteString
  -> [Header]
  -> IO (Either String a)
requestAppAndParse path method headers =
  requestAndParse path method headers appUrl


doAgentStubRequest ::
  String
  -> Method
  -> IO (HTTP.Response LBS.ByteString)
doAgentStubRequest path method =
  doRequest path method [] agentStubUrl


doAppRequest ::
  String
  -> Method
  -> [Header]
  -> IO (HTTP.Response LBS.ByteString)
doAppRequest path method headers =
  doRequest path method headers appUrl


requestAndParse ::
  Aeson.FromJSON a =>
  String
  -> Method
  -> [Header]
  -> (String -> String)
  -> IO (Either String a)
requestAndParse path method headers urlCreator = do
  response <- doRequest path method headers urlCreator
  parseResponse response


doRequest ::
  String
  -> Method
  -> [Header]
  -> (String -> String)
  -> IO (HTTP.Response LBS.ByteString)
doRequest path method headers urlCreator = do
  httpManager <- HTTP.newManager $
    HTTP.defaultManagerSettings { HTTP.managerConnCount = 5 }
  let
    url = urlCreator path
  defaultRequestSettings <- HTTP.parseUrlThrow url
  let
    request = defaultRequestSettings
       { HTTP.method = method
       , HTTP.requestHeaders = defaultHeaders ++ headers
       }
  HTTP.httpLbs request httpManager


parseResponse ::
  Aeson.FromJSON a
  => HTTP.Response LBS.ByteString
  -> IO (Either String a)
parseResponse response = do
  let
    body = HTTP.responseBody response
    parsed = Aeson.decode body
    result =
      case parsed of
        Just p  -> Right p
        Nothing -> Left "Could not parse response."
  return result


retryRequestRecovering ::
  IO (HTTP.Response LBS.ByteString)
  -> IO (HTTP.Response LBS.ByteString)
retryRequestRecovering request =
  let
    retryPolicy :: Retry.RetryPolicyM IO =
      Retry.limitRetriesByCumulativeDelay maxRetryDelay $
        Retry.constantDelay retryDelay
    reportHttpError :: Bool -> HTTP.HttpException -> Retry.RetryStatus -> IO ()
    reportHttpError _ _ _ = putStrFlush "."
    retryOnAnyHttpError :: HTTP.HttpException -> IO Bool
    retryOnAnyHttpError _ = return True
    retryOnAnyStatus :: Retry.RetryStatus -> Handler IO Bool
    retryOnAnyStatus = Retry.logRetries retryOnAnyHttpError reportHttpError

  in
    Retry.recovering
       retryPolicy
       [retryOnAnyStatus]
       (const request)


retryRequest ::
  forall a.
  Show a =>
  (a -> Bool)
  -> IO (Either String a)
  -> IO (Either String a)
retryRequest retryUntil executeRequestAndParseResponse = do
  let
    retryPolicy :: Retry.RetryPolicyM IO =
      Retry.limitRetriesByCumulativeDelay maxRetryDelay $
        Retry.constantDelay retryDelay
    retryCheck :: Retry.RetryStatus -> Either String a -> IO Bool
    retryCheck _ decodedResponse = do
      putStrFlush "."
      case decodedResponse of
        Left _ ->
          return True
        Right value ->
          return $ not $ retryUntil value
  lastResponse <- Retry.retrying
     retryPolicy
     retryCheck
     (const executeRequestAndParseResponse)
  case lastResponse of
    Left _ ->
      return lastResponse
    Right lastValue ->
      if retryUntil lastValue
      then
        return lastResponse
      else
        return $
          Left $
            "HTTP request was successful, but the expected value " ++
            "has not been obtained. The last response was: " ++
            show lastValue