Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion Sources/AWSLambdaRuntime/Lambda+LocalServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
4 changes: 2 additions & 2 deletions Sources/AWSLambdaRuntime/Lambda.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
184 changes: 184 additions & 0 deletions Sources/AWSLambdaRuntime/LambdaClock.swift
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 I did think of using c std lib functions, but couldn't be arsed working them out. This is great

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My C is a bit rusty, I got some help from Amazon Q

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)
}
}
21 changes: 9 additions & 12 deletions Sources/AWSLambdaRuntime/LambdaContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
//
//===----------------------------------------------------------------------===//

import Dispatch
import Logging
import NIOCore

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
}

Expand All @@ -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
Expand All @@ -173,30 +172,28 @@ 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 {
"\(Self.self)(requestID: \(self.requestID), traceID: \(self.traceID), invokedFunctionARN: \(self.invokedFunctionARN), cognitoIdentity: \(self.cognitoIdentity ?? "nil"), clientContext: \(String(describing: self.clientContext)), deadline: \(self.deadline))"
}

/// 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
)
}
Expand Down
17 changes: 1 addition & 16 deletions Sources/AWSLambdaRuntime/Utils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
//
//===----------------------------------------------------------------------===//

import Dispatch
import NIOConcurrencyHelpers
import NIOPosix

Expand All @@ -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: "\""))
Expand Down Expand Up @@ -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.
Expand Down
Loading
Loading