mirror of
https://github.com/swift-server/swift-aws-lambda-runtime.git
synced 2026-05-03 07:22:27 +00:00
Fix [#384](https://github.com/swift-server/swift-aws-lambda-runtime/issues/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](https://github.com/swift-server/swift-aws-lambda-runtime/issues/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.
This commit is contained in:
committed by
GitHub
parent
11bea7b2ee
commit
447c1e4db1
@@ -20,9 +20,12 @@ 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(Int64(bitPattern: self.deadline.rawValue)) / -1_000_000_000
|
||||
return Date(timeIntervalSince1970: secondsSinceEpoch)
|
||||
// Date(timeIntervalSince1970:) expects seconds, so we convert milliseconds to seconds.
|
||||
Date(timeIntervalSince1970: Double(self.deadline.millisecondsSinceEpoch()) / 1000)
|
||||
}
|
||||
}
|
||||
#endif // trait: FoundationJSONSupport
|
||||
|
||||
@@ -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, "\(LambdaClock.maxLambdaDeadline)"),
|
||||
])
|
||||
|
||||
return LocalServerResponse(
|
||||
|
||||
@@ -70,8 +70,8 @@ public enum Lambda {
|
||||
requestID: invocation.metadata.requestID,
|
||||
traceID: invocation.metadata.traceID,
|
||||
invokedFunctionARN: invocation.metadata.invokedFunctionARN,
|
||||
deadline: DispatchWallTime(
|
||||
millisSinceEpoch: invocation.metadata.deadlineInMillisSinceEpoch
|
||||
deadline: LambdaClock.Instant(
|
||||
millisecondsSinceEpoch: invocation.metadata.deadlineInMillisSinceEpoch
|
||||
),
|
||||
logger: logger
|
||||
)
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// 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(macOS)
|
||||
import Darwin.C
|
||||
#elseif canImport(Glibc)
|
||||
import Glibc
|
||||
#elseif canImport(Musl)
|
||||
import Musl
|
||||
#elseif os(Windows)
|
||||
import ucrt
|
||||
#else
|
||||
#error("Unsupported platform")
|
||||
#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.
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Dispatch
|
||||
import Logging
|
||||
import NIOCore
|
||||
|
||||
@@ -89,7 +88,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable {
|
||||
let requestID: String
|
||||
let traceID: String
|
||||
let invokedFunctionARN: String
|
||||
let deadline: DispatchWallTime
|
||||
let deadline: LambdaClock.Instant
|
||||
let cognitoIdentity: String?
|
||||
let clientContext: ClientContext?
|
||||
let logger: Logger
|
||||
@@ -98,7 +97,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable {
|
||||
requestID: String,
|
||||
traceID: String,
|
||||
invokedFunctionARN: String,
|
||||
deadline: DispatchWallTime,
|
||||
deadline: LambdaClock.Instant,
|
||||
cognitoIdentity: String?,
|
||||
clientContext: ClientContext?,
|
||||
logger: Logger
|
||||
@@ -131,7 +130,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable {
|
||||
}
|
||||
|
||||
/// The timestamp that the function times out.
|
||||
public var deadline: DispatchWallTime {
|
||||
public var deadline: LambdaClock.Instant {
|
||||
self.storage.deadline
|
||||
}
|
||||
|
||||
@@ -156,7 +155,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable {
|
||||
requestID: String,
|
||||
traceID: String,
|
||||
invokedFunctionARN: String,
|
||||
deadline: DispatchWallTime,
|
||||
deadline: LambdaClock.Instant,
|
||||
cognitoIdentity: String? = nil,
|
||||
clientContext: ClientContext? = nil,
|
||||
logger: Logger
|
||||
@@ -173,11 +172,8 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable {
|
||||
}
|
||||
|
||||
public func getRemainingTime() -> Duration {
|
||||
let deadline = self.deadline.millisSinceEpoch
|
||||
let now = DispatchWallTime.now().millisSinceEpoch
|
||||
|
||||
let remaining = deadline - now
|
||||
return .milliseconds(remaining)
|
||||
let deadline = self.deadline
|
||||
return LambdaClock().now.duration(to: deadline)
|
||||
}
|
||||
|
||||
public var debugDescription: String {
|
||||
@@ -185,18 +181,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: LambdaClock().now.advanced(by: timeout),
|
||||
logger: logger
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Dispatch
|
||||
import NIOConcurrencyHelpers
|
||||
import NIOPosix
|
||||
|
||||
@@ -39,20 +38,6 @@ enum AmazonHeaders {
|
||||
static let invokedFunctionARN = "Lambda-Runtime-Invoked-Function-Arn"
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
func encodeAsJSONString(into bytes: inout [UInt8]) {
|
||||
bytes.append(UInt8(ascii: "\""))
|
||||
@@ -103,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(DispatchWallTime.now().millisSinceEpoch / 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.
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// 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
|
||||
|
||||
#if canImport(FoundationEssentials)
|
||||
import FoundationEssentials
|
||||
#else
|
||||
import Foundation
|
||||
#endif
|
||||
|
||||
@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
|
||||
}
|
||||
|
||||
@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"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user