From e7eed85caa0069a2e17e66f18a0733789c4ad210 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Thu, 31 Jul 2025 20:26:55 +0400 Subject: [PATCH 01/10] remove dependency on DispatchWallTime (fix #384) --- .../Context+Foundation.swift | 2 +- .../AWSLambdaRuntime/Lambda+LocalServer.swift | 2 +- Sources/AWSLambdaRuntime/Lambda.swift | 4 +- Sources/AWSLambdaRuntime/LambdaContext.swift | 20 +++++----- Sources/AWSLambdaRuntime/Utils.swift | 37 ++++++++++++++----- 5 files changed, 41 insertions(+), 24 deletions(-) diff --git a/Sources/AWSLambdaRuntime/FoundationSupport/Context+Foundation.swift b/Sources/AWSLambdaRuntime/FoundationSupport/Context+Foundation.swift index 72b7a65d..8890e673 100644 --- a/Sources/AWSLambdaRuntime/FoundationSupport/Context+Foundation.swift +++ b/Sources/AWSLambdaRuntime/FoundationSupport/Context+Foundation.swift @@ -21,7 +21,7 @@ import struct Foundation.Date extension LambdaContext { var deadlineDate: Date { - let secondsSinceEpoch = Double(Int64(bitPattern: self.deadline.rawValue)) / -1_000_000_000 + let secondsSinceEpoch = Double(self.deadline.milliseconds()) / -1_000_000_000 return Date(timeIntervalSince1970: secondsSinceEpoch) } } diff --git a/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift b/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift index f536e3f4..7f5b0357 100644 --- a/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift +++ b/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift @@ -650,7 +650,7 @@ internal struct LambdaHTTPServer { "arn:aws:lambda:us-east-1:\(Int16.random(in: Int16.min ... Int16.max)):function:custom-runtime" ), (AmazonHeaders.traceID, "Root=\(AmazonHeaders.generateXRayTraceID());Sampled=1"), - (AmazonHeaders.deadline, "\(DispatchWallTime.distantFuture.millisSinceEpoch)"), + (AmazonHeaders.deadline, "\(Duration.distantFuture.milliseconds())"), ]) return LocalServerResponse( diff --git a/Sources/AWSLambdaRuntime/Lambda.swift b/Sources/AWSLambdaRuntime/Lambda.swift index 5412c139..7c691eae 100644 --- a/Sources/AWSLambdaRuntime/Lambda.swift +++ b/Sources/AWSLambdaRuntime/Lambda.swift @@ -70,9 +70,7 @@ public enum Lambda { requestID: invocation.metadata.requestID, traceID: invocation.metadata.traceID, invokedFunctionARN: invocation.metadata.invokedFunctionARN, - deadline: DispatchWallTime( - millisSinceEpoch: invocation.metadata.deadlineInMillisSinceEpoch - ), + deadline: Duration(millisSinceEpoch: invocation.metadata.deadlineInMillisSinceEpoch), logger: logger ) ) diff --git a/Sources/AWSLambdaRuntime/LambdaContext.swift b/Sources/AWSLambdaRuntime/LambdaContext.swift index fbf84158..3c69de7f 100644 --- a/Sources/AWSLambdaRuntime/LambdaContext.swift +++ b/Sources/AWSLambdaRuntime/LambdaContext.swift @@ -25,7 +25,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { let requestID: String let traceID: String let invokedFunctionARN: String - let deadline: DispatchWallTime + let deadline: Duration let cognitoIdentity: String? let clientContext: String? let logger: Logger @@ -34,7 +34,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { requestID: String, traceID: String, invokedFunctionARN: String, - deadline: DispatchWallTime, + deadline: Duration, cognitoIdentity: String?, clientContext: String?, logger: Logger @@ -67,7 +67,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { } /// The timestamp that the function times out. - public var deadline: DispatchWallTime { + public var deadline: Duration { self.storage.deadline } @@ -92,7 +92,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { requestID: String, traceID: String, invokedFunctionARN: String, - deadline: DispatchWallTime, + deadline: Duration, cognitoIdentity: String? = nil, clientContext: String? = nil, logger: Logger @@ -109,11 +109,10 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { } public func getRemainingTime() -> Duration { - let deadline = self.deadline.millisSinceEpoch - let now = DispatchWallTime.now().millisSinceEpoch + let deadline = self.deadline + let now = Duration.millisSinceEpoch - let remaining = deadline - now - return .milliseconds(remaining) + return deadline - now } public var debugDescription: String { @@ -121,18 +120,19 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { } /// This interface is not part of the public API and must not be used by adopters. This API is not part of semver versioning. + /// The timeout is expressed relative to now package static func __forTestsOnly( requestID: String, traceID: String, invokedFunctionARN: String, - timeout: DispatchTimeInterval, + timeout: Duration, logger: Logger ) -> LambdaContext { LambdaContext( requestID: requestID, traceID: traceID, invokedFunctionARN: invokedFunctionARN, - deadline: .now() + timeout, + deadline: Duration.millisSinceEpoch + timeout, logger: logger ) } diff --git a/Sources/AWSLambdaRuntime/Utils.swift b/Sources/AWSLambdaRuntime/Utils.swift index aa36fd76..a883cb6f 100644 --- a/Sources/AWSLambdaRuntime/Utils.swift +++ b/Sources/AWSLambdaRuntime/Utils.swift @@ -39,17 +39,36 @@ enum AmazonHeaders { static let invokedFunctionARN = "Lambda-Runtime-Invoked-Function-Arn" } -extension DispatchWallTime { +/// A simple set of additions to Duration helping to work with Unix epoch that does not require Foundation. +/// It provides a distant future value and a way to get the current time in milliseconds since the epoch. +/// The Lambda execution environment uses UTC as a timezone, this struct must not manage timezones. +/// see: TZ in https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html +extension Duration { + /// Returns the time in milliseconds since the Unix epoch. @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))) + 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) } - var millisSinceEpoch: Int64 { - Int64(bitPattern: self.rawValue) / -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) } } @@ -103,7 +122,7 @@ extension AmazonHeaders { // The version number, that is, 1. let version: UInt = 1 // The time of the original request, in Unix epoch time, in 8 hexadecimal digits. - let now = UInt32(DispatchWallTime.now().millisSinceEpoch / 1000) + let now = UInt32(Duration.millisSinceEpoch.milliseconds() / 1000) let dateValue = String(now, radix: 16, uppercase: false) let datePadding = String(repeating: "0", count: max(0, 8 - dateValue.count)) // A 96-bit identifier for the trace, globally unique, in 24 hexadecimal digits. From 0903737de32cf65e967221841478f85557f7bccf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Fri, 1 Aug 2025 07:22:37 +0400 Subject: [PATCH 02/10] Remove distant future and use hardcoded max lambda execution time in HTTP local server --- .../AWSLambdaRuntime/Lambda+LocalServer.swift | 2 +- Sources/AWSLambdaRuntime/Utils.swift | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift b/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift index 7f5b0357..2d21e82e 100644 --- a/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift +++ b/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift @@ -650,7 +650,7 @@ internal struct LambdaHTTPServer { "arn:aws:lambda:us-east-1:\(Int16.random(in: Int16.min ... Int16.max)):function:custom-runtime" ), (AmazonHeaders.traceID, "Root=\(AmazonHeaders.generateXRayTraceID());Sampled=1"), - (AmazonHeaders.deadline, "\(Duration.distantFuture.milliseconds())"), + (AmazonHeaders.deadline, "\(Duration.maxLambdaExecutionTime.milliseconds())"), ]) return LocalServerResponse( diff --git a/Sources/AWSLambdaRuntime/Utils.swift b/Sources/AWSLambdaRuntime/Utils.swift index a883cb6f..2d999bd6 100644 --- a/Sources/AWSLambdaRuntime/Utils.swift +++ b/Sources/AWSLambdaRuntime/Utils.swift @@ -52,11 +52,20 @@ extension Duration { return .milliseconds(Int64(ts.tv_sec) * 1000 + Int64(ts.tv_nsec) / 1_000_000) } - /// Returns a Duration between Unix epoch and the distant future + /// Hardcoded maximum execution time for a Lambda function. @usableFromInline - static var distantFuture: Duration { - // Use a very large value to represent the distant future - millisSinceEpoch + Duration.seconds(.greatestFiniteMagnitude) + static var maxLambdaExecutionTime: Duration { + // 15 minutes in milliseconds + // see https://docs.aws.amazon.com/lambda/latest/dg/configuration-timeout.html + .milliseconds(15 * 60 * 1000) + } + + /// Returns the maximum deadline for a Lambda function execution. + /// This is the current time plus the maximum execution time. + /// This function is onwly used by the local server for testing purposes. + @usableFromInline + static var maxLambdaDeadline: Duration { + millisSinceEpoch + maxLambdaExecutionTime } /// Returns the Duration in milliseconds From bf738fe375d6ea1f63ef372c737d93ae59efdefd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Fri, 1 Aug 2025 07:24:55 +0400 Subject: [PATCH 03/10] change comment --- Sources/AWSLambdaRuntime/Utils.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/AWSLambdaRuntime/Utils.swift b/Sources/AWSLambdaRuntime/Utils.swift index 2d999bd6..13be5b70 100644 --- a/Sources/AWSLambdaRuntime/Utils.swift +++ b/Sources/AWSLambdaRuntime/Utils.swift @@ -40,7 +40,6 @@ enum AmazonHeaders { } /// A simple set of additions to Duration helping to work with Unix epoch that does not require Foundation. -/// It provides a distant future value and a way to get the current time in milliseconds since the epoch. /// The Lambda execution environment uses UTC as a timezone, this struct must not manage timezones. /// see: TZ in https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html extension Duration { From 70c0af163c04d52a636a512aa85880118570937f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Fri, 1 Aug 2025 07:31:07 +0400 Subject: [PATCH 04/10] Simplify deadlineDate --- .../FoundationSupport/Context+Foundation.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/AWSLambdaRuntime/FoundationSupport/Context+Foundation.swift b/Sources/AWSLambdaRuntime/FoundationSupport/Context+Foundation.swift index 8890e673..14fbb4b4 100644 --- a/Sources/AWSLambdaRuntime/FoundationSupport/Context+Foundation.swift +++ b/Sources/AWSLambdaRuntime/FoundationSupport/Context+Foundation.swift @@ -20,9 +20,11 @@ import struct Foundation.Date #endif extension LambdaContext { + /// Returns the deadline as a Date for the Lambda function execution. + /// I'm not sure how usefull it is to have this as a Date, with only seconds precision, + /// but I leave it here for compatibility with the FoundationJSONSupport trait. var deadlineDate: Date { - let secondsSinceEpoch = Double(self.deadline.milliseconds()) / -1_000_000_000 - return Date(timeIntervalSince1970: secondsSinceEpoch) + Date(timeIntervalSince1970: Double(self.deadline.milliseconds()) / 1000) } } #endif // trait: FoundationJSONSupport From 730230acdd58657847df05e5dfa073a18cab1100 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Sat, 2 Aug 2025 19:37:46 +0200 Subject: [PATCH 05/10] use a lambda clock based on unix epoch time --- .../Context+Foundation.swift | 3 +- .../AWSLambdaRuntime/Lambda+LocalServer.swift | 2 +- Sources/AWSLambdaRuntime/Lambda.swift | 4 +- Sources/AWSLambdaRuntime/LambdaClock.swift | 179 ++++++++++++++++++ Sources/AWSLambdaRuntime/LambdaContext.swift | 14 +- Sources/AWSLambdaRuntime/Utils.swift | 44 +---- .../LambdaClockTests.swift | 111 +++++++++++ 7 files changed, 303 insertions(+), 54 deletions(-) create mode 100644 Sources/AWSLambdaRuntime/LambdaClock.swift create mode 100644 Tests/AWSLambdaRuntimeTests/LambdaClockTests.swift diff --git a/Sources/AWSLambdaRuntime/FoundationSupport/Context+Foundation.swift b/Sources/AWSLambdaRuntime/FoundationSupport/Context+Foundation.swift index 14fbb4b4..fad7dcec 100644 --- a/Sources/AWSLambdaRuntime/FoundationSupport/Context+Foundation.swift +++ b/Sources/AWSLambdaRuntime/FoundationSupport/Context+Foundation.swift @@ -24,7 +24,8 @@ extension LambdaContext { /// I'm not sure how usefull it is to have this as a Date, with only seconds precision, /// but I leave it here for compatibility with the FoundationJSONSupport trait. var deadlineDate: Date { - Date(timeIntervalSince1970: Double(self.deadline.milliseconds()) / 1000) + // Date(timeIntervalSince1970:) expects seconds, so we convert milliseconds to seconds. + Date(timeIntervalSince1970: Double(self.deadline.millisecondsSinceEpoch()) / 1000) } } #endif // trait: FoundationJSONSupport diff --git a/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift b/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift index 2d21e82e..20163633 100644 --- a/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift +++ b/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift @@ -650,7 +650,7 @@ internal struct LambdaHTTPServer { "arn:aws:lambda:us-east-1:\(Int16.random(in: Int16.min ... Int16.max)):function:custom-runtime" ), (AmazonHeaders.traceID, "Root=\(AmazonHeaders.generateXRayTraceID());Sampled=1"), - (AmazonHeaders.deadline, "\(Duration.maxLambdaExecutionTime.milliseconds())"), + (AmazonHeaders.deadline, "\(LambdaClock.maxLambdaDeadline)"), ]) return LocalServerResponse( diff --git a/Sources/AWSLambdaRuntime/Lambda.swift b/Sources/AWSLambdaRuntime/Lambda.swift index 7c691eae..706fe567 100644 --- a/Sources/AWSLambdaRuntime/Lambda.swift +++ b/Sources/AWSLambdaRuntime/Lambda.swift @@ -70,7 +70,9 @@ public enum Lambda { requestID: invocation.metadata.requestID, traceID: invocation.metadata.traceID, invokedFunctionARN: invocation.metadata.invokedFunctionARN, - deadline: Duration(millisSinceEpoch: invocation.metadata.deadlineInMillisSinceEpoch), + deadline: LambdaClock.Instant( + millisecondsSinceEpoch: invocation.metadata.deadlineInMillisSinceEpoch + ), logger: logger ) ) diff --git a/Sources/AWSLambdaRuntime/LambdaClock.swift b/Sources/AWSLambdaRuntime/LambdaClock.swift new file mode 100644 index 00000000..0d7b6c6a --- /dev/null +++ b/Sources/AWSLambdaRuntime/LambdaClock.swift @@ -0,0 +1,179 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2025 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if os(Linux) +import Glibc +#else +import Darwin +#endif + +/// A clock implementation based on Unix epoch time for AWS Lambda runtime operations. +/// +/// `LambdaClock` provides millisecond-precision timing based on the Unix epoch +/// (January 1, 1970, 00:00:00 UTC). This clock is designed for Lambda runtime +/// operations where precise wall-clock time is required. +/// +/// ## Usage +/// +/// ```swift +/// let clock = LambdaClock() +/// let now = clock.now +/// let deadline = now.advanced(by: .seconds(30)) +/// +/// // Sleep until deadline +/// try await clock.sleep(until: deadline) +/// ``` +/// +/// ## Performance +/// +/// This clock uses `clock_gettime(CLOCK_REALTIME)` on Unix systems for +/// high-precision wall-clock time measurement with millisecond resolution. +/// +/// ## TimeZone Handling +/// +/// The Lambda execution environment uses UTC as a timezone, +/// `LambdaClock` operates in UTC and does not account for time zones. +/// see: TZ in https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html +public struct LambdaClock: Clock { + public typealias Duration = Swift.Duration + + /// A moment in time represented as milliseconds since the Unix epoch. + /// + /// `Instant` represents a specific point in time as the number of milliseconds + /// that have elapsed since January 1, 1970, 00:00:00 UTC (Unix epoch). + /// + /// ## Thread Safety + /// + /// `Instant` is a value type and is inherently thread-safe. + public struct Instant: InstantProtocol { + /// The number of milliseconds since the Unix epoch. + let instant: Int64 + + public typealias Duration = Swift.Duration + + /// Creates a new instant by adding a duration to this instant. + /// + /// - Parameter duration: The duration to add to this instant. + /// - Returns: A new instant advanced by the specified duration. + /// + /// ## Example + /// + /// ```swift + /// let now = LambdaClock().now + /// let future = now.advanced(by: .seconds(30)) + /// ``` + public func advanced(by duration: Duration) -> Instant { + .init(millisecondsSinceEpoch: Int64(instant + Int64(duration / .milliseconds(1)))) + } + + /// Calculates the duration between this instant and another instant. + /// + /// - Parameter other: The target instant to calculate duration to. + /// - Returns: The duration from this instant to the other instant. + /// Positive if `other` is in the future, negative if in the past. + /// + /// ## Example + /// + /// ```swift + /// let start = LambdaClock().now + /// // ... some work ... + /// let end = LambdaClock().now + /// let elapsed = start.duration(to: end) + /// ``` + public func duration(to other: Instant) -> Duration { + .milliseconds(other.instant - self.instant) + } + + /// Compares two instants for ordering. + /// + /// - Parameters: + /// - lhs: The left-hand side instant. + /// - rhs: The right-hand side instant. + /// - Returns: `true` if `lhs` represents an earlier time than `rhs`. + public static func < (lhs: Instant, rhs: Instant) -> Bool { + lhs.instant < rhs.instant + } + + /// Returns this instant as the number of milliseconds since the Unix epoch. + /// - Returns: The number of milliseconds since the Unix epoch. + public func millisecondsSinceEpoch() -> Int64 { + self.instant + } + + /// Creates an instant from milliseconds since the Unix epoch. + /// - Parameter milliseconds: The number of milliseconds since the Unix epoch. + /// - Returns: A new `Instant` representing the specified time. + public init(millisecondsSinceEpoch milliseconds: Int64) { + self.instant = milliseconds + } + } + + /// The current instant according to this clock. + /// + /// This property returns the current wall-clock time as milliseconds + /// since the Unix epoch. + /// This method uses `clock_gettime(CLOCK_REALTIME)` to obtain high-precision + /// wall-clock time. + /// + /// - Returns: An `Instant` representing the current time. + public var now: Instant { + var ts = timespec() + clock_gettime(CLOCK_REALTIME, &ts) + return .init(millisecondsSinceEpoch: Int64(ts.tv_sec) * 1000 + Int64(ts.tv_nsec) / 1_000_000) + } + + /// The minimum resolution of this clock. + /// + /// `LambdaClock` provides millisecond resolution. + public var minimumResolution: Duration { + .milliseconds(1) + } + + /// Suspends the current task until the specified deadline. + /// + /// - Parameters: + /// - deadline: The instant until which to sleep. + /// - tolerance: The allowed tolerance for the sleep duration. Currently unused. + /// + /// - Throws: `CancellationError` if the task is cancelled during sleep. + /// + /// ## Example + /// + /// ```swift + /// let clock = LambdaClock() + /// let deadline = clock.now.advanced(by: .seconds(5)) + /// try await clock.sleep(until: deadline) + /// ``` + public func sleep(until deadline: Instant, tolerance: Instant.Duration?) async throws { + let now = self.now + let sleepDuration = now.duration(to: deadline) + if sleepDuration > .zero { + try await ContinuousClock().sleep(for: sleepDuration) + } + } + + /// Hardcoded maximum execution time for a Lambda function. + public static var maxLambdaExecutionTime: Duration { + // 15 minutes in milliseconds + // see https://docs.aws.amazon.com/lambda/latest/dg/configuration-timeout.html + .milliseconds(15 * 60 * 1000) + } + + /// Returns the maximum deadline for a Lambda function execution. + /// This is the current time plus the maximum execution time. + /// This function is only used by the local server for testing purposes. + public static var maxLambdaDeadline: Instant { + LambdaClock().now.advanced(by: maxLambdaExecutionTime) + } +} diff --git a/Sources/AWSLambdaRuntime/LambdaContext.swift b/Sources/AWSLambdaRuntime/LambdaContext.swift index 3c69de7f..639a0ddb 100644 --- a/Sources/AWSLambdaRuntime/LambdaContext.swift +++ b/Sources/AWSLambdaRuntime/LambdaContext.swift @@ -25,7 +25,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { let requestID: String let traceID: String let invokedFunctionARN: String - let deadline: Duration + let deadline: LambdaClock.Instant let cognitoIdentity: String? let clientContext: String? let logger: Logger @@ -34,7 +34,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { requestID: String, traceID: String, invokedFunctionARN: String, - deadline: Duration, + deadline: LambdaClock.Instant, cognitoIdentity: String?, clientContext: String?, logger: Logger @@ -67,7 +67,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { } /// The timestamp that the function times out. - public var deadline: Duration { + public var deadline: LambdaClock.Instant { self.storage.deadline } @@ -92,7 +92,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { requestID: String, traceID: String, invokedFunctionARN: String, - deadline: Duration, + deadline: LambdaClock.Instant, cognitoIdentity: String? = nil, clientContext: String? = nil, logger: Logger @@ -110,9 +110,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { public func getRemainingTime() -> Duration { let deadline = self.deadline - let now = Duration.millisSinceEpoch - - return deadline - now + return deadline.duration(to: LambdaClock().now) } public var debugDescription: String { @@ -132,7 +130,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { requestID: requestID, traceID: traceID, invokedFunctionARN: invokedFunctionARN, - deadline: Duration.millisSinceEpoch + timeout, + deadline: LambdaClock().now.advanced(by: timeout), logger: logger ) } diff --git a/Sources/AWSLambdaRuntime/Utils.swift b/Sources/AWSLambdaRuntime/Utils.swift index 13be5b70..8ab544c8 100644 --- a/Sources/AWSLambdaRuntime/Utils.swift +++ b/Sources/AWSLambdaRuntime/Utils.swift @@ -12,7 +12,6 @@ // //===----------------------------------------------------------------------===// -import Dispatch import NIOConcurrencyHelpers import NIOPosix @@ -39,47 +38,6 @@ enum AmazonHeaders { static let invokedFunctionARN = "Lambda-Runtime-Invoked-Function-Arn" } -/// A simple set of additions to Duration helping to work with Unix epoch that does not require Foundation. -/// The Lambda execution environment uses UTC as a timezone, this struct must not manage timezones. -/// see: TZ in https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html -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) - } - - /// Hardcoded maximum execution time for a Lambda function. - @usableFromInline - static var maxLambdaExecutionTime: Duration { - // 15 minutes in milliseconds - // see https://docs.aws.amazon.com/lambda/latest/dg/configuration-timeout.html - .milliseconds(15 * 60 * 1000) - } - - /// Returns the maximum deadline for a Lambda function execution. - /// This is the current time plus the maximum execution time. - /// This function is onwly used by the local server for testing purposes. - @usableFromInline - static var maxLambdaDeadline: Duration { - millisSinceEpoch + maxLambdaExecutionTime - } - - /// 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) - } -} - extension String { func encodeAsJSONString(into bytes: inout [UInt8]) { bytes.append(UInt8(ascii: "\"")) @@ -130,7 +88,7 @@ extension AmazonHeaders { // The version number, that is, 1. let version: UInt = 1 // The time of the original request, in Unix epoch time, in 8 hexadecimal digits. - let now = UInt32(Duration.millisSinceEpoch.milliseconds() / 1000) + let now = UInt32(LambdaClock().now.millisecondsSinceEpoch() / 1000) let dateValue = String(now, radix: 16, uppercase: false) let datePadding = String(repeating: "0", count: max(0, 8 - dateValue.count)) // A 96-bit identifier for the trace, globally unique, in 24 hexadecimal digits. diff --git a/Tests/AWSLambdaRuntimeTests/LambdaClockTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaClockTests.swift new file mode 100644 index 00000000..6745fe6b --- /dev/null +++ b/Tests/AWSLambdaRuntimeTests/LambdaClockTests.swift @@ -0,0 +1,111 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2025 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Testing + +@testable import AWSLambdaRuntime + +@Suite("LambdaClock Tests") +struct LambdaClockTests { + + @Test("Clock provides current time") + func clockProvidesCurrentTime() { + let clock = LambdaClock() + let now = clock.now + + // Verify we get a reasonable timestamp (after today) + let dateOfWritingThisTestInMillis: Int64 = 1_754_130_134_000 + #expect(now.instant > dateOfWritingThisTestInMillis) + } + + @Test("Instant can be advanced by duration") + func instantCanBeAdvancedByDuration() { + let clock = LambdaClock() + let start = clock.now + let advanced = start.advanced(by: .seconds(30)) + + #expect(advanced.instant == start.instant + 30_000) + } + + @Test("Duration calculation between instants") + func durationCalculationBetweenInstants() { + let clock = LambdaClock() + let start = clock.now + let end = start.advanced(by: .seconds(5)) + + let duration = start.duration(to: end) + #expect(duration == .seconds(5)) + } + + @Test("Instant comparison works correctly") + func instantComparisonWorksCorrectly() { + let clock = LambdaClock() + let earlier = clock.now + let later = earlier.advanced(by: .milliseconds(1)) + + #expect(earlier < later) + #expect(!(later < earlier)) + } + + @Test("Clock minimum resolution is milliseconds") + func clockMinimumResolutionIsMilliseconds() { + let clock = LambdaClock() + #expect(clock.minimumResolution == .milliseconds(1)) + } + + @Test("Sleep until deadline works") + func sleepUntilDeadlineWorks() async throws { + let clock = LambdaClock() + let start = clock.now + let deadline = start.advanced(by: .milliseconds(50)) + + try await clock.sleep(until: deadline, tolerance: nil) + + let end = clock.now + let elapsed = start.duration(to: end) + + // Allow some tolerance for timing precision + #expect(elapsed >= .milliseconds(40)) + #expect(elapsed <= .milliseconds(100)) + } + + @Test("Sleep with past deadline returns immediately") + func sleepWithPastDeadlineReturnsImmediately() async throws { + let clock = LambdaClock() + let now = clock.now + let pastDeadline = now.advanced(by: .milliseconds(-100)) + + let start = clock.now + try await clock.sleep(until: pastDeadline, tolerance: nil) + let end = clock.now + + let elapsed = start.duration(to: end) + // Should return almost immediately + #expect(elapsed < .milliseconds(10)) + } + + @Test("Duration to future instant returns negative duration") + func durationToFutureInstantReturnsNegativeDuration() { + let clock = LambdaClock() + let futureDeadline = clock.now.advanced(by: .seconds(30)) + let currentTime = clock.now + + // This simulates getRemainingTime() where deadline is in future + let remainingTime = futureDeadline.duration(to: currentTime) + + // Should be negative since we're going from future to present + #expect(remainingTime < .zero) + #expect(remainingTime <= .seconds(-29)) // Allow some timing tolerance + } +} From 8de2212661c1a2a7eaf32f7a8018bed701f52b10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Sat, 2 Aug 2025 19:38:31 +0200 Subject: [PATCH 06/10] remove import dispatch --- Sources/AWSLambdaRuntime/LambdaContext.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/AWSLambdaRuntime/LambdaContext.swift b/Sources/AWSLambdaRuntime/LambdaContext.swift index 639a0ddb..314170ea 100644 --- a/Sources/AWSLambdaRuntime/LambdaContext.swift +++ b/Sources/AWSLambdaRuntime/LambdaContext.swift @@ -12,7 +12,6 @@ // //===----------------------------------------------------------------------===// -import Dispatch import Logging import NIOCore From bf2f10d16717f59b9bebc867b7ef447c58d4a906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Sat, 2 Aug 2025 19:42:42 +0200 Subject: [PATCH 07/10] fix docc --- Sources/AWSLambdaRuntime/LambdaClock.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/AWSLambdaRuntime/LambdaClock.swift b/Sources/AWSLambdaRuntime/LambdaClock.swift index 0d7b6c6a..5ae8402b 100644 --- a/Sources/AWSLambdaRuntime/LambdaClock.swift +++ b/Sources/AWSLambdaRuntime/LambdaClock.swift @@ -113,7 +113,6 @@ public struct LambdaClock: Clock { /// Creates an instant from milliseconds since the Unix epoch. /// - Parameter milliseconds: The number of milliseconds since the Unix epoch. - /// - Returns: A new `Instant` representing the specified time. public init(millisecondsSinceEpoch milliseconds: Int64) { self.instant = milliseconds } From e319ff4447ff1b8ce75c205001eb3f9b70feb420 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Sat, 2 Aug 2025 19:43:53 +0200 Subject: [PATCH 08/10] import correct lib based on platform --- Sources/AWSLambdaRuntime/LambdaClock.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Sources/AWSLambdaRuntime/LambdaClock.swift b/Sources/AWSLambdaRuntime/LambdaClock.swift index 5ae8402b..6f3df393 100644 --- a/Sources/AWSLambdaRuntime/LambdaClock.swift +++ b/Sources/AWSLambdaRuntime/LambdaClock.swift @@ -12,10 +12,16 @@ // //===----------------------------------------------------------------------===// -#if os(Linux) +#if os(macOS) +import Darwin.C +#elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +import Musl +#elseif os(Windows) +import ucrt #else -import Darwin +#error("Unsupported platform") #endif /// A clock implementation based on Unix epoch time for AWS Lambda runtime operations. From d699f36583fd09592080b468d82339b01ccd5cab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Mon, 4 Aug 2025 15:53:19 +0200 Subject: [PATCH 09/10] fix remianing time --- Sources/AWSLambdaRuntime/LambdaContext.swift | 2 +- .../LambdaContextTests.swift | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/Sources/AWSLambdaRuntime/LambdaContext.swift b/Sources/AWSLambdaRuntime/LambdaContext.swift index 0c9ef13a..0407f9e6 100644 --- a/Sources/AWSLambdaRuntime/LambdaContext.swift +++ b/Sources/AWSLambdaRuntime/LambdaContext.swift @@ -173,7 +173,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { public func getRemainingTime() -> Duration { let deadline = self.deadline - return deadline.duration(to: LambdaClock().now) + return LambdaClock().now.duration(to: deadline) } public var debugDescription: String { diff --git a/Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift index c1108b68..827105da 100644 --- a/Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift +++ b/Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import Foundation +import Logging import Testing @testable import AWSLambdaRuntime @@ -111,4 +112,25 @@ struct LambdaContextTests { #expect(environment["platform"] == "Android") #expect(environment["platform_version"] == "10") } + + @Test("getRemainingTime returns positive duration for future deadline") + func getRemainingTimeReturnsPositiveDurationForFutureDeadline() { + + // Create context with deadline 30 seconds in the future + let context = LambdaContext.__forTestsOnly( + requestID: "test-request", + traceID: "test-trace", + invokedFunctionARN: "test-arn", + timeout: .seconds(30), + logger: Logger(label: "test") + ) + + // Get remaining time - should be positive since deadline is in future + let remainingTime = context.getRemainingTime() + + // Verify Duration can be negative (not absolute value) + #expect(remainingTime > .zero, "getRemainingTime() should return positive duration when deadline is in future") + #expect(remainingTime <= Duration.seconds(31), "Remaining time should be approximately 30 seconds") + #expect(remainingTime >= Duration.seconds(-29), "Remaining time should be approximately -30 seconds") + } } From 62ae2115c0c1cf1c40f501a06536640ac8abab11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Mon, 4 Aug 2025 15:59:44 +0200 Subject: [PATCH 10/10] add unit test for LambdaClock.now --- .../LambdaClockTests.swift | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/Tests/AWSLambdaRuntimeTests/LambdaClockTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaClockTests.swift index 6745fe6b..413a3aa2 100644 --- a/Tests/AWSLambdaRuntimeTests/LambdaClockTests.swift +++ b/Tests/AWSLambdaRuntimeTests/LambdaClockTests.swift @@ -16,6 +16,12 @@ import Testing @testable import AWSLambdaRuntime +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + @Suite("LambdaClock Tests") struct LambdaClockTests { @@ -108,4 +114,26 @@ struct LambdaClockTests { #expect(remainingTime < .zero) #expect(remainingTime <= .seconds(-29)) // Allow some timing tolerance } + + @Test("LambdaClock now matches Foundation Date within tolerance") + func lambdaClockNowMatchesFoundationDate() { + + let clock = LambdaClock() + + // Get timestamps as close together as possible + let lambdaClockNow = clock.now + let foundationDate = Date() + + // Convert Foundation Date to milliseconds since epoch + let foundationMillis = Int64(foundationDate.timeIntervalSince1970 * 1000) + let lambdaClockMillis = lambdaClockNow.millisecondsSinceEpoch() + + // Allow small tolerance for timing differences between calls + let difference = abs(foundationMillis - lambdaClockMillis) + + #expect( + difference <= 10, + "LambdaClock and Foundation Date should be within 10ms of each other, difference was \(difference)ms" + ) + } }