azure-functions-worker-0.0.0.0: src/Azure/Functions/Commands/Run.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
module Azure.Functions.Commands.Run
where
import Azure.Functions.Internal.Runtime (Runtime, createRuntime, getRuntimeFunction, loadRuntimeFunction, runningFunction)
import qualified Azure.Functions.Internal.Templates as Tpl
import Azure.Functions.Registry (RegisteredFunction (..), Registry (..))
import qualified Azure.Functions.Registry as Registry
import Control.Concurrent.Async (async, link)
import Control.Concurrent.Chan (newChan, readChan, writeChan)
import Control.Monad (forM_, mapM_, void)
import Control.Monad.Except (runExceptT)
import Control.Monad.IO.Class (liftIO)
import qualified Data.Map.Strict as Map
import Data.Text (Text)
import qualified Data.Text as Text
import GHC.Generics (Generic)
import Lens.Family ((&), (.~), (^.))
import Network.GRPC.HTTP2.ProtoLens (RPC (..))
import Options.Applicative
import System.Directory as Dir
import System.Environment (getArgs, getExecutablePath)
import System.FilePath ((</>))
import Network.GRPC.Client as GRPC
import Network.GRPC.Client.Helpers as GRPC
import Network.HTTP2.Client (HostName, PortNumber, TooMuchConcurrency)
import Network.HTTP2.Client.Exceptions (ClientIO)
import Data.ProtoLens.Runtime.Data.ProtoLens (defMessage)
import Proto.FunctionRpc
import qualified Proto.FunctionRpc_Fields as Fields
import Proto.FunctionRpc_Helpers (failureStatus, successStatus, toResponse)
import Data.Version (showVersion)
import Paths_azure_functions_worker (version)
import Azure.Functions.Bindings.Class (fromInvocationRequest, toInvocationResponse)
import Azure.Functions.Bindings.HTTP (HttpRequest (..), HttpResponse (..))
import Azure.Functions.Bindings.ServiceBus (ReceivedMessage (..))
type RequestId = Text
type WorkerId = Text
data Options = Options
{ host :: HostName
, port :: PortNumber
, workerId :: WorkerId
, requestId :: RequestId
} deriving (Show, Generic)
optionsParser :: Parser Options
optionsParser = Options
<$> strOption
( long "host"
<> metavar "HOST"
<> help "Run on the specified host"
)
<*> option auto
( long "port"
<> metavar "PORT"
<> help "Run gRPC channel on the specified port"
)
<*> strOption
( long "workerId"
<> help "Worker ID"
)
<*> strOption
( long "requestId"
<> help "Request ID"
)
<* option (auto @Int)
( long "grpcMaxMessageLength"
<> help "The maximum message length to use for gRPC messages"
)
runCommand :: Registry -> Parser (IO ())
runCommand registry = runRunCommand registry <$> optionsParser
data WorkerState = Unitialised | Started
runRunCommand :: Registry -> Options -> IO ()
runRunCommand registry opts = do
runtime <- createRuntime
outgoingMessageChannel <- newChan
let cfg = GRPC.grpcClientConfigSimple (host opts) (port opts) False
let startMsg = defMessage @StartStream & Fields.workerId .~ (workerId opts)
let initMsg = defMessage @StreamingMessage
& Fields.requestId .~ (requestId opts)
& Fields.maybe'content .~ Just (StreamingMessage'StartStream startMsg)
writeChan outgoingMessageChannel initMsg
let rpc = RPC :: RPC FunctionRpc "eventStream"
void $ runExceptT $ do
client <- GRPC.setupGrpcClient cfg
notTooMuch $ rawGeneralStream rpc client
outgoingMessageChannel -- incoming messages loop state, handler needs to write to the channel
(handleIncoming runtime) -- incoming messages loop
outgoingMessageChannel -- outgoing messages loop state, reads messages from the channel and sends the,
handleOutgoing -- outgoing messages loop
print "-----------------------------------------------------------------------------"
where
handleIncoming runtime chan = \case
RecvMessage msg -> do
liftIO $ runAsync $ do
out <- handleEnvelope registry runtime msg
forM_ out (writeChan chan)
pure chan
_ -> pure chan
handleOutgoing chan = do
msg <- liftIO (readChan chan)
pure (chan, SendMessage Uncompressed msg)
runAsync f = async f >>= link
handleEnvelope :: Registry -> Runtime -> StreamingMessage -> IO [StreamingMessage]
handleEnvelope registry runtime req = do
appendFile "/tmp/msg" ("Request:\n============================================================\n")
appendFile "/tmp/msg" (show req <> "\n\n")
let rid = req ^. Fields.requestId
resp <- case req ^. Fields.maybe'content of
Nothing -> pure []
Just c -> case c of
StreamingMessage'WorkerInitRequest msg -> sequence [toResponse req <$> handleWorkerInit msg]
StreamingMessage'FunctionLoadRequest msg -> sequence [toResponse req <$> handleFunctionLoad registry runtime msg]
StreamingMessage'InvocationRequest msg -> sequence [toResponse req <$> handleInvocation runtime msg]
msg -> pure []
appendFile "/tmp/msg" ("Response:\n------------------------------------------------------------\n")
appendFile "/tmp/msg" (show resp <> "\n\n")
pure resp
handleWorkerInit :: WorkerInitRequest -> IO WorkerInitResponse
handleWorkerInit msg = do
let resp = defMessage @WorkerInitResponse
& Fields.workerVersion .~ Text.pack (showVersion version)
& Fields.result .~ successStatus ("Haskell worker loaded, version: " <> Text.pack (showVersion version))
pure resp
handleFunctionLoad :: Registry -> Runtime -> FunctionLoadRequest -> IO FunctionLoadResponse
handleFunctionLoad registry runtime req = do
let funcName = req ^. Fields.metadata . Fields.name
let funcId = req ^. Fields.functionId
let resp = defMessage @FunctionLoadResponse
& Fields.functionId .~ (req ^. Fields.functionId)
case Registry.getFunction registry funcName of
Nothing ->
pure $ resp & Fields.result .~ failureStatus ("Unable to find function: " <> funcName)
Just func -> do
loadRuntimeFunction runtime funcId func
pure $ resp & Fields.result .~ successStatus ("Loaded function: " <> funcName)
handleInvocation :: Runtime -> InvocationRequest -> IO InvocationResponse
handleInvocation runtime req = do
let funcId = req ^. Fields.functionId
mbFunction <- getRuntimeFunction runtime funcId
case mbFunction of
Nothing -> pure $
defMessage @InvocationResponse
& Fields.invocationId .~ (req ^. Fields.invocationId)
& Fields.result .~ failureStatus ("Unable to find running function with ID: " <> funcId)
Just rfunc -> do
resp <- runningFunction rfunc req
pure $ resp & Fields.invocationId .~ (req ^. Fields.invocationId)
-------------------------------------------------------------------------------
notTooMuch :: ClientIO (Either TooMuchConcurrency a) -> ClientIO a
notTooMuch f = do
ma <- f
case ma of
Left _ -> error "Too much concurrency. TODO: do something smarter"
Right a -> pure a