Skip to content

Commit 447c1e4

Browse files
authored
Remove dependency on DispatchWallTime (fix #384) (#540)
Fix [#384](#384) Note: this PR introduces an API change that will break Lambda functions using `LambdaContext`, we should integrate this change during the beta otherwise it will require a major version bump. ### Motivation: `DispatchWallTime` has no public API to extract the time in milliseconds, making it a dead end. Previous implementation used the internal representation of time inside `DispatchWallTime` to extract the value, creating a risk if its implementation will change in the future. Moreover, the use of `DispatchWallTime` obliges users to import the `Dispatch` library or `Foundation`. Old Code: ``` extension DispatchWallTime { @usableFromInline 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 { Int64(bitPattern: self.rawValue) / -1_000_000 } } ``` Issue [#384](#384) has a long discussion about possible replacements, including creating a brand new `UTCClock`, which I think is an overkill for this project. Instead, I propose this simple implementation, based on two assumptions: - AWS always sends the time in milliseconds since Unix Epoch (1st Jan 1970) ([Lambda Runtime API documentation](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html#runtimes-api-next)) - AWS always uses UTC time (not only for Lambda, this is a general rule for all AWS APIs) ([TZ=UTC on Lambda](https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html)) Therefore, this library just needs to store and make math on milliseconds since epoch, without having to care about timezone. I had two possibilities to implement the storage and the math on milliseconds since Unix Epoch: either I could use an `UInt64` (as does [the Rust implementation](https://github.com/awslabs/aws-lambda-rust-runtime/blob/aff8d883c62997ef2615714dce9f7ddfd557147d/lambda-runtime/src/types.rs#L70)) or I could use a standard Swift type, such as `Duration`. `Duration` is a good candidate for this because 1/ the time we receive from the Lambda Service API is indeed a duration between 1/1/1970 and the execution deadline for the Lambda function, expressed in milliseconds, 2/ it gives a strong type that can be verified by the compiler, and 3/ it is possible to do basic arithmetic operations and compare two values. As an additional benefit, it allows library users to not import `Dispatch` or `Foundation` ### Modifications: I made two changes: 1. I extend the `Duration` type to provide us with simple unix epoch time manipulation functions and values. ```swift extension Duration { /// Returns the time in milliseconds since the Unix epoch. @usableFromInline static var millisSinceEpoch: Duration { var ts = timespec() clock_gettime(CLOCK_REALTIME, &ts) return .milliseconds(Int64(ts.tv_sec) * 1000 + Int64(ts.tv_nsec) / 1_000_000) } /// Returns a Duration between Unix epoch and the distant future @usableFromInline static var distantFuture: Duration { // Use a very large value to represent the distant future millisSinceEpoch + Duration.seconds(.greatestFiniteMagnitude) } /// Returns the Duration in milliseconds @usableFromInline func milliseconds() -> Int64 { Int64(self / .milliseconds(1)) } /// Create a Duration from milliseconds since Unix Epoch @usableFromInline init(millisSinceEpoch: Int64) { self = .milliseconds(millisSinceEpoch) } } ``` 3. I replaced all references to `DispatchWallTime` by `Duration` ### Result: No more `DispatchWallTime` No dependencies on Foundation, as I use `clock_gettime()` to get the epoch from the system clock.
1 parent 11bea7b commit 447c1e4

File tree

8 files changed

+363
-33
lines changed

8 files changed

+363
-33
lines changed

Sources/AWSLambdaRuntime/FoundationSupport/Context+Foundation.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@ import struct Foundation.Date
2020
#endif
2121

2222
extension LambdaContext {
23+
/// Returns the deadline as a Date for the Lambda function execution.
24+
/// I'm not sure how usefull it is to have this as a Date, with only seconds precision,
25+
/// but I leave it here for compatibility with the FoundationJSONSupport trait.
2326
var deadlineDate: Date {
24-
let secondsSinceEpoch = Double(Int64(bitPattern: self.deadline.rawValue)) / -1_000_000_000
25-
return Date(timeIntervalSince1970: secondsSinceEpoch)
27+
// Date(timeIntervalSince1970:) expects seconds, so we convert milliseconds to seconds.
28+
Date(timeIntervalSince1970: Double(self.deadline.millisecondsSinceEpoch()) / 1000)
2629
}
2730
}
2831
#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, "\(DispatchWallTime.distantFuture.millisSinceEpoch)"),
653+
(AmazonHeaders.deadline, "\(LambdaClock.maxLambdaDeadline)"),
654654
])
655655

656656
return LocalServerResponse(

Sources/AWSLambdaRuntime/Lambda.swift

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

Sources/AWSLambdaRuntime/LambdaContext.swift

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

15-
import Dispatch
1615
import Logging
1716
import NIOCore
1817

@@ -89,7 +88,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable {
8988
let requestID: String
9089
let traceID: String
9190
let invokedFunctionARN: String
92-
let deadline: DispatchWallTime
91+
let deadline: LambdaClock.Instant
9392
let cognitoIdentity: String?
9493
let clientContext: ClientContext?
9594
let logger: Logger
@@ -98,7 +97,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable {
9897
requestID: String,
9998
traceID: String,
10099
invokedFunctionARN: String,
101-
deadline: DispatchWallTime,
100+
deadline: LambdaClock.Instant,
102101
cognitoIdentity: String?,
103102
clientContext: ClientContext?,
104103
logger: Logger
@@ -131,7 +130,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable {
131130
}
132131

133132
/// The timestamp that the function times out.
134-
public var deadline: DispatchWallTime {
133+
public var deadline: LambdaClock.Instant {
135134
self.storage.deadline
136135
}
137136

@@ -156,7 +155,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable {
156155
requestID: String,
157156
traceID: String,
158157
invokedFunctionARN: String,
159-
deadline: DispatchWallTime,
158+
deadline: LambdaClock.Instant,
160159
cognitoIdentity: String? = nil,
161160
clientContext: ClientContext? = nil,
162161
logger: Logger
@@ -173,30 +172,28 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable {
173172
}
174173

175174
public func getRemainingTime() -> Duration {
176-
let deadline = self.deadline.millisSinceEpoch
177-
let now = DispatchWallTime.now().millisSinceEpoch
178-
179-
let remaining = deadline - now
180-
return .milliseconds(remaining)
175+
let deadline = self.deadline
176+
return LambdaClock().now.duration(to: deadline)
181177
}
182178

183179
public var debugDescription: String {
184180
"\(Self.self)(requestID: \(self.requestID), traceID: \(self.traceID), invokedFunctionARN: \(self.invokedFunctionARN), cognitoIdentity: \(self.cognitoIdentity ?? "nil"), clientContext: \(String(describing: self.clientContext)), deadline: \(self.deadline))"
185181
}
186182

187183
/// This interface is not part of the public API and must not be used by adopters. This API is not part of semver versioning.
184+
/// The timeout is expressed relative to now
188185
package static func __forTestsOnly(
189186
requestID: String,
190187
traceID: String,
191188
invokedFunctionARN: String,
192-
timeout: DispatchTimeInterval,
189+
timeout: Duration,
193190
logger: Logger
194191
) -> LambdaContext {
195192
LambdaContext(
196193
requestID: requestID,
197194
traceID: traceID,
198195
invokedFunctionARN: invokedFunctionARN,
199-
deadline: .now() + timeout,
196+
deadline: LambdaClock().now.advanced(by: timeout),
200197
logger: logger
201198
)
202199
}

Sources/AWSLambdaRuntime/Utils.swift

Lines changed: 1 addition & 16 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,20 +38,6 @@ enum AmazonHeaders {
3938
static let invokedFunctionARN = "Lambda-Runtime-Invoked-Function-Arn"
4039
}
4140

42-
extension DispatchWallTime {
43-
@usableFromInline
44-
init(millisSinceEpoch: Int64) {
45-
let nanoSinceEpoch = UInt64(millisSinceEpoch) * 1_000_000
46-
let seconds = UInt64(nanoSinceEpoch / 1_000_000_000)
47-
let nanoseconds = nanoSinceEpoch - (seconds * 1_000_000_000)
48-
self.init(timespec: timespec(tv_sec: Int(seconds), tv_nsec: Int(nanoseconds)))
49-
}
50-
51-
var millisSinceEpoch: Int64 {
52-
Int64(bitPattern: self.rawValue) / -1_000_000
53-
}
54-
}
55-
5641
extension String {
5742
func encodeAsJSONString(into bytes: inout [UInt8]) {
5843
bytes.append(UInt8(ascii: "\""))
@@ -103,7 +88,7 @@ extension AmazonHeaders {
10388
// The version number, that is, 1.
10489
let version: UInt = 1
10590
// The time of the original request, in Unix epoch time, in 8 hexadecimal digits.
106-
let now = UInt32(DispatchWallTime.now().millisSinceEpoch / 1000)
91+
let now = UInt32(LambdaClock().now.millisecondsSinceEpoch() / 1000)
10792
let dateValue = String(now, radix: 16, uppercase: false)
10893
let datePadding = String(repeating: "0", count: max(0, 8 - dateValue.count))
10994
// A 96-bit identifier for the trace, globally unique, in 24 hexadecimal digits.

0 commit comments

Comments
 (0)