hal 0.3.2 → 0.3.3
raw patch · 3 files changed
+106/−42 lines, 3 filesdep +conduitdep +conduit-extradep +http-clientPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependencies added: conduit, conduit-extra, http-client
API changes (from Hackage documentation)
- AWS.Lambda.RuntimeClient: getBaseRuntimeRequest :: IO Request
+ AWS.Lambda.RuntimeClient: data RuntimeClientConfig
+ AWS.Lambda.RuntimeClient: getRuntimeClientConfig :: IO RuntimeClientConfig
+ AWS.Lambda.RuntimeClient: instance GHC.Exception.Type.Exception AWS.Lambda.RuntimeClient.JSONParseException
+ AWS.Lambda.RuntimeClient: instance GHC.Show.Show AWS.Lambda.RuntimeClient.JSONParseException
- AWS.Lambda.RuntimeClient: getNextEvent :: FromJSON a => Request -> IO (Response (Either JSONException a))
+ AWS.Lambda.RuntimeClient: getNextEvent :: FromJSON a => RuntimeClientConfig -> IO (Response (Either JSONParseException a))
- AWS.Lambda.RuntimeClient: sendEventError :: Request -> ByteString -> String -> IO ()
+ AWS.Lambda.RuntimeClient: sendEventError :: RuntimeClientConfig -> ByteString -> String -> IO ()
- AWS.Lambda.RuntimeClient: sendEventSuccess :: ToJSON a => Request -> ByteString -> a -> IO ()
+ AWS.Lambda.RuntimeClient: sendEventSuccess :: ToJSON a => RuntimeClientConfig -> ByteString -> a -> IO ()
- AWS.Lambda.RuntimeClient: sendInitError :: Request -> String -> IO ()
+ AWS.Lambda.RuntimeClient: sendInitError :: RuntimeClientConfig -> String -> IO ()
Files
- hal.cabal +5/−2
- src/AWS/Lambda/Runtime.hs +15/−16
- src/AWS/Lambda/RuntimeClient.hs +86/−24
hal.cabal view
@@ -4,10 +4,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: a0d1117010a69cf876345f650da90e9f54e3fdeca036d05fdaf533cb53f43749+-- hash: 3b85d41bf94d4c072cef0d5534d7e8207a495eccfbaad524eaff1b922bcbc8c9 name: hal-version: 0.3.2+version: 0.3.3 synopsis: A runtime environment for Haskell applications running on AWS Lambda. description: This library uniquely supports different types of AWS Lambda Handlers for your needs/comfort with advanced Haskell. Instead of exposing a single function@@ -46,9 +46,12 @@ aeson , base >=4.7 && <5 , bytestring+ , conduit+ , conduit-extra , containers , envy , exceptions+ , http-client , http-conduit , http-types , mtl
src/AWS/Lambda/Runtime.hs view
@@ -28,8 +28,10 @@ mRuntimeWithContext ) where -import AWS.Lambda.RuntimeClient (getBaseRuntimeRequest, getNextEvent,- sendEventError, sendEventSuccess, sendInitError)+import AWS.Lambda.RuntimeClient (RuntimeClientConfig, getNextEvent,+ getRuntimeClientConfig,+ sendEventError, sendEventSuccess,+ sendInitError) import AWS.Lambda.Combinators (withIOInterface, withFallibleInterface, withPureInterface,@@ -51,8 +53,7 @@ import Data.Text (unpack) import Data.Text.Encoding (decodeUtf8) import Data.Time.Clock.POSIX (posixSecondsToUTCTime)-import Network.HTTP.Simple (Request, getResponseBody,- getResponseHeader)+import Network.HTTP.Simple (getResponseBody, getResponseHeader) import System.Environment (setEnv) import System.Envy (decodeEnv) @@ -87,11 +88,11 @@ _ -> Nothing -runtimeLoop :: (HasLambdaContext r, MonadReader r m, MonadCatch m, MonadIO m, FromJSON event, ToJSON result) => Request -> StaticContext ->+runtimeLoop :: (HasLambdaContext r, MonadReader r m, MonadCatch m, MonadIO m, FromJSON event, ToJSON result) => RuntimeClientConfig -> StaticContext -> (event -> m result) -> m ()-runtimeLoop baseRuntimeRequest staticContext fn = do+runtimeLoop runtimeClientConfig staticContext fn = do -- Get an event- nextRes <- liftIO $ getNextEvent baseRuntimeRequest+ nextRes <- liftIO $ getNextEvent runtimeClientConfig -- If we got an event but our requestId is invalid/missing, there's no hope of meaningful recovery let reqIdBS = head $ getResponseHeader "Lambda-Runtime-Aws-Request-Id" nextRes@@ -139,8 +140,8 @@ return $ first (displayException :: SomeException -> String) caughtResult liftIO $ case result of- Right r -> sendEventSuccess baseRuntimeRequest reqIdBS r- Left e -> sendEventError baseRuntimeRequest reqIdBS e+ Right r -> sendEventSuccess runtimeClientConfig reqIdBS r+ Left e -> sendEventError runtimeClientConfig reqIdBS e --TODO: Revisit all names before we put them under contract -- | For any monad that supports IO/catch/Reader LambdaContext.@@ -183,17 +184,15 @@ mRuntimeWithContext :: (HasLambdaContext r, MonadCatch m, MonadReader r m, MonadIO m, FromJSON event, ToJSON result) => (event -> m result) -> m () mRuntimeWithContext fn = do- -- TODO: Hide the implementation details of Request and StaticContext.- -- If we instead have a method that returns an opaque RuntimeClientConfig- -- that encapsulates these details, and then all clientMethods accept- -- the RuntimeClientConfig instead of a baseRuntimeRequest.- baseRuntimeRequest <- liftIO getBaseRuntimeRequest+ -- TODO: Hide the implementation details of StaticContext within+ -- RuntimeClientConfig that encapsulates more details+ runtimeClientConfig <- liftIO getRuntimeClientConfig possibleStaticCtx <- liftIO $ (decodeEnv :: IO (Either String StaticContext)) case possibleStaticCtx of- Left err -> liftIO $ sendInitError baseRuntimeRequest err- Right staticContext -> forever $ runtimeLoop baseRuntimeRequest staticContext fn+ Left err -> liftIO $ sendInitError runtimeClientConfig err+ Right staticContext -> forever $ runtimeLoop runtimeClientConfig staticContext fn -- | For functions that can read the lambda context and use IO within the same monad. --
src/AWS/Lambda/RuntimeClient.hs view
@@ -8,7 +8,8 @@ -} module AWS.Lambda.RuntimeClient (- getBaseRuntimeRequest,+ RuntimeClientConfig,+ getRuntimeClientConfig, getNextEvent, sendEventSuccess, sendEventError,@@ -16,16 +17,29 @@ ) where import Control.Concurrent (threadDelay)-import Control.Exception (displayException, try, throw)-import Data.Aeson (encode)+import Control.Exception (Exception, displayException, throw,+ try)+import Control.Monad (unless)+import Control.Monad.IO.Class (MonadIO, liftIO)+import Data.Aeson (Result (..), Value, encode,+ fromJSON)+import Data.Aeson.Parser (value') import Data.Aeson.Types (FromJSON, ToJSON) import Data.Bifunctor (first) import qualified Data.ByteString as BS+import Data.Conduit (ConduitM, runConduit, yield, (.|))+import Data.Conduit.Attoparsec (sinkParser) import GHC.Generics (Generic (..))-import Network.HTTP.Simple (HttpException, JSONException,- Request, Response,- getResponseStatus, httpJSONEither,- httpNoBody, parseRequest,+import Network.HTTP.Client (BodyReader, HttpException, Manager,+ Request, Response, brRead,+ defaultManagerSettings, httpNoBody,+ managerConnCount,+ managerIdleConnectionCount,+ managerResponseTimeout,+ managerSetProxy, newManager,+ noProxy, parseRequest, responseBody,+ responseTimeoutNone, withResponse)+import Network.HTTP.Simple (getResponseStatus, setRequestBodyJSON, setRequestBodyLBS, setRequestCheckStatus,@@ -43,6 +57,8 @@ instance ToJSON LambdaError +data RuntimeClientConfig = RuntimeClientConfig Request Manager+ -- Exposed Handlers -- TODO: It would be interesting if we could make the interface a sort of@@ -50,14 +66,28 @@ -- things off we get a 'getNextEvent' handler and then the 'getNextEvent' -- handler returns both the 'success' and 'error' handlers. So things like -- baseRequest and reqId are pre-injected.-getBaseRuntimeRequest :: IO Request-getBaseRuntimeRequest = do+getRuntimeClientConfig :: IO RuntimeClientConfig+getRuntimeClientConfig = do awsLambdaRuntimeApi <- getEnv "AWS_LAMBDA_RUNTIME_API"- parseRequest $ "http://" ++ awsLambdaRuntimeApi+ req <- parseRequest $ "http://" ++ awsLambdaRuntimeApi+ man <- newManager+ -- In the off chance that they set a proxy value, we don't want to+ -- use it. There's also no reason to spend time reading env vars.+ $ managerSetProxy noProxy+ $ defaultManagerSettings+ -- This is the most important setting, we must not timeout requests+ { managerResponseTimeout = responseTimeoutNone+ -- We only ever need a single connection, because we'll never make+ -- concurrent requests and never talk to more than one host.+ , managerConnCount = 1+ , managerIdleConnectionCount = 1+ }+ return $ RuntimeClientConfig req man -getNextEvent :: FromJSON a => Request -> IO (Response (Either JSONException a))-getNextEvent baseRuntimeRequest = do- resOrEx <- runtimeClientRetryTry $ httpJSONEither $ toNextEventRequest baseRuntimeRequest++getNextEvent :: FromJSON a => RuntimeClientConfig -> IO (Response (Either JSONParseException a))+getNextEvent rcc@(RuntimeClientConfig baseRuntimeRequest manager) = do+ resOrEx <- runtimeClientRetryTry $ flip httpJSONEither manager $ toNextEventRequest baseRuntimeRequest let checkStatus res = if not $ statusIsSuccessful $ getResponseStatus res then Left "Unexpected Runtime Error: Could not retrieve next event." else@@ -65,13 +95,13 @@ let resOrMsg = first (displayException :: HttpException -> String) resOrEx >>= checkStatus case resOrMsg of Left msg -> do- _ <- sendInitError baseRuntimeRequest msg+ _ <- sendInitError rcc msg error msg Right y -> return y -sendEventSuccess :: ToJSON a => Request -> BS.ByteString -> a -> IO ()-sendEventSuccess baseRuntimeRequest reqId json = do- resOrEx <- runtimeClientRetryTry $ httpNoBody $ toEventSuccessRequest reqId json baseRuntimeRequest+sendEventSuccess :: ToJSON a => RuntimeClientConfig -> BS.ByteString -> a -> IO ()+sendEventSuccess rcc@(RuntimeClientConfig baseRuntimeRequest manager) reqId json = do+ resOrEx <- runtimeClientRetryTry $ flip httpNoBody manager $ toEventSuccessRequest reqId json baseRuntimeRequest let resOrTypedMsg = case resOrEx of Left ex ->@@ -92,17 +122,49 @@ case resOrTypedMsg of Left (Left msg) -> -- If an exception occurs here, we want that to propogate- sendEventError baseRuntimeRequest reqId msg+ sendEventError rcc reqId msg Left (Right msg) -> error msg Right () -> return () -sendEventError :: Request -> BS.ByteString -> String -> IO ()-sendEventError baseRuntimeRequest reqId e =- fmap (const ()) $ runtimeClientRetry $ httpNoBody $ toEventErrorRequest reqId e baseRuntimeRequest+sendEventError :: RuntimeClientConfig -> BS.ByteString -> String -> IO ()+sendEventError (RuntimeClientConfig baseRuntimeRequest manager) reqId e =+ fmap (const ()) $ runtimeClientRetry $ flip httpNoBody manager $ toEventErrorRequest reqId e baseRuntimeRequest -sendInitError :: Request -> String -> IO ()-sendInitError baseRuntimeRequest e =- fmap (const ()) $ runtimeClientRetry $ httpNoBody $ toInitErrorRequest e baseRuntimeRequest+sendInitError :: RuntimeClientConfig -> String -> IO ()+sendInitError (RuntimeClientConfig baseRuntimeRequest manager) e =+ fmap (const ()) $ runtimeClientRetry $ flip httpNoBody manager $ toInitErrorRequest e baseRuntimeRequest++-- Helpers for Requests with JSON Bodies++newtype JSONParseException = JSONParseException { getException :: String } deriving (Show)+instance Exception JSONParseException++httpJSONEither :: FromJSON a => Request -> Manager -> IO (Response (Either JSONParseException a))+httpJSONEither request manager =+ let toEither v =+ case fromJSON v of+ Error e -> Left (JSONParseException e)+ Success decoded -> Right decoded+ in fmap toEither <$> httpValue request manager++httpValue :: Request -> Manager -> IO (Response Value)+httpValue request manager =+ withResponse request manager (\bodyReaderRes -> do+ value <- runConduit $ bodyReaderSource (responseBody bodyReaderRes) .| sinkParser value'+ return $ fmap (const value) bodyReaderRes+ )++bodyReaderSource :: MonadIO m+ => BodyReader+ -> ConduitM i BS.ByteString m ()+bodyReaderSource br =+ loop+ where+ loop = do+ bs <- liftIO $ brRead br+ unless (BS.null bs) $ do+ yield bs+ loop -- Retry Helpers