Skip to content

Commit 298a34f

Browse files
committed
Implement single process
1 parent f46dd8b commit 298a34f

File tree

13 files changed

+210
-54
lines changed

13 files changed

+210
-54
lines changed

app/Main.hs

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,6 @@ module Main
44
) where
55

66
import Aws.Lambda.Runtime
7-
import Control.Monad
8-
import qualified Network.HTTP.Client as Http
9-
10-
11-
httpManagerSettings :: Http.ManagerSettings
12-
httpManagerSettings =
13-
-- We set the timeout to none, as AWS Lambda freezes the containers.
14-
Http.defaultManagerSettings
15-
{ Http.managerResponseTimeout = Http.responseTimeoutNone
16-
}
177

188
main :: IO ()
19-
main = do
20-
manager <- Http.newManager httpManagerSettings
21-
forever (runLambda manager)
9+
main = runLambda IPC

package.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ library:
3737
- Aws.Lambda.Runtime
3838

3939
executables:
40-
bootstrap:
40+
bootstrap-lol:
4141
source-dirs: app
4242
main: Main.hs
4343
dependencies:

src/Aws/Lambda.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ module Aws.Lambda
33
) where
44

55
import Aws.Lambda.Configuration as Reexported
6+
import Aws.Lambda.Runtime as Reexported
67
import Aws.Lambda.Runtime.Context as Reexported

src/Aws/Lambda/Configuration.hs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
{-# OPTIONS_GHC -fno-warn-unused-pattern-binds #-}
22
module Aws.Lambda.Configuration
33
( Main.LambdaOptions(..)
4-
, Main.generate
54
, Main.getRecord
65
, configureLambda
6+
, bootstrapLambda
77
, IPC.returnAndFail
88
, IPC.returnAndSucceed
99
, Dispatch.decodeObj
10+
, DispatchNoIPC.encodeObj
1011
)
1112
where
1213

1314
import qualified Language.Haskell.TH as Meta
1415

1516
import qualified Aws.Lambda.Meta.Dispatch as Dispatch
17+
import qualified Aws.Lambda.Meta.DispatchNoIPC as DispatchNoIPC
1618
import qualified Aws.Lambda.Meta.Main as Main
1719
import qualified Aws.Lambda.Meta.Run as Run
1820
import qualified Aws.Lambda.Runtime.IPC as IPC
@@ -22,6 +24,13 @@ AWS Lambda layer.
2224
-}
2325
configureLambda :: Meta.DecsQ
2426
configureLambda = do
25-
main <- Main.generate
27+
main <- Main.generateIPC
2628
run <- Run.generate
2729
return (main <> [run])
30+
31+
{-| -}
32+
bootstrapLambda :: Meta.DecsQ
33+
bootstrapLambda = do
34+
main <- Main.generateDirectCall
35+
run <- Run.generateNoIPC
36+
return (main <> [run])

src/Aws/Lambda/Meta/Common.hs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module Aws.Lambda.Meta.Common
33
( declarationName
44
, expressionName
55
, getFieldsFrom
6+
, constructorName
67
) where
78

89
import Data.Text (Text)
@@ -19,6 +20,10 @@ declarationName = pure . VarP . mkName . Text.unpack
1920
expressionName :: Text -> Q Exp
2021
expressionName = pure . VarE . mkName . Text.unpack
2122

23+
-- | Helper for defining names for constructors
24+
-- think of @Foo@ in @quux = Foo 3@
25+
constructorName :: Text -> Q Exp
26+
constructorName = pure . ConE . mkName . Text.unpack
2227

2328
-- | Helper for extracting fields of a specified record
2429
-- it expects the constructor name as the first parameter,
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
{-| Dispatcher generation -}
2+
module Aws.Lambda.Meta.DispatchNoIPC
3+
( generate
4+
, decodeObj
5+
, encodeObj
6+
, Runtime.LambdaResult(..)
7+
) where
8+
9+
import Data.Function ((&))
10+
import Data.Text (Text)
11+
import qualified Data.Text as Text
12+
13+
import Data.Aeson
14+
import qualified Data.ByteString.Lazy.Char8 as LazyByteString
15+
import qualified Language.Haskell.TH as Meta
16+
17+
import Aws.Lambda.Meta.Common
18+
import qualified Aws.Lambda.Runtime.Common as Runtime
19+
20+
{-| Helper function that the dispatcher will use to
21+
decode the JSON that comes as an AWS Lambda event into the
22+
appropriate type expected by the handler.
23+
-}
24+
decodeObj :: FromJSON a => String -> a
25+
decodeObj x =
26+
case (eitherDecode $ LazyByteString.pack x) of
27+
Left e -> error e
28+
Right v -> v
29+
30+
{-| Helper function that the dispatcher will use to
31+
decode the JSON that comes as an AWS Lambda event into the
32+
appropriate type expected by the handler.
33+
-}
34+
encodeObj :: ToJSON a => a -> String
35+
encodeObj x = LazyByteString.unpack (encode x)
36+
37+
38+
{-| Generates the dispatcher out of a list of
39+
handler names in the form @src/Foo/Bar.handler@
40+
41+
This dispatcher has a case for each of the handlers that calls
42+
the appropriate qualified function. In the case of the example above,
43+
the dispatcher will call @Foo.Bar.handler@.
44+
-}
45+
generate :: [Text] -> Meta.ExpQ
46+
generate handlerNames = do
47+
caseExp <- expressionName "functionHandler"
48+
matches <- traverse handlerCase handlerNames
49+
unmatched <- unmatchedCase
50+
pure $ Meta.CaseE caseExp (matches <> [unmatched])
51+
52+
handlerCase :: Text -> Meta.MatchQ
53+
handlerCase lambdaHandler = do
54+
let pat = Meta.LitP (Meta.StringL $ Text.unpack lambdaHandler)
55+
body <- [e|do
56+
result <- $(expressionName qualifiedName) (decodeObj $(expressionName "eventObject")) (decodeObj $(expressionName "contextObject"))
57+
-- ($(constructorName "LambdaResult") . encodeObj)
58+
either (pure . Left . encodeObj) (pure . Right . $(constructorName "LambdaResult") . encodeObj) result |]
59+
pure $ Meta.Match pat (Meta.NormalB body) []
60+
where
61+
qualifiedName =
62+
lambdaHandler
63+
& Text.dropWhile (/= '/')
64+
& Text.drop 1
65+
& Text.replace "/" "."
66+
67+
unmatchedCase :: Meta.MatchQ
68+
unmatchedCase = do
69+
let pattern = Meta.WildP
70+
body <- [e|
71+
pure $ Left ("Handler " <> $(expressionName "functionHandler") <> " does not exist on project")
72+
|]
73+
pure $ Meta.Match pattern (Meta.NormalB body) []

src/Aws/Lambda/Meta/Main.hs

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,29 @@
11
{-| main function generation for interoperation with the layer -}
22
module Aws.Lambda.Meta.Main
3-
( LambdaOptions(..)
4-
, generate
3+
( Runtime.LambdaOptions(..)
4+
, generateIPC
5+
, generateDirectCall
56
, Options.getRecord
67
) where
78

8-
import GHC.Generics (Generic)
9-
109
import qualified Language.Haskell.TH as Meta
1110
import qualified Options.Generic as Options
1211

1312
import Aws.Lambda.Meta.Common
14-
15-
-- | Options that the generated main expects
16-
data LambdaOptions = LambdaOptions
17-
{ eventObject :: !String
18-
, contextObject :: !String
19-
, functionHandler :: !String
20-
, executionUuid :: !String
21-
} deriving (Generic, Options.ParseRecord)
13+
import qualified Aws.Lambda.Runtime.Common as Runtime
2214

2315
-- | Generate the main function that the layer will call
24-
generate :: Meta.DecsQ
25-
generate = [d|
16+
generateIPC :: Meta.DecsQ
17+
generateIPC = [d|
2618
$(declarationName "main") = getRecord "" >>= run
2719
|]
20+
21+
generateDirectCall :: Meta.DecsQ
22+
generateDirectCall = [d|
23+
$(declarationName "main") = $(directCallBody)
24+
|]
25+
where
26+
directCallBody =
27+
[e|do
28+
runLambda $ $(constructorName "DirectCall") run
29+
|]

src/Aws/Lambda/Meta/Run.hs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
module Aws.Lambda.Meta.Run
22
( generate
3+
, generateNoIPC
34
) where
45

56
import qualified Language.Haskell.TH as Meta
67

78
import Aws.Lambda.Meta.Common
89
import qualified Aws.Lambda.Meta.Discover as Discover
910
import qualified Aws.Lambda.Meta.Dispatch as Dispatch
11+
import qualified Aws.Lambda.Meta.DispatchNoIPC as DispatchNoIPC
1012

1113
{-| Generate the run function
1214
@@ -20,3 +22,10 @@ generate = do
2022
clause' <- getFieldsFrom "LambdaOptions" ["functionHandler", "contextObject", "eventObject", "executionUuid"]
2123
body <- Dispatch.generate handlers
2224
pure $ Meta.FunD (Meta.mkName "run") [Meta.Clause [clause'] (Meta.NormalB body) []]
25+
26+
generateNoIPC :: Meta.DecQ
27+
generateNoIPC = do
28+
handlers <- Meta.runIO Discover.handlers
29+
clause' <- getFieldsFrom "LambdaOptions" ["functionHandler", "contextObject", "eventObject", "executionUuid"]
30+
body <- DispatchNoIPC.generate handlers
31+
pure $ Meta.FunD (Meta.mkName "run") [Meta.Clause [clause'] (Meta.NormalB body) []]

src/Aws/Lambda/Runtime.hs

Lines changed: 56 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,86 @@
11
module Aws.Lambda.Runtime
22
( runLambda
3+
, Runtime.Mode(..)
4+
, Runtime.LambdaResult(..)
35
) where
46

57
import Control.Exception.Safe.Checked
8+
import Control.Monad (forever)
69
import qualified Network.HTTP.Client as Http
710

11+
import Data.Aeson
12+
import qualified Data.ByteString.Lazy.Char8 as LazyByteString
13+
814
import qualified Aws.Lambda.Runtime.ApiInfo as ApiInfo
915
import qualified Aws.Lambda.Runtime.Context as Context
1016
import qualified Aws.Lambda.Runtime.Environment as Environment
1117
import qualified Aws.Lambda.Runtime.Error as Error
1218
import qualified Aws.Lambda.Runtime.IPC as IPC
1319
import qualified Aws.Lambda.Runtime.Publish as Publish
20+
import qualified Aws.Lambda.Runtime.Common as Runtime
1421

1522
-- | Runs the user @haskell_lambda@ executable and posts back the
16-
-- results
17-
runLambda
18-
:: Http.Manager
19-
-> IO ()
20-
runLambda manager = do
21-
lambdaApi <- Environment.apiEndpoint `catch` variableNotSet
22-
event <- ApiInfo.fetchEvent manager lambdaApi `catch` errorParsing
23-
context <- Context.initialize event `catch` errorParsing `catch` variableNotSet
24-
((invokeAndRun manager lambdaApi event context
25-
`catch` \err -> Publish.parsingError err lambdaApi context manager)
26-
`catch` \err -> Publish.invocationError err lambdaApi context manager)
27-
`catch` \(err :: Error.EnvironmentVariableNotSet) -> Publish.runtimeInitError err lambdaApi context manager
23+
-- results. This is called from the layer's @main@ function.
24+
runLambda :: Runtime.Mode -> IO ()
25+
runLambda mode = do
26+
manager <- Http.newManager httpManagerSettings
27+
forever $ do
28+
lambdaApi <- Environment.apiEndpoint `catch` variableNotSet
29+
event <- ApiInfo.fetchEvent manager lambdaApi `catch` errorParsing
30+
context <- Context.initialize event `catch` errorParsing `catch` variableNotSet
31+
((invokeAndRun mode manager lambdaApi event context
32+
`catch` \err -> Publish.parsingError err lambdaApi context manager)
33+
`catch` \err -> Publish.invocationError err lambdaApi context manager)
34+
`catch` \(err :: Error.EnvironmentVariableNotSet) -> Publish.runtimeInitError err lambdaApi context manager
35+
36+
httpManagerSettings :: Http.ManagerSettings
37+
httpManagerSettings =
38+
-- We set the timeout to none, as AWS Lambda freezes the containers.
39+
Http.defaultManagerSettings
40+
{ Http.managerResponseTimeout = Http.responseTimeoutNone
41+
}
2842

2943
invokeAndRun
3044
:: Throws Error.Parsing
3145
=> Throws Error.Invocation
3246
=> Throws Error.EnvironmentVariableNotSet
33-
=> Http.Manager
47+
=> Runtime.Mode
48+
-> Http.Manager
3449
-> String
3550
-> ApiInfo.Event
3651
-> Context.Context
3752
-> IO ()
38-
invokeAndRun manager lambdaApi event context = do
39-
result <- IPC.invoke (ApiInfo.event event) context
53+
invokeAndRun mode manager lambdaApi event context = do
54+
result <- invokeWithMode mode event context
4055
Publish.result result lambdaApi context manager
4156
`catch` \err -> Publish.invocationError err lambdaApi context manager
4257

58+
invokeWithMode
59+
:: Throws Error.Invocation
60+
=> Throws Error.Parsing
61+
=> Throws Error.EnvironmentVariableNotSet
62+
=> Runtime.Mode
63+
-> ApiInfo.Event
64+
-> Context.Context
65+
-> IO Runtime.LambdaResult
66+
invokeWithMode mode event context =
67+
case mode of
68+
Runtime.IPC -> IPC.invoke (ApiInfo.event event) context
69+
(Runtime.DirectCall f) -> do
70+
handlerName <- Environment.handlerName
71+
let lambdaOptions = Runtime.LambdaOptions
72+
{ eventObject = LazyByteString.unpack $ ApiInfo.event event
73+
, contextObject = LazyByteString.unpack . encode $ context
74+
, functionHandler = handlerName
75+
, executionUuid = "" -- DirectCall doesnt use UUID
76+
}
77+
result <- f lambdaOptions
78+
case result of
79+
Left err ->
80+
throw $ Error.Invocation err
81+
Right value ->
82+
pure value
83+
4384
variableNotSet :: Error.EnvironmentVariableNotSet -> IO a
4485
variableNotSet (Error.EnvironmentVariableNotSet env) =
4586
error ("Error initializing, variable not set: " <> env)

src/Aws/Lambda/Runtime/Common.hs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
module Aws.Lambda.Runtime.Common
2+
( Mode(..)
3+
, LambdaResult(..)
4+
, LambdaOptions(..)
5+
) where
6+
7+
import GHC.Generics (Generic)
8+
import qualified Options.Generic as Options
9+
10+
{-| Mode of calling the user functions.
11+
12+
It can be 'IPC' (inter-process communication), where the
13+
dispatcher will spawn a process with the handlers of the
14+
user. (Used when using the layer)
15+
16+
Or, it can be 'DirectCall', for when the handlers are in
17+
the same process. (The runtime is bootstrapped with the
18+
project).
19+
-}
20+
data Mode
21+
= IPC
22+
| DirectCall (LambdaOptions -> IO (Either String LambdaResult))
23+
-- ^ This horrible signature implies the following
24+
25+
-- | Options that the generated main expects
26+
data LambdaOptions = LambdaOptions
27+
{ eventObject :: !String
28+
, contextObject :: !String
29+
, functionHandler :: !String
30+
, executionUuid :: !String
31+
} deriving (Generic, Options.ParseRecord)
32+
33+
-- | Wrapper type to handle the result of the user
34+
newtype LambdaResult =
35+
LambdaResult String

0 commit comments

Comments
 (0)