Skip to content

Commit 730230a

Browse files
committed
use a lambda clock based on unix epoch time
1 parent 70c0af1 commit 730230a

File tree

7 files changed

+303
-54
lines changed

7 files changed

+303
-54
lines changed

Sources/AWSLambdaRuntime/FoundationSupport/Context+Foundation.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ extension LambdaContext {
2424
/// I'm not sure how usefull it is to have this as a Date, with only seconds precision,
2525
/// but I leave it here for compatibility with the FoundationJSONSupport trait.
2626
var deadlineDate: Date {
27-
Date(timeIntervalSince1970: Double(self.deadline.milliseconds()) / 1000)
27+
// Date(timeIntervalSince1970:) expects seconds, so we convert milliseconds to seconds.
28+
Date(timeIntervalSince1970: Double(self.deadline.millisecondsSinceEpoch()) / 1000)
2829
}
2930
}
3031
#endif // trait: FoundationJSONSupport

Sources/AWSLambdaRuntime/Lambda+LocalServer.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -650,7 +650,7 @@ internal struct LambdaHTTPServer {
650650
"arn:aws:lambda:us-east-1:\(Int16.random(in: Int16.min ... Int16.max)):function:custom-runtime"
651651
),
652652
(AmazonHeaders.traceID, "Root=\(AmazonHeaders.generateXRayTraceID());Sampled=1"),
653-
(AmazonHeaders.deadline, "\(Duration.maxLambdaExecutionTime.milliseconds())"),
653+
(AmazonHeaders.deadline, "\(LambdaClock.maxLambdaDeadline)"),
654654
])
655655

656656
return LocalServerResponse(

Sources/AWSLambdaRuntime/Lambda.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ public enum Lambda {
7070
requestID: invocation.metadata.requestID,
7171
traceID: invocation.metadata.traceID,
7272
invokedFunctionARN: invocation.metadata.invokedFunctionARN,
73-
deadline: Duration(millisSinceEpoch: invocation.metadata.deadlineInMillisSinceEpoch),
73+
deadline: LambdaClock.Instant(
74+
millisecondsSinceEpoch: invocation.metadata.deadlineInMillisSinceEpoch
75+
),
7476
logger: logger
7577
)
7678
)
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftAWSLambdaRuntime open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the SwiftAWSLambdaRuntime project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
#if os(Linux)
16+
import Glibc
17+
#else
18+
import Darwin
19+
#endif
20+
21+
/// A clock implementation based on Unix epoch time for AWS Lambda runtime operations.
22+
///
23+
/// `LambdaClock` provides millisecond-precision timing based on the Unix epoch
24+
/// (January 1, 1970, 00:00:00 UTC). This clock is designed for Lambda runtime
25+
/// operations where precise wall-clock time is required.
26+
///
27+
/// ## Usage
28+
///
29+
/// ```swift
30+
/// let clock = LambdaClock()
31+
/// let now = clock.now
32+
/// let deadline = now.advanced(by: .seconds(30))
33+
///
34+
/// // Sleep until deadline
35+
/// try await clock.sleep(until: deadline)
36+
/// ```
37+
///
38+
/// ## Performance
39+
///
40+
/// This clock uses `clock_gettime(CLOCK_REALTIME)` on Unix systems for
41+
/// high-precision wall-clock time measurement with millisecond resolution.
42+
///
43+
/// ## TimeZone Handling
44+
///
45+
/// The Lambda execution environment uses UTC as a timezone,
46+
/// `LambdaClock` operates in UTC and does not account for time zones.
47+
/// see: TZ in https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html
48+
public struct LambdaClock: Clock {
49+
public typealias Duration = Swift.Duration
50+
51+
/// A moment in time represented as milliseconds since the Unix epoch.
52+
///
53+
/// `Instant` represents a specific point in time as the number of milliseconds
54+
/// that have elapsed since January 1, 1970, 00:00:00 UTC (Unix epoch).
55+
///
56+
/// ## Thread Safety
57+
///
58+
/// `Instant` is a value type and is inherently thread-safe.
59+
public struct Instant: InstantProtocol {
60+
/// The number of milliseconds since the Unix epoch.
61+
let instant: Int64
62+
63+
public typealias Duration = Swift.Duration
64+
65+
/// Creates a new instant by adding a duration to this instant.
66+
///
67+
/// - Parameter duration: The duration to add to this instant.
68+
/// - Returns: A new instant advanced by the specified duration.
69+
///
70+
/// ## Example
71+
///
72+
/// ```swift
73+
/// let now = LambdaClock().now
74+
/// let future = now.advanced(by: .seconds(30))
75+
/// ```
76+
public func advanced(by duration: Duration) -> Instant {
77+
.init(millisecondsSinceEpoch: Int64(instant + Int64(duration / .milliseconds(1))))
78+
}
79+
80+
/// Calculates the duration between this instant and another instant.
81+
///
82+
/// - Parameter other: The target instant to calculate duration to.
83+
/// - Returns: The duration from this instant to the other instant.
84+
/// Positive if `other` is in the future, negative if in the past.
85+
///
86+
/// ## Example
87+
///
88+
/// ```swift
89+
/// let start = LambdaClock().now
90+
/// // ... some work ...
91+
/// let end = LambdaClock().now
92+
/// let elapsed = start.duration(to: end)
93+
/// ```
94+
public func duration(to other: Instant) -> Duration {
95+
.milliseconds(other.instant - self.instant)
96+
}
97+
98+
/// Compares two instants for ordering.
99+
///
100+
/// - Parameters:
101+
/// - lhs: The left-hand side instant.
102+
/// - rhs: The right-hand side instant.
103+
/// - Returns: `true` if `lhs` represents an earlier time than `rhs`.
104+
public static func < (lhs: Instant, rhs: Instant) -> Bool {
105+
lhs.instant < rhs.instant
106+
}
107+
108+
/// Returns this instant as the number of milliseconds since the Unix epoch.
109+
/// - Returns: The number of milliseconds since the Unix epoch.
110+
public func millisecondsSinceEpoch() -> Int64 {
111+
self.instant
112+
}
113+
114+
/// Creates an instant from milliseconds since the Unix epoch.
115+
/// - Parameter milliseconds: The number of milliseconds since the Unix epoch.
116+
/// - Returns: A new `Instant` representing the specified time.
117+
public init(millisecondsSinceEpoch milliseconds: Int64) {
118+
self.instant = milliseconds
119+
}
120+
}
121+
122+
/// The current instant according to this clock.
123+
///
124+
/// This property returns the current wall-clock time as milliseconds
125+
/// since the Unix epoch.
126+
/// This method uses `clock_gettime(CLOCK_REALTIME)` to obtain high-precision
127+
/// wall-clock time.
128+
///
129+
/// - Returns: An `Instant` representing the current time.
130+
public var now: Instant {
131+
var ts = timespec()
132+
clock_gettime(CLOCK_REALTIME, &ts)
133+
return .init(millisecondsSinceEpoch: Int64(ts.tv_sec) * 1000 + Int64(ts.tv_nsec) / 1_000_000)
134+
}
135+
136+
/// The minimum resolution of this clock.
137+
///
138+
/// `LambdaClock` provides millisecond resolution.
139+
public var minimumResolution: Duration {
140+
.milliseconds(1)
141+
}
142+
143+
/// Suspends the current task until the specified deadline.
144+
///
145+
/// - Parameters:
146+
/// - deadline: The instant until which to sleep.
147+
/// - tolerance: The allowed tolerance for the sleep duration. Currently unused.
148+
///
149+
/// - Throws: `CancellationError` if the task is cancelled during sleep.
150+
///
151+
/// ## Example
152+
///
153+
/// ```swift
154+
/// let clock = LambdaClock()
155+
/// let deadline = clock.now.advanced(by: .seconds(5))
156+
/// try await clock.sleep(until: deadline)
157+
/// ```
158+
public func sleep(until deadline: Instant, tolerance: Instant.Duration?) async throws {
159+
let now = self.now
160+
let sleepDuration = now.duration(to: deadline)
161+
if sleepDuration > .zero {
162+
try await ContinuousClock().sleep(for: sleepDuration)
163+
}
164+
}
165+
166+
/// Hardcoded maximum execution time for a Lambda function.
167+
public static var maxLambdaExecutionTime: Duration {
168+
// 15 minutes in milliseconds
169+
// see https://docs.aws.amazon.com/lambda/latest/dg/configuration-timeout.html
170+
.milliseconds(15 * 60 * 1000)
171+
}
172+
173+
/// Returns the maximum deadline for a Lambda function execution.
174+
/// This is the current time plus the maximum execution time.
175+
/// This function is only used by the local server for testing purposes.
176+
public static var maxLambdaDeadline: Instant {
177+
LambdaClock().now.advanced(by: maxLambdaExecutionTime)
178+
}
179+
}

Sources/AWSLambdaRuntime/LambdaContext.swift

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable {
2525
let requestID: String
2626
let traceID: String
2727
let invokedFunctionARN: String
28-
let deadline: Duration
28+
let deadline: LambdaClock.Instant
2929
let cognitoIdentity: String?
3030
let clientContext: String?
3131
let logger: Logger
@@ -34,7 +34,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable {
3434
requestID: String,
3535
traceID: String,
3636
invokedFunctionARN: String,
37-
deadline: Duration,
37+
deadline: LambdaClock.Instant,
3838
cognitoIdentity: String?,
3939
clientContext: String?,
4040
logger: Logger
@@ -67,7 +67,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable {
6767
}
6868

6969
/// The timestamp that the function times out.
70-
public var deadline: Duration {
70+
public var deadline: LambdaClock.Instant {
7171
self.storage.deadline
7272
}
7373

@@ -92,7 +92,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable {
9292
requestID: String,
9393
traceID: String,
9494
invokedFunctionARN: String,
95-
deadline: Duration,
95+
deadline: LambdaClock.Instant,
9696
cognitoIdentity: String? = nil,
9797
clientContext: String? = nil,
9898
logger: Logger
@@ -110,9 +110,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable {
110110

111111
public func getRemainingTime() -> Duration {
112112
let deadline = self.deadline
113-
let now = Duration.millisSinceEpoch
114-
115-
return deadline - now
113+
return deadline.duration(to: LambdaClock().now)
116114
}
117115

118116
public var debugDescription: String {
@@ -132,7 +130,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable {
132130
requestID: requestID,
133131
traceID: traceID,
134132
invokedFunctionARN: invokedFunctionARN,
135-
deadline: Duration.millisSinceEpoch + timeout,
133+
deadline: LambdaClock().now.advanced(by: timeout),
136134
logger: logger
137135
)
138136
}

Sources/AWSLambdaRuntime/Utils.swift

Lines changed: 1 addition & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15-
import Dispatch
1615
import NIOConcurrencyHelpers
1716
import NIOPosix
1817

@@ -39,47 +38,6 @@ enum AmazonHeaders {
3938
static let invokedFunctionARN = "Lambda-Runtime-Invoked-Function-Arn"
4039
}
4140

42-
/// A simple set of additions to Duration helping to work with Unix epoch that does not require Foundation.
43-
/// The Lambda execution environment uses UTC as a timezone, this struct must not manage timezones.
44-
/// see: TZ in https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html
45-
extension Duration {
46-
/// Returns the time in milliseconds since the Unix epoch.
47-
@usableFromInline
48-
static var millisSinceEpoch: Duration {
49-
var ts = timespec()
50-
clock_gettime(CLOCK_REALTIME, &ts)
51-
return .milliseconds(Int64(ts.tv_sec) * 1000 + Int64(ts.tv_nsec) / 1_000_000)
52-
}
53-
54-
/// Hardcoded maximum execution time for a Lambda function.
55-
@usableFromInline
56-
static var maxLambdaExecutionTime: Duration {
57-
// 15 minutes in milliseconds
58-
// see https://docs.aws.amazon.com/lambda/latest/dg/configuration-timeout.html
59-
.milliseconds(15 * 60 * 1000)
60-
}
61-
62-
/// Returns the maximum deadline for a Lambda function execution.
63-
/// This is the current time plus the maximum execution time.
64-
/// This function is onwly used by the local server for testing purposes.
65-
@usableFromInline
66-
static var maxLambdaDeadline: Duration {
67-
millisSinceEpoch + maxLambdaExecutionTime
68-
}
69-
70-
/// Returns the Duration in milliseconds
71-
@usableFromInline
72-
func milliseconds() -> Int64 {
73-
Int64(self / .milliseconds(1))
74-
}
75-
76-
/// Create a Duration from milliseconds since Unix Epoch
77-
@usableFromInline
78-
init(millisSinceEpoch: Int64) {
79-
self = .milliseconds(millisSinceEpoch)
80-
}
81-
}
82-
8341
extension String {
8442
func encodeAsJSONString(into bytes: inout [UInt8]) {
8543
bytes.append(UInt8(ascii: "\""))
@@ -130,7 +88,7 @@ extension AmazonHeaders {
13088
// The version number, that is, 1.
13189
let version: UInt = 1
13290
// The time of the original request, in Unix epoch time, in 8 hexadecimal digits.
133-
let now = UInt32(Duration.millisSinceEpoch.milliseconds() / 1000)
91+
let now = UInt32(LambdaClock().now.millisecondsSinceEpoch() / 1000)
13492
let dateValue = String(now, radix: 16, uppercase: false)
13593
let datePadding = String(repeating: "0", count: max(0, 8 - dateValue.count))
13694
// A 96-bit identifier for the trace, globally unique, in 24 hexadecimal digits.

0 commit comments

Comments
 (0)