From 9f5d86ec0ab3193a32918f55bc48c3890d8b15ab Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Mon, 9 Mar 2020 20:15:11 +0100 Subject: [PATCH 1/4] Add Deadline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Motivation: - As a developer I want to be able to know how much time I have left to execute my lambda, before it times out Changes: - storing the deadline as Int64 on Context and Invocation - added getRemainingTime() to Context which returns a TimeAmount - in the FoundationCompat library we should offer something based on Date - fixed MockLambdaServer to not return “keep-alive” anymore --- Sources/MockServer/main.swift | 3 +- Sources/SwiftAwsLambda/Lambda.swift | 30 ++++++++++++++++--- Sources/SwiftAwsLambda/LambdaRunner.swift | 2 +- .../SwiftAwsLambda/LambdaRuntimeClient.swift | 7 +++-- Tests/SwiftAwsLambdaTests/LambdaTest.swift | 11 +++++++ .../MockLambdaServer.swift | 7 +++-- 6 files changed, 49 insertions(+), 11 deletions(-) diff --git a/Sources/MockServer/main.swift b/Sources/MockServer/main.swift index add024f5..fee6b298 100644 --- a/Sources/MockServer/main.swift +++ b/Sources/MockServer/main.swift @@ -108,11 +108,12 @@ internal final class HTTPHandler: ChannelInboundHandler { case .json: responseBody = "{ \"body\": \"\(requestId)\" }" } + let deadline = Int64(Date(timeIntervalSinceNow: 60).timeIntervalSince1970 * 1000) responseHeaders = [ (AmazonHeaders.requestID, requestId), (AmazonHeaders.invokedFunctionARN, "arn:aws:lambda:us-east-1:123456789012:function:custom-runtime"), (AmazonHeaders.traceID, "Root=1-5bef4de7-ad49b0e87f6ef6c87fc2e700;Parent=9a9197af755a6419;Sampled=1"), - (AmazonHeaders.deadline, String(Date(timeIntervalSinceNow: 60).timeIntervalSince1970 * 1000)), + (AmazonHeaders.deadline, String(deadline)), ] } else if request.head.uri.hasSuffix("/response") { responseStatus = .accepted diff --git a/Sources/SwiftAwsLambda/Lambda.swift b/Sources/SwiftAwsLambda/Lambda.swift index 330a3109..23529f5e 100644 --- a/Sources/SwiftAwsLambda/Lambda.swift +++ b/Sources/SwiftAwsLambda/Lambda.swift @@ -78,20 +78,31 @@ public enum Lambda { } public class Context { - // from aws + /// The request ID, which identifies the request that triggered the function invocation. public let requestId: String + + /// The AWS X-Ray tracing header. public let traceId: String + + /// The ARN of the Lambda function, version, or alias that's specified in the invocation. public let invokedFunctionArn: String - public let deadline: String + + /// The date that the function times out in Unix time milliseconds + public let deadline: Int64 + + /// For invocations from the AWS Mobile SDK, data about the Amazon Cognito identity provider. public let cognitoIdentity: String? + + /// For invocations from the AWS Mobile SDK, data about the client application and device. public let clientContext: String? - // utility + + /// a logger to log public let logger: Logger internal init(requestId: String, traceId: String, invokedFunctionArn: String, - deadline: String, + deadline: Int64, cognitoIdentity: String? = nil, clientContext: String? = nil, logger: Logger) { @@ -107,6 +118,17 @@ public enum Lambda { logger[metadataKey: "awsTraceId"] = .string(traceId) self.logger = logger } + + @available(OSX 10.12, *) + public func getRemainingTime() -> TimeAmount { + func getTimeInMilliSeconds() -> Int64 { + var ts: timespec = timespec(tv_sec: 0, tv_nsec: 0) + clock_gettime(CLOCK_REALTIME, &ts) + return Int64(ts.tv_sec * 1000) + Int64(ts.tv_nsec / 1_000_000) + } + + return .milliseconds(self.deadline - getTimeInMilliSeconds()) + } } private final class Lifecycle { diff --git a/Sources/SwiftAwsLambda/LambdaRunner.swift b/Sources/SwiftAwsLambda/LambdaRunner.swift index 448d81f1..de9c2c3d 100644 --- a/Sources/SwiftAwsLambda/LambdaRunner.swift +++ b/Sources/SwiftAwsLambda/LambdaRunner.swift @@ -130,7 +130,7 @@ private extension Lambda.Context { self.init(requestId: invocation.requestId, traceId: invocation.traceId, invokedFunctionArn: invocation.invokedFunctionArn, - deadline: invocation.deadlineDate, + deadline: invocation.deadline, cognitoIdentity: invocation.cognitoIdentity, clientContext: invocation.clientContext, logger: logger) diff --git a/Sources/SwiftAwsLambda/LambdaRuntimeClient.swift b/Sources/SwiftAwsLambda/LambdaRuntimeClient.swift index abaa40d0..af1fa039 100644 --- a/Sources/SwiftAwsLambda/LambdaRuntimeClient.swift +++ b/Sources/SwiftAwsLambda/LambdaRuntimeClient.swift @@ -181,7 +181,7 @@ private extension HTTPClient.Response { internal struct Invocation { let requestId: String - let deadlineDate: String + let deadline: Int64 let invokedFunctionArn: String let traceId: String let clientContext: String? @@ -192,7 +192,8 @@ internal struct Invocation { throw LambdaRuntimeClientError.invocationMissingHeader(AmazonHeaders.requestID) } - guard let unixTimeMilliseconds = headers.first(name: AmazonHeaders.deadline) else { + guard let deadline = headers.first(name: AmazonHeaders.deadline), + let unixTimeInMilliseconds = Int64(deadline) else { throw LambdaRuntimeClientError.invocationMissingHeader(AmazonHeaders.deadline) } @@ -205,7 +206,7 @@ internal struct Invocation { } self.requestId = requestId - self.deadlineDate = unixTimeMilliseconds + self.deadline = unixTimeInMilliseconds self.invokedFunctionArn = invokedFunctionArn self.traceId = traceId self.clientContext = headers["Lambda-Runtime-Client-Context"].first diff --git a/Tests/SwiftAwsLambdaTests/LambdaTest.swift b/Tests/SwiftAwsLambdaTests/LambdaTest.swift index 48e1380c..c8e52760 100644 --- a/Tests/SwiftAwsLambdaTests/LambdaTest.swift +++ b/Tests/SwiftAwsLambdaTests/LambdaTest.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import Logging import NIO @testable import SwiftAwsLambda import XCTest @@ -212,6 +213,16 @@ class LambdaTest: XCTestCase { let result = Lambda.run(handler: EchoHandler(), configuration: configuration) assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) } + + @available(OSX 10.12, *) + func testGetTimeRemaining() { + let deadline = round(Date(timeIntervalSinceNow: 60).timeIntervalSince1970 * 1000) + let context = Lambda.Context(requestId: "abc", traceId: "abc", invokedFunctionArn: "abc", deadline: Int64(deadline), logger: Logger(label: "")) + + let remaining = context.getRemainingTime().nanoseconds + // maximal allowed time offset 2 milliseconds + XCTAssertLessThanOrEqual(abs(remaining - 60 * 1_000_000_000), 2_000_000) + } } private struct Behavior: LambdaServerBehavior { diff --git a/Tests/SwiftAwsLambdaTests/MockLambdaServer.swift b/Tests/SwiftAwsLambdaTests/MockLambdaServer.swift index a1c721b7..ffbefefd 100644 --- a/Tests/SwiftAwsLambdaTests/MockLambdaServer.swift +++ b/Tests/SwiftAwsLambdaTests/MockLambdaServer.swift @@ -140,11 +140,12 @@ internal final class HTTPHandler: ChannelInboundHandler { } responseStatus = .ok responseBody = result + let deadline = Int64(Date(timeIntervalSinceNow: 60).timeIntervalSince1970 * 1000) responseHeaders = [ (AmazonHeaders.requestID, requestId), (AmazonHeaders.invokedFunctionARN, "arn:aws:lambda:us-east-1:123456789012:function:custom-runtime"), (AmazonHeaders.traceID, "Root=1-5bef4de7-ad49b0e87f6ef6c87fc2e700;Parent=9a9197af755a6419;Sampled=1"), - (AmazonHeaders.deadline, String(Date(timeIntervalSinceNow: 60).timeIntervalSince1970 * 1000)), + (AmazonHeaders.deadline, String(deadline)), ] case .failure(let error): responseStatus = .init(statusCode: error.rawValue) @@ -181,7 +182,9 @@ internal final class HTTPHandler: ChannelInboundHandler { func writeResponse(context: ChannelHandlerContext, status: HTTPResponseStatus, headers: [(String, String)]? = nil, body: String? = nil) { var headers = HTTPHeaders(headers ?? []) headers.add(name: "Content-Length", value: "\(body?.utf8.count ?? 0)") - headers.add(name: "Connection", value: self.keepAlive ? "keep-alive" : "close") + if !self.keepAlive { + headers.add(name: "Connection", value: "close") + } let head = HTTPResponseHead(version: HTTPVersion(major: 1, minor: 1), status: status, headers: headers) context.write(wrapOutboundOut(.head(head))).whenFailure { error in From d12b20ee1d2cbc09220342df9fff96765e9ecec3 Mon Sep 17 00:00:00 2001 From: tom doron Date: Mon, 9 Mar 2020 19:37:46 -0700 Subject: [PATCH 2/4] another approach --- Sources/SwiftAwsLambda/Lambda.swift | 17 ++----- Sources/SwiftAwsLambda/LambdaRunner.swift | 2 +- Sources/SwiftAwsLambda/Utils.swift | 13 ++++++ .../LambdaTest+XCTest.swift | 1 + Tests/SwiftAwsLambdaTests/LambdaTest.swift | 45 +++++++++++++++---- .../MockLambdaServer.swift | 2 +- Tests/SwiftAwsLambdaTests/Utils.swift | 6 +++ 7 files changed, 62 insertions(+), 24 deletions(-) diff --git a/Sources/SwiftAwsLambda/Lambda.swift b/Sources/SwiftAwsLambda/Lambda.swift index 23529f5e..17d5bf92 100644 --- a/Sources/SwiftAwsLambda/Lambda.swift +++ b/Sources/SwiftAwsLambda/Lambda.swift @@ -87,8 +87,8 @@ public enum Lambda { /// The ARN of the Lambda function, version, or alias that's specified in the invocation. public let invokedFunctionArn: String - /// The date that the function times out in Unix time milliseconds - public let deadline: Int64 + /// The timestamp that the function times out + public let deadline: DispatchWallTime /// For invocations from the AWS Mobile SDK, data about the Amazon Cognito identity provider. public let cognitoIdentity: String? @@ -102,7 +102,7 @@ public enum Lambda { internal init(requestId: String, traceId: String, invokedFunctionArn: String, - deadline: Int64, + deadline: DispatchWallTime, cognitoIdentity: String? = nil, clientContext: String? = nil, logger: Logger) { @@ -118,17 +118,6 @@ public enum Lambda { logger[metadataKey: "awsTraceId"] = .string(traceId) self.logger = logger } - - @available(OSX 10.12, *) - public func getRemainingTime() -> TimeAmount { - func getTimeInMilliSeconds() -> Int64 { - var ts: timespec = timespec(tv_sec: 0, tv_nsec: 0) - clock_gettime(CLOCK_REALTIME, &ts) - return Int64(ts.tv_sec * 1000) + Int64(ts.tv_nsec / 1_000_000) - } - - return .milliseconds(self.deadline - getTimeInMilliSeconds()) - } } private final class Lifecycle { diff --git a/Sources/SwiftAwsLambda/LambdaRunner.swift b/Sources/SwiftAwsLambda/LambdaRunner.swift index de9c2c3d..11b2a2d5 100644 --- a/Sources/SwiftAwsLambda/LambdaRunner.swift +++ b/Sources/SwiftAwsLambda/LambdaRunner.swift @@ -130,7 +130,7 @@ private extension Lambda.Context { self.init(requestId: invocation.requestId, traceId: invocation.traceId, invokedFunctionArn: invocation.invokedFunctionArn, - deadline: invocation.deadline, + deadline: DispatchWallTime(millisSinceEpoch: invocation.deadline), cognitoIdentity: invocation.cognitoIdentity, clientContext: invocation.clientContext, logger: logger) diff --git a/Sources/SwiftAwsLambda/Utils.swift b/Sources/SwiftAwsLambda/Utils.swift index 80a62b17..a01e9ceb 100644 --- a/Sources/SwiftAwsLambda/Utils.swift +++ b/Sources/SwiftAwsLambda/Utils.swift @@ -64,3 +64,16 @@ internal enum Signal: Int32 { case ALRM = 14 case TERM = 15 } + +internal extension DispatchWallTime { + init(millisSinceEpoch: Int64) { + let nanoSinceEpoch = UInt64(millisSinceEpoch) * 1_000_000 + let seconds = UInt64(nanoSinceEpoch / 1_000_000_000) + let nanoseconds = nanoSinceEpoch - (seconds * 1_000_000_000) + self.init(timespec: timespec(tv_sec: Int(seconds), tv_nsec: Int(nanoseconds))) + } + + var millisSinceEpoch: Int64 { + return Int64(bitPattern: self.rawValue) / -1_000_000 + } +} diff --git a/Tests/SwiftAwsLambdaTests/LambdaTest+XCTest.swift b/Tests/SwiftAwsLambdaTests/LambdaTest+XCTest.swift index f65ce1fc..2f84da82 100644 --- a/Tests/SwiftAwsLambdaTests/LambdaTest+XCTest.swift +++ b/Tests/SwiftAwsLambdaTests/LambdaTest+XCTest.swift @@ -36,6 +36,7 @@ extension LambdaTest { ("testBigPayload", testBigPayload), ("testKeepAliveServer", testKeepAliveServer), ("testNoKeepAliveServer", testNoKeepAliveServer), + ("testDeadline", testDeadline), ] } } diff --git a/Tests/SwiftAwsLambdaTests/LambdaTest.swift b/Tests/SwiftAwsLambdaTests/LambdaTest.swift index c8e52760..22fb609e 100644 --- a/Tests/SwiftAwsLambdaTests/LambdaTest.swift +++ b/Tests/SwiftAwsLambdaTests/LambdaTest.swift @@ -214,14 +214,43 @@ class LambdaTest: XCTestCase { assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) } - @available(OSX 10.12, *) - func testGetTimeRemaining() { - let deadline = round(Date(timeIntervalSinceNow: 60).timeIntervalSince1970 * 1000) - let context = Lambda.Context(requestId: "abc", traceId: "abc", invokedFunctionArn: "abc", deadline: Int64(deadline), logger: Logger(label: "")) - - let remaining = context.getRemainingTime().nanoseconds - // maximal allowed time offset 2 milliseconds - XCTAssertLessThanOrEqual(abs(remaining - 60 * 1_000_000_000), 2_000_000) + func testDeadline() { + let delta = Int.random(in: 1 ... 600) + + let milli1 = Date(timeIntervalSinceNow: Double(delta)).millisSinceEpoch + let milli2 = (DispatchWallTime.now() + .seconds(delta)).millisSinceEpoch + XCTAssertEqual(Double(milli1), Double(milli2), accuracy: 2.0) + + let now1 = DispatchWallTime.now() + let now2 = DispatchWallTime(millisSinceEpoch: Date().millisSinceEpoch) + XCTAssertEqual(Double(now2.rawValue), Double(now1.rawValue), accuracy: 2_000_000.0) + + let future1 = DispatchWallTime.now() + .seconds(delta) + let future2 = DispatchWallTime(millisSinceEpoch: Date(timeIntervalSinceNow: Double(delta)).millisSinceEpoch) + XCTAssertEqual(Double(future1.rawValue), Double(future2.rawValue), accuracy: 2_000_000.0) + + let past1 = DispatchWallTime.now() - .seconds(delta) + let past2 = DispatchWallTime(millisSinceEpoch: Date(timeIntervalSinceNow: Double(-delta)).millisSinceEpoch) + XCTAssertEqual(Double(past1.rawValue), Double(past2.rawValue), accuracy: 2_000_000.0) + + let logger = Logger(label: "test") + let context = Lambda.Context(requestId: UUID().uuidString, + traceId: UUID().uuidString, + invokedFunctionArn: UUID().uuidString, + deadline: .now() + .seconds(1), + cognitoIdentity: nil, + clientContext: nil, + logger: logger) + XCTAssertGreaterThan(context.deadline, .now()) + + let expiredContext = Lambda.Context(requestId: UUID().uuidString, + traceId: UUID().uuidString, + invokedFunctionArn: UUID().uuidString, + deadline: .now() - .seconds(1), + cognitoIdentity: nil, + clientContext: nil, + logger: logger) + XCTAssertLessThan(expiredContext.deadline, .now()) } } diff --git a/Tests/SwiftAwsLambdaTests/MockLambdaServer.swift b/Tests/SwiftAwsLambdaTests/MockLambdaServer.swift index ffbefefd..1f828f4c 100644 --- a/Tests/SwiftAwsLambdaTests/MockLambdaServer.swift +++ b/Tests/SwiftAwsLambdaTests/MockLambdaServer.swift @@ -140,7 +140,7 @@ internal final class HTTPHandler: ChannelInboundHandler { } responseStatus = .ok responseBody = result - let deadline = Int64(Date(timeIntervalSinceNow: 60).timeIntervalSince1970 * 1000) + let deadline = Date(timeIntervalSinceNow: 60).millisSinceEpoch responseHeaders = [ (AmazonHeaders.requestID, requestId), (AmazonHeaders.invokedFunctionARN, "arn:aws:lambda:us-east-1:123456789012:function:custom-runtime"), diff --git a/Tests/SwiftAwsLambdaTests/Utils.swift b/Tests/SwiftAwsLambdaTests/Utils.swift index 1363625f..ec204a72 100644 --- a/Tests/SwiftAwsLambdaTests/Utils.swift +++ b/Tests/SwiftAwsLambdaTests/Utils.swift @@ -93,3 +93,9 @@ struct TestError: Error, Equatable, CustomStringConvertible { self.description = description } } + +internal extension Date { + var millisSinceEpoch: Int64 { + return Int64(self.timeIntervalSince1970 * 1000) + } +} From 3450478873b1b58439286cb88c29195be8cebc5e Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Tue, 10 Mar 2020 16:08:56 +0100 Subject: [PATCH 3/4] Added `Context.getRemainingTime()` back in. - Renamed `Invocation.deadline` to `Invocation.deadlineInMillisSinceEpoch` to better reflect meaning. - Added a test for `Context.getRemainingTime()` --- Sources/SwiftAwsLambda/Lambda.swift | 8 ++++++++ Sources/SwiftAwsLambda/LambdaRunner.swift | 2 +- Sources/SwiftAwsLambda/LambdaRuntimeClient.swift | 4 ++-- Tests/SwiftAwsLambdaTests/LambdaTest.swift | 13 +++++++++++++ 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/Sources/SwiftAwsLambda/Lambda.swift b/Sources/SwiftAwsLambda/Lambda.swift index 17d5bf92..2375c55e 100644 --- a/Sources/SwiftAwsLambda/Lambda.swift +++ b/Sources/SwiftAwsLambda/Lambda.swift @@ -118,6 +118,14 @@ public enum Lambda { logger[metadataKey: "awsTraceId"] = .string(traceId) self.logger = logger } + + public func getRemainingTime() -> TimeAmount { + let deadline = self.deadline.millisSinceEpoch + let now = DispatchWallTime.now().millisSinceEpoch + + let remaining = deadline - now + return .milliseconds(remaining) + } } private final class Lifecycle { diff --git a/Sources/SwiftAwsLambda/LambdaRunner.swift b/Sources/SwiftAwsLambda/LambdaRunner.swift index 11b2a2d5..20af39bf 100644 --- a/Sources/SwiftAwsLambda/LambdaRunner.swift +++ b/Sources/SwiftAwsLambda/LambdaRunner.swift @@ -130,7 +130,7 @@ private extension Lambda.Context { self.init(requestId: invocation.requestId, traceId: invocation.traceId, invokedFunctionArn: invocation.invokedFunctionArn, - deadline: DispatchWallTime(millisSinceEpoch: invocation.deadline), + deadline: DispatchWallTime(millisSinceEpoch: invocation.deadlineInMillisSinceEpoch), cognitoIdentity: invocation.cognitoIdentity, clientContext: invocation.clientContext, logger: logger) diff --git a/Sources/SwiftAwsLambda/LambdaRuntimeClient.swift b/Sources/SwiftAwsLambda/LambdaRuntimeClient.swift index af1fa039..a0079d7d 100644 --- a/Sources/SwiftAwsLambda/LambdaRuntimeClient.swift +++ b/Sources/SwiftAwsLambda/LambdaRuntimeClient.swift @@ -181,7 +181,7 @@ private extension HTTPClient.Response { internal struct Invocation { let requestId: String - let deadline: Int64 + let deadlineInMillisSinceEpoch: Int64 let invokedFunctionArn: String let traceId: String let clientContext: String? @@ -206,7 +206,7 @@ internal struct Invocation { } self.requestId = requestId - self.deadline = unixTimeInMilliseconds + self.deadlineInMillisSinceEpoch = unixTimeInMilliseconds self.invokedFunctionArn = invokedFunctionArn self.traceId = traceId self.clientContext = headers["Lambda-Runtime-Client-Context"].first diff --git a/Tests/SwiftAwsLambdaTests/LambdaTest.swift b/Tests/SwiftAwsLambdaTests/LambdaTest.swift index 22fb609e..c31792ea 100644 --- a/Tests/SwiftAwsLambdaTests/LambdaTest.swift +++ b/Tests/SwiftAwsLambdaTests/LambdaTest.swift @@ -252,6 +252,19 @@ class LambdaTest: XCTestCase { logger: logger) XCTAssertLessThan(expiredContext.deadline, .now()) } + + func testGetRemainingTime() { + let logger = Logger(label: "test") + let context = Lambda.Context(requestId: UUID().uuidString, + traceId: UUID().uuidString, + invokedFunctionArn: UUID().uuidString, + deadline: .now() + .seconds(1), + cognitoIdentity: nil, + clientContext: nil, + logger: logger) + XCTAssertLessThanOrEqual(context.getRemainingTime(), .seconds(1)) + XCTAssertGreaterThan(context.getRemainingTime(), .milliseconds(800)) + } } private struct Behavior: LambdaServerBehavior { From 9f92ee0a0be29510a89fff728088fe9e92c1773f Mon Sep 17 00:00:00 2001 From: tom doron Date: Tue, 10 Mar 2020 10:27:10 -0700 Subject: [PATCH 4/4] fixup --- Tests/SwiftAwsLambdaTests/LambdaTest+XCTest.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/SwiftAwsLambdaTests/LambdaTest+XCTest.swift b/Tests/SwiftAwsLambdaTests/LambdaTest+XCTest.swift index 2f84da82..b10732d8 100644 --- a/Tests/SwiftAwsLambdaTests/LambdaTest+XCTest.swift +++ b/Tests/SwiftAwsLambdaTests/LambdaTest+XCTest.swift @@ -37,6 +37,7 @@ extension LambdaTest { ("testKeepAliveServer", testKeepAliveServer), ("testNoKeepAliveServer", testNoKeepAliveServer), ("testDeadline", testDeadline), + ("testGetRemainingTime", testGetRemainingTime), ] } }