Skip to content

Commit ddee38c

Browse files
authored
LambdaRuntimeClient works with Invocation (#19)
### Motivation: - We want to store different entities that are needed when executing a handler within the LambdaContext (Logger, EventLoop, ByteBufferAllocator, …) - Currently the LambdaRuntimeClient creates the LambdaContext. Having the LambdaContext with the Logger, EventLoop and ByteBufferAllocator be created from the LambdaRuntimeClient feels to me too much for me. - Conceptionally the Lambda control plane api call is “get next Invocation” (API naming) ### Changes: - LambdaRuntimeClient responds with an Invocation and does not use the LambdaContext at all anymore. - LambdaRunner creates the LambdaContext with the Invocation, Logger and EventLoop. - LambdaContext has been renamed to Lambda.Context - Lambda.Context is a class now, since it is conceptionally not a value type and might be passed around a lot - Lambda.Context properties `traceId`, `invokedFunctionArn`, `deadline` are not optional anymore since they will be always set when executing a lambda - Creating an Invocation can fail with LambdaRuntimeClientError.invocationMissingHeader(String), if non optional headers are not present - the test MockLambdaServer and the performance test MockServer always return headers for deadline, traceId and function arn (static for now – could be changed with Behaviour flag?!) ### Open ends: - we will need to build some kind of Deadline into the context (See also #9 - probably for a different PR) - we have a stupid mapping between ByteBuffer and [UInt8] in the LambdaRunner for now (marked with two TODOs). I don’t want to change this in this PR since it will lead to huge merge conflicts down the road with the potentiall API changes we have in mind.
1 parent 08f75f5 commit ddee38c

13 files changed

+138
-91
lines changed

Sources/MockServer/main.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,12 @@ internal final class HTTPHandler: ChannelInboundHandler {
108108
case .json:
109109
responseBody = "{ \"body\": \"\(requestId)\" }"
110110
}
111-
responseHeaders = [(AmazonHeaders.requestID, requestId)]
111+
responseHeaders = [
112+
(AmazonHeaders.requestID, requestId),
113+
(AmazonHeaders.invokedFunctionARN, "arn:aws:lambda:us-east-1:123456789012:function:custom-runtime"),
114+
(AmazonHeaders.traceID, "Root=1-5bef4de7-ad49b0e87f6ef6c87fc2e700;Parent=9a9197af755a6419;Sampled=1"),
115+
(AmazonHeaders.deadline, String(Date(timeIntervalSinceNow: 60).timeIntervalSince1970 * 1000)),
116+
]
112117
} else if request.head.uri.hasSuffix("/response") {
113118
responseStatus = .accepted
114119
} else {

Sources/SwiftAwsLambda/Lambda+Codable.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,15 @@ public typealias LambdaCodableCallback<Out> = (LambdaCodableResult<Out>) -> Void
5050

5151
/// A processing closure for a Lambda that takes an `In` and returns an `Out` via `LambdaCodableCallback<Out>` asynchronously,
5252
/// having `In` and `Out` extending `Decodable` and `Encodable` respectively.
53-
public typealias LambdaCodableClosure<In, Out> = (LambdaContext, In, LambdaCodableCallback<Out>) -> Void
53+
public typealias LambdaCodableClosure<In, Out> = (Lambda.Context, In, LambdaCodableCallback<Out>) -> Void
5454

5555
/// A processing protocol for a Lambda that takes an `In` and returns an `Out` via `LambdaCodableCallback<Out>` asynchronously,
5656
/// having `In` and `Out` extending `Decodable` and `Encodable` respectively.
5757
public protocol LambdaCodableHandler: LambdaHandler {
5858
associatedtype In: Decodable
5959
associatedtype Out: Encodable
6060

61-
func handle(context: LambdaContext, payload: In, callback: @escaping LambdaCodableCallback<Out>)
61+
func handle(context: Lambda.Context, payload: In, callback: @escaping LambdaCodableCallback<Out>)
6262
var codec: LambdaCodableCodec<In, Out> { get }
6363
}
6464

@@ -79,7 +79,7 @@ public class LambdaCodableCodec<In: Decodable, Out: Encodable> {
7979

8080
/// Default implementation of `Encodable` -> `[UInt8]` encoding and `[UInt8]` -> `Decodable' decoding
8181
public extension LambdaCodableHandler {
82-
func handle(context: LambdaContext, payload: [UInt8], callback: @escaping (LambdaResult) -> Void) {
82+
func handle(context: Lambda.Context, payload: [UInt8], callback: @escaping (LambdaResult) -> Void) {
8383
switch self.codec.decode(payload) {
8484
case .failure(let error):
8585
return callback(.failure(Errors.requestDecoding(error)))
@@ -133,7 +133,7 @@ private struct LambdaClosureWrapper<In: Decodable, Out: Encodable>: LambdaCodabl
133133
self.closure = closure
134134
}
135135

136-
public func handle(context: LambdaContext, payload: In, callback: @escaping LambdaCodableCallback<Out>) {
136+
public func handle(context: Lambda.Context, payload: In, callback: @escaping LambdaCodableCallback<Out>) {
137137
self.closure(context, payload, callback)
138138
}
139139
}

Sources/SwiftAwsLambda/Lambda+String.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,16 @@ public typealias LambdaStringResult = Result<String, Error>
4646
public typealias LambdaStringCallback = (LambdaStringResult) -> Void
4747

4848
/// A processing closure for a Lambda that takes a `String` and returns a `LambdaStringResult` via `LambdaStringCallback` asynchronously.
49-
public typealias LambdaStringClosure = (LambdaContext, String, LambdaStringCallback) -> Void
49+
public typealias LambdaStringClosure = (Lambda.Context, String, LambdaStringCallback) -> Void
5050

5151
/// A processing protocol for a Lambda that takes a `String` and returns a `LambdaStringResult` via `LambdaStringCallback` asynchronously.
5252
public protocol LambdaStringHandler: LambdaHandler {
53-
func handle(context: LambdaContext, payload: String, callback: @escaping LambdaStringCallback)
53+
func handle(context: Lambda.Context, payload: String, callback: @escaping LambdaStringCallback)
5454
}
5555

5656
/// Default implementation of `String` -> `[UInt8]` encoding and `[UInt8]` -> `String' decoding
5757
public extension LambdaStringHandler {
58-
func handle(context: LambdaContext, payload: [UInt8], callback: @escaping LambdaCallback) {
58+
func handle(context: Lambda.Context, payload: [UInt8], callback: @escaping LambdaCallback) {
5959
self.handle(context: context, payload: String(decoding: payload, as: UTF8.self)) { result in
6060
switch result {
6161
case .success(let string):
@@ -73,7 +73,7 @@ private struct LambdaClosureWrapper: LambdaStringHandler {
7373
self.closure = closure
7474
}
7575

76-
func handle(context: LambdaContext, payload: String, callback: @escaping LambdaStringCallback) {
76+
func handle(context: Lambda.Context, payload: String, callback: @escaping LambdaStringCallback) {
7777
self.closure(context, payload, callback)
7878
}
7979
}

Sources/SwiftAwsLambda/Lambda.swift

Lines changed: 35 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,38 @@ public enum Lambda {
7777
}
7878
}
7979

80+
public class Context {
81+
// from aws
82+
public let requestId: String
83+
public let traceId: String
84+
public let invokedFunctionArn: String
85+
public let deadline: String
86+
public let cognitoIdentity: String?
87+
public let clientContext: String?
88+
// utility
89+
public let logger: Logger
90+
91+
internal init(requestId: String,
92+
traceId: String,
93+
invokedFunctionArn: String,
94+
deadline: String,
95+
cognitoIdentity: String? = nil,
96+
clientContext: String? = nil,
97+
logger: Logger) {
98+
self.requestId = requestId
99+
self.traceId = traceId
100+
self.invokedFunctionArn = invokedFunctionArn
101+
self.cognitoIdentity = cognitoIdentity
102+
self.clientContext = clientContext
103+
self.deadline = deadline
104+
// mutate logger with context
105+
var logger = logger
106+
logger[metadataKey: "awsRequestId"] = .string(requestId)
107+
logger[metadataKey: "awsTraceId"] = .string(traceId)
108+
self.logger = logger
109+
}
110+
}
111+
80112
private final class Lifecycle {
81113
private let eventLoop: EventLoop
82114
private let logger: Logger
@@ -258,7 +290,7 @@ public typealias LambdaResult = Result<[UInt8], Error>
258290
public typealias LambdaCallback = (LambdaResult) -> Void
259291

260292
/// A processing closure for a Lambda that takes a `[UInt8]` and returns a `LambdaResult` result type asynchronously.
261-
public typealias LambdaClosure = (LambdaContext, [UInt8], LambdaCallback) -> Void
293+
public typealias LambdaClosure = (Lambda.Context, [UInt8], LambdaCallback) -> Void
262294

263295
/// A result type for a Lambda initialization.
264296
public typealias LambdaInitResult = Result<Void, Error>
@@ -270,7 +302,7 @@ public typealias LambdaInitCallBack = (LambdaInitResult) -> Void
270302
public protocol LambdaHandler {
271303
/// Initializes the `LambdaHandler`.
272304
func initialize(callback: @escaping LambdaInitCallBack)
273-
func handle(context: LambdaContext, payload: [UInt8], callback: @escaping LambdaCallback)
305+
func handle(context: Lambda.Context, payload: [UInt8], callback: @escaping LambdaCallback)
274306
}
275307

276308
extension LambdaHandler {
@@ -280,40 +312,6 @@ extension LambdaHandler {
280312
}
281313
}
282314

283-
public struct LambdaContext {
284-
// from aws
285-
public let requestId: String
286-
public let traceId: String?
287-
public let invokedFunctionArn: String?
288-
public let cognitoIdentity: String?
289-
public let clientContext: String?
290-
public let deadline: String?
291-
// utliity
292-
public let logger: Logger
293-
294-
public init(requestId: String,
295-
traceId: String? = nil,
296-
invokedFunctionArn: String? = nil,
297-
cognitoIdentity: String? = nil,
298-
clientContext: String? = nil,
299-
deadline: String? = nil,
300-
logger: Logger) {
301-
self.requestId = requestId
302-
self.traceId = traceId
303-
self.invokedFunctionArn = invokedFunctionArn
304-
self.cognitoIdentity = cognitoIdentity
305-
self.clientContext = clientContext
306-
self.deadline = deadline
307-
// mutate logger with context
308-
var logger = logger
309-
logger[metadataKey: "awsRequestId"] = .string(requestId)
310-
if let traceId = traceId {
311-
logger[metadataKey: "awsTraceId"] = .string(traceId)
312-
}
313-
self.logger = logger
314-
}
315-
}
316-
317315
@usableFromInline
318316
internal typealias LambdaLifecycleResult = Result<Int, Error>
319317

@@ -323,7 +321,7 @@ private struct LambdaClosureWrapper: LambdaHandler {
323321
self.closure = closure
324322
}
325323

326-
func handle(context: LambdaContext, payload: [UInt8], callback: @escaping LambdaCallback) {
324+
func handle(context: Lambda.Context, payload: [UInt8], callback: @escaping LambdaCallback) {
327325
self.closure(context, payload, callback)
328326
}
329327
}

Sources/SwiftAwsLambda/LambdaRunner.swift

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,17 +54,36 @@ internal struct LambdaRunner {
5454
// 1. request work from lambda runtime engine
5555
return self.runtimeClient.requestWork(logger: logger).peekError { error in
5656
logger.error("could not fetch work from lambda runtime engine: \(error)")
57-
}.flatMap { context, payload in
57+
}.flatMap { invocation, payload in
5858
// 2. send work to handler
59+
let context = Lambda.Context(logger: logger, eventLoop: self.eventLoop, invocation: invocation)
5960
logger.debug("sending work to lambda handler \(self.lambdaHandler)")
61+
62+
// TODO: This is just for now, so that we can work with ByteBuffers only
63+
// in the LambdaRuntimeClient
64+
let bytes = [UInt8](payload.readableBytesView)
6065
return self.lambdaHandler.handle(eventLoop: self.eventLoop,
6166
lifecycleId: self.lifecycleId,
6267
offload: self.offload,
6368
context: context,
64-
payload: payload).map { (context, $0) }
65-
}.flatMap { context, result in
69+
payload: bytes)
70+
.map {
71+
// TODO: This mapping shall be removed as soon as the LambdaHandler protocol
72+
// works with ByteBuffer? instead of [UInt8]
73+
let mappedResult: Result<ByteBuffer, Error>
74+
switch $0 {
75+
case .success(let bytes):
76+
var buffer = ByteBufferAllocator().buffer(capacity: bytes.count)
77+
buffer.writeBytes(bytes)
78+
mappedResult = .success(buffer)
79+
case .failure(let error):
80+
mappedResult = .failure(error)
81+
}
82+
return (invocation, mappedResult)
83+
}
84+
}.flatMap { invocation, result in
6685
// 3. report results to runtime engine
67-
self.runtimeClient.reportResults(logger: logger, context: context, result: result).peekError { error in
86+
self.runtimeClient.reportResults(logger: logger, invocation: invocation, result: result).peekError { error in
6887
logger.error("failed reporting results to lambda runtime engine: \(error)")
6988
}
7089
}.always { result in
@@ -88,7 +107,7 @@ private extension LambdaHandler {
88107
return promise.futureResult
89108
}
90109

91-
func handle(eventLoop: EventLoop, lifecycleId: String, offload: Bool, context: LambdaContext, payload: [UInt8]) -> EventLoopFuture<LambdaResult> {
110+
func handle(eventLoop: EventLoop, lifecycleId: String, offload: Bool, context: Lambda.Context, payload: [UInt8]) -> EventLoopFuture<LambdaResult> {
92111
// offloading so user code never blocks the eventloop
93112
let promise = eventLoop.makePromise(of: LambdaResult.self)
94113
if offload {
@@ -106,6 +125,18 @@ private extension LambdaHandler {
106125
}
107126
}
108127

128+
private extension Lambda.Context {
129+
convenience init(logger: Logger, eventLoop: EventLoop, invocation: Invocation) {
130+
self.init(requestId: invocation.requestId,
131+
traceId: invocation.traceId,
132+
invokedFunctionArn: invocation.invokedFunctionArn,
133+
deadline: invocation.deadlineDate,
134+
cognitoIdentity: invocation.cognitoIdentity,
135+
clientContext: invocation.clientContext,
136+
logger: logger)
137+
}
138+
}
139+
109140
// TODO: move to nio?
110141
private extension EventLoopFuture {
111142
// callback does not have side effects, failing with original result

Sources/SwiftAwsLambda/LambdaRuntimeClient.swift

Lines changed: 38 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -33,20 +33,18 @@ internal struct LambdaRuntimeClient {
3333
}
3434

3535
/// Requests work from the Runtime Engine.
36-
func requestWork(logger: Logger) -> EventLoopFuture<(LambdaContext, [UInt8])> {
36+
func requestWork(logger: Logger) -> EventLoopFuture<(Invocation, ByteBuffer)> {
3737
let url = Consts.invocationURLPrefix + Consts.requestWorkURLSuffix
3838
logger.debug("requesting work from lambda runtime engine using \(url)")
3939
return self.httpClient.get(url: url).flatMapThrowing { response in
4040
guard response.status == .ok else {
4141
throw LambdaRuntimeClientError.badStatusCode(response.status)
4242
}
43-
guard let payload = response.readWholeBody() else {
43+
let invocation = try Invocation(headers: response.headers)
44+
guard let payload = response.body else {
4445
throw LambdaRuntimeClientError.noBody
4546
}
46-
guard let context = LambdaContext(logger: logger, response: response) else {
47-
throw LambdaRuntimeClientError.noContext
48-
}
49-
return (context, payload)
47+
return (invocation, payload)
5048
}.flatMapErrorThrowing { error in
5149
switch error {
5250
case HTTPClient.Errors.timeout:
@@ -60,14 +58,13 @@ internal struct LambdaRuntimeClient {
6058
}
6159

6260
/// Reports a result to the Runtime Engine.
63-
func reportResults(logger: Logger, context: LambdaContext, result: LambdaResult) -> EventLoopFuture<Void> {
64-
var url = Consts.invocationURLPrefix + "/" + context.requestId
61+
func reportResults(logger: Logger, invocation: Invocation, result: Result<ByteBuffer, Error>) -> EventLoopFuture<Void> {
62+
var url = Consts.invocationURLPrefix + "/" + invocation.requestId
6563
var body: ByteBuffer
6664
switch result {
67-
case .success(let data):
65+
case .success(let buffer):
6866
url += Consts.postResponseURLSuffix
69-
body = self.allocator.buffer(capacity: data.count)
70-
body.writeBytes(data)
67+
body = buffer
7168
case .failure(let error):
7269
url += Consts.postErrorURLSuffix
7370
// TODO: make FunctionError a const
@@ -132,8 +129,8 @@ internal struct LambdaRuntimeClient {
132129
internal enum LambdaRuntimeClientError: Error, Equatable {
133130
case badStatusCode(HTTPResponseStatus)
134131
case upstreamError(String)
132+
case invocationMissingHeader(String)
135133
case noBody
136-
case noContext
137134
case json(JsonCodecError)
138135
}
139136

@@ -182,25 +179,36 @@ private extension HTTPClient.Response {
182179
}
183180
}
184181

185-
private extension LambdaContext {
186-
init?(logger: Logger, response: HTTPClient.Response) {
187-
guard let requestId = response.headerValue(AmazonHeaders.requestID) else {
188-
return nil
182+
internal struct Invocation {
183+
let requestId: String
184+
let deadlineDate: String
185+
let invokedFunctionArn: String
186+
let traceId: String
187+
let clientContext: String?
188+
let cognitoIdentity: String?
189+
190+
init(headers: HTTPHeaders) throws {
191+
guard let requestId = headers.first(name: AmazonHeaders.requestID), !requestId.isEmpty else {
192+
throw LambdaRuntimeClientError.invocationMissingHeader(AmazonHeaders.requestID)
189193
}
190-
if requestId.isEmpty {
191-
return nil
194+
195+
guard let unixTimeMilliseconds = headers.first(name: AmazonHeaders.deadline) else {
196+
throw LambdaRuntimeClientError.invocationMissingHeader(AmazonHeaders.deadline)
192197
}
193-
let traceId = response.headerValue(AmazonHeaders.traceID)
194-
let invokedFunctionArn = response.headerValue(AmazonHeaders.invokedFunctionARN)
195-
let cognitoIdentity = response.headerValue(AmazonHeaders.cognitoIdentity)
196-
let clientContext = response.headerValue(AmazonHeaders.clientContext)
197-
let deadline = response.headerValue(AmazonHeaders.deadline)
198-
self = LambdaContext(requestId: requestId,
199-
traceId: traceId,
200-
invokedFunctionArn: invokedFunctionArn,
201-
cognitoIdentity: cognitoIdentity,
202-
clientContext: clientContext,
203-
deadline: deadline,
204-
logger: logger)
198+
199+
guard let invokedFunctionArn = headers.first(name: AmazonHeaders.invokedFunctionARN) else {
200+
throw LambdaRuntimeClientError.invocationMissingHeader(AmazonHeaders.invokedFunctionARN)
201+
}
202+
203+
guard let traceId = headers.first(name: AmazonHeaders.traceID) else {
204+
throw LambdaRuntimeClientError.invocationMissingHeader(AmazonHeaders.traceID)
205+
}
206+
207+
self.requestId = requestId
208+
self.deadlineDate = unixTimeMilliseconds
209+
self.invokedFunctionArn = invokedFunctionArn
210+
self.traceId = traceId
211+
self.clientContext = headers["Lambda-Runtime-Client-Context"].first
212+
self.cognitoIdentity = headers["Lambda-Runtime-Cognito-Identity"].first
205213
}
206214
}

Tests/SwiftAwsLambdaTests/Lambda+CodeableTest.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ private struct Response: Codable {
150150
}
151151

152152
private struct CodableEchoHandler: LambdaCodableHandler {
153-
func handle(context: LambdaContext, payload: Request, callback: @escaping LambdaCodableCallback<Response>) {
153+
func handle(context: Lambda.Context, payload: Request, callback: @escaping LambdaCodableCallback<Response>) {
154154
callback(.success(Response(requestId: payload.requestId)))
155155
}
156156
}

Tests/SwiftAwsLambdaTests/Lambda+StringTest.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ private struct BadBehavior: LambdaServerBehavior {
120120
}
121121

122122
private struct StringEchoHandler: LambdaStringHandler {
123-
func handle(context: LambdaContext, payload: String, callback: @escaping LambdaStringCallback) {
123+
func handle(context: Lambda.Context, payload: String, callback: @escaping LambdaStringCallback) {
124124
callback(.success(payload))
125125
}
126126
}

Tests/SwiftAwsLambdaTests/LambdaRuntimeClientTest+XCTest.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ extension LambdaRuntimeClientTest {
2727
return [
2828
("testGetWorkServerInternalError", testGetWorkServerInternalError),
2929
("testGetWorkServerNoBodyError", testGetWorkServerNoBodyError),
30-
("testGetWorkServerNoContextError", testGetWorkServerNoContextError),
30+
("testGetWorkServerMissingHeaderRequestIDError", testGetWorkServerMissingHeaderRequestIDError),
3131
("testProcessResponseInternalServerError", testProcessResponseInternalServerError),
3232
("testProcessErrorInternalServerError", testProcessErrorInternalServerError),
3333
("testProcessInitErrorInternalServerError", testProcessInitErrorInternalServerError),

0 commit comments

Comments
 (0)