Skip to content

Commit d046617

Browse files
fabianfetttomerd
andauthored
Add Deadline (#27)
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 on Context and Invocation - added getRemainingTime() to Context which returns a TimeAmount - Renamed `Invocation.deadline` to `Invocation.deadlineInMillisSinceEpoch` to better reflect - fixed MockLambdaServer to not return “keep-alive” anymore Co-authored-by: tom doron <tomer@apple.com>
1 parent eb2f31c commit d046617

File tree

9 files changed

+109
-11
lines changed

9 files changed

+109
-11
lines changed

Sources/MockServer/main.swift

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

Sources/SwiftAwsLambda/Lambda.swift

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,20 +78,31 @@ public enum Lambda {
7878
}
7979

8080
public class Context {
81-
// from aws
81+
/// The request ID, which identifies the request that triggered the function invocation.
8282
public let requestId: String
83+
84+
/// The AWS X-Ray tracing header.
8385
public let traceId: String
86+
87+
/// The ARN of the Lambda function, version, or alias that's specified in the invocation.
8488
public let invokedFunctionArn: String
85-
public let deadline: String
89+
90+
/// The timestamp that the function times out
91+
public let deadline: DispatchWallTime
92+
93+
/// For invocations from the AWS Mobile SDK, data about the Amazon Cognito identity provider.
8694
public let cognitoIdentity: String?
95+
96+
/// For invocations from the AWS Mobile SDK, data about the client application and device.
8797
public let clientContext: String?
88-
// utility
98+
99+
/// a logger to log
89100
public let logger: Logger
90101

91102
internal init(requestId: String,
92103
traceId: String,
93104
invokedFunctionArn: String,
94-
deadline: String,
105+
deadline: DispatchWallTime,
95106
cognitoIdentity: String? = nil,
96107
clientContext: String? = nil,
97108
logger: Logger) {
@@ -107,6 +118,14 @@ public enum Lambda {
107118
logger[metadataKey: "awsTraceId"] = .string(traceId)
108119
self.logger = logger
109120
}
121+
122+
public func getRemainingTime() -> TimeAmount {
123+
let deadline = self.deadline.millisSinceEpoch
124+
let now = DispatchWallTime.now().millisSinceEpoch
125+
126+
let remaining = deadline - now
127+
return .milliseconds(remaining)
128+
}
110129
}
111130

112131
private final class Lifecycle {

Sources/SwiftAwsLambda/LambdaRunner.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ private extension Lambda.Context {
130130
self.init(requestId: invocation.requestId,
131131
traceId: invocation.traceId,
132132
invokedFunctionArn: invocation.invokedFunctionArn,
133-
deadline: invocation.deadlineDate,
133+
deadline: DispatchWallTime(millisSinceEpoch: invocation.deadlineInMillisSinceEpoch),
134134
cognitoIdentity: invocation.cognitoIdentity,
135135
clientContext: invocation.clientContext,
136136
logger: logger)

Sources/SwiftAwsLambda/LambdaRuntimeClient.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ private extension HTTPClient.Response {
181181

182182
internal struct Invocation {
183183
let requestId: String
184-
let deadlineDate: String
184+
let deadlineInMillisSinceEpoch: Int64
185185
let invokedFunctionArn: String
186186
let traceId: String
187187
let clientContext: String?
@@ -192,7 +192,8 @@ internal struct Invocation {
192192
throw LambdaRuntimeClientError.invocationMissingHeader(AmazonHeaders.requestID)
193193
}
194194

195-
guard let unixTimeMilliseconds = headers.first(name: AmazonHeaders.deadline) else {
195+
guard let deadline = headers.first(name: AmazonHeaders.deadline),
196+
let unixTimeInMilliseconds = Int64(deadline) else {
196197
throw LambdaRuntimeClientError.invocationMissingHeader(AmazonHeaders.deadline)
197198
}
198199

@@ -205,7 +206,7 @@ internal struct Invocation {
205206
}
206207

207208
self.requestId = requestId
208-
self.deadlineDate = unixTimeMilliseconds
209+
self.deadlineInMillisSinceEpoch = unixTimeInMilliseconds
209210
self.invokedFunctionArn = invokedFunctionArn
210211
self.traceId = traceId
211212
self.clientContext = headers["Lambda-Runtime-Client-Context"].first

Sources/SwiftAwsLambda/Utils.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,16 @@ internal enum Signal: Int32 {
6464
case ALRM = 14
6565
case TERM = 15
6666
}
67+
68+
internal extension DispatchWallTime {
69+
init(millisSinceEpoch: Int64) {
70+
let nanoSinceEpoch = UInt64(millisSinceEpoch) * 1_000_000
71+
let seconds = UInt64(nanoSinceEpoch / 1_000_000_000)
72+
let nanoseconds = nanoSinceEpoch - (seconds * 1_000_000_000)
73+
self.init(timespec: timespec(tv_sec: Int(seconds), tv_nsec: Int(nanoseconds)))
74+
}
75+
76+
var millisSinceEpoch: Int64 {
77+
return Int64(bitPattern: self.rawValue) / -1_000_000
78+
}
79+
}

Tests/SwiftAwsLambdaTests/LambdaTest+XCTest.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ extension LambdaTest {
3636
("testBigPayload", testBigPayload),
3737
("testKeepAliveServer", testKeepAliveServer),
3838
("testNoKeepAliveServer", testNoKeepAliveServer),
39+
("testDeadline", testDeadline),
40+
("testGetRemainingTime", testGetRemainingTime),
3941
]
4042
}
4143
}

Tests/SwiftAwsLambdaTests/LambdaTest.swift

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15+
import Logging
1516
import NIO
1617
@testable import SwiftAwsLambda
1718
import XCTest
@@ -212,6 +213,58 @@ class LambdaTest: XCTestCase {
212213
let result = Lambda.run(handler: EchoHandler(), configuration: configuration)
213214
assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes)
214215
}
216+
217+
func testDeadline() {
218+
let delta = Int.random(in: 1 ... 600)
219+
220+
let milli1 = Date(timeIntervalSinceNow: Double(delta)).millisSinceEpoch
221+
let milli2 = (DispatchWallTime.now() + .seconds(delta)).millisSinceEpoch
222+
XCTAssertEqual(Double(milli1), Double(milli2), accuracy: 2.0)
223+
224+
let now1 = DispatchWallTime.now()
225+
let now2 = DispatchWallTime(millisSinceEpoch: Date().millisSinceEpoch)
226+
XCTAssertEqual(Double(now2.rawValue), Double(now1.rawValue), accuracy: 2_000_000.0)
227+
228+
let future1 = DispatchWallTime.now() + .seconds(delta)
229+
let future2 = DispatchWallTime(millisSinceEpoch: Date(timeIntervalSinceNow: Double(delta)).millisSinceEpoch)
230+
XCTAssertEqual(Double(future1.rawValue), Double(future2.rawValue), accuracy: 2_000_000.0)
231+
232+
let past1 = DispatchWallTime.now() - .seconds(delta)
233+
let past2 = DispatchWallTime(millisSinceEpoch: Date(timeIntervalSinceNow: Double(-delta)).millisSinceEpoch)
234+
XCTAssertEqual(Double(past1.rawValue), Double(past2.rawValue), accuracy: 2_000_000.0)
235+
236+
let logger = Logger(label: "test")
237+
let context = Lambda.Context(requestId: UUID().uuidString,
238+
traceId: UUID().uuidString,
239+
invokedFunctionArn: UUID().uuidString,
240+
deadline: .now() + .seconds(1),
241+
cognitoIdentity: nil,
242+
clientContext: nil,
243+
logger: logger)
244+
XCTAssertGreaterThan(context.deadline, .now())
245+
246+
let expiredContext = Lambda.Context(requestId: UUID().uuidString,
247+
traceId: UUID().uuidString,
248+
invokedFunctionArn: UUID().uuidString,
249+
deadline: .now() - .seconds(1),
250+
cognitoIdentity: nil,
251+
clientContext: nil,
252+
logger: logger)
253+
XCTAssertLessThan(expiredContext.deadline, .now())
254+
}
255+
256+
func testGetRemainingTime() {
257+
let logger = Logger(label: "test")
258+
let context = Lambda.Context(requestId: UUID().uuidString,
259+
traceId: UUID().uuidString,
260+
invokedFunctionArn: UUID().uuidString,
261+
deadline: .now() + .seconds(1),
262+
cognitoIdentity: nil,
263+
clientContext: nil,
264+
logger: logger)
265+
XCTAssertLessThanOrEqual(context.getRemainingTime(), .seconds(1))
266+
XCTAssertGreaterThan(context.getRemainingTime(), .milliseconds(800))
267+
}
215268
}
216269

217270
private struct Behavior: LambdaServerBehavior {

Tests/SwiftAwsLambdaTests/MockLambdaServer.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,12 @@ internal final class HTTPHandler: ChannelInboundHandler {
140140
}
141141
responseStatus = .ok
142142
responseBody = result
143+
let deadline = Date(timeIntervalSinceNow: 60).millisSinceEpoch
143144
responseHeaders = [
144145
(AmazonHeaders.requestID, requestId),
145146
(AmazonHeaders.invokedFunctionARN, "arn:aws:lambda:us-east-1:123456789012:function:custom-runtime"),
146147
(AmazonHeaders.traceID, "Root=1-5bef4de7-ad49b0e87f6ef6c87fc2e700;Parent=9a9197af755a6419;Sampled=1"),
147-
(AmazonHeaders.deadline, String(Date(timeIntervalSinceNow: 60).timeIntervalSince1970 * 1000)),
148+
(AmazonHeaders.deadline, String(deadline)),
148149
]
149150
case .failure(let error):
150151
responseStatus = .init(statusCode: error.rawValue)
@@ -181,7 +182,9 @@ internal final class HTTPHandler: ChannelInboundHandler {
181182
func writeResponse(context: ChannelHandlerContext, status: HTTPResponseStatus, headers: [(String, String)]? = nil, body: String? = nil) {
182183
var headers = HTTPHeaders(headers ?? [])
183184
headers.add(name: "Content-Length", value: "\(body?.utf8.count ?? 0)")
184-
headers.add(name: "Connection", value: self.keepAlive ? "keep-alive" : "close")
185+
if !self.keepAlive {
186+
headers.add(name: "Connection", value: "close")
187+
}
185188
let head = HTTPResponseHead(version: HTTPVersion(major: 1, minor: 1), status: status, headers: headers)
186189

187190
context.write(wrapOutboundOut(.head(head))).whenFailure { error in

Tests/SwiftAwsLambdaTests/Utils.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,9 @@ struct TestError: Error, Equatable, CustomStringConvertible {
9393
self.description = description
9494
}
9595
}
96+
97+
internal extension Date {
98+
var millisSinceEpoch: Int64 {
99+
return Int64(self.timeIntervalSince1970 * 1000)
100+
}
101+
}

0 commit comments

Comments
 (0)