-
Notifications
You must be signed in to change notification settings - Fork 116
Remove dependency on DispatchWallTime (fix #384) #540
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 10 commits
e7eed85
0903737
bf738fe
70c0af1
730230a
8de2212
bf2f10d
e319ff4
5fb71d8
02a54df
d699f36
62ae211
c98caa3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 { | ||
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) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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,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 deadline.duration(to: LambdaClock().now) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't this be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're correct, otherwise it would return a negative duration. I added a unit test to verify this |
||
} | ||
|
||
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 | ||
) | ||
} | ||
|
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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