-
Notifications
You must be signed in to change notification settings - Fork 113
[RFC] Adopts *Writable* BaggageContext (Better) #167
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 3 commits
05753aa
9ae0cb7
9ecf61a
68b230a
381ddd1
e4bfc22
1456211
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 |
---|---|---|
|
@@ -12,6 +12,7 @@ | |
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
import BaggageContext | ||
import Dispatch | ||
import Logging | ||
import NIO | ||
|
@@ -49,40 +50,118 @@ extension Lambda { | |
extension Lambda { | ||
/// Lambda runtime context. | ||
/// The Lambda runtime generates and passes the `Context` to the Lambda handler as an argument. | ||
public final class Context: CustomDebugStringConvertible { | ||
public struct Context: BaggageContext.Context, CustomDebugStringConvertible { | ||
private var storage: _Storage | ||
|
||
final class _Storage { | ||
var baggage: Baggage | ||
|
||
let invokedFunctionARN: String | ||
let deadline: DispatchWallTime | ||
let cognitoIdentity: String? | ||
let clientContext: String? | ||
ktoso marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
var _logger: Logger | ||
ktoso marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
let eventLoop: EventLoop | ||
let allocator: ByteBufferAllocator | ||
|
||
init( | ||
baggage: Baggage, | ||
invokedFunctionARN: String, | ||
deadline: DispatchWallTime, | ||
cognitoIdentity: String?, | ||
clientContext: String?, | ||
_logger: Logger, | ||
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. remove underscore from here |
||
eventLoop: EventLoop, | ||
allocator: ByteBufferAllocator | ||
) { | ||
self.baggage = baggage | ||
self.invokedFunctionARN = invokedFunctionARN | ||
self.deadline = deadline | ||
self.cognitoIdentity = cognitoIdentity | ||
self.clientContext = clientContext | ||
self._logger = _logger | ||
self.eventLoop = eventLoop | ||
self.allocator = allocator | ||
} | ||
} | ||
|
||
/// Contains contextual metadata such as request and trace identifiers, along with other information which may | ||
/// be carried throughout asynchronous and cross-node boundaries (e.g. through HTTPClient calls). | ||
public var baggage: Baggage { | ||
get { | ||
self.storage.baggage | ||
} | ||
set { | ||
if isKnownUniquelyReferenced(&self.storage) { | ||
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. please correct me if i am wrong, as far as I understand it:
see discussion here https://gist.github.com/drewmccormack/b1c4487935cf3c3e0a5feaf488a95ebd#gistcomment-2826755 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. Yeah I'm aware of that rule, it could be that it's misapplied here after all... I'll dig into it again.
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. @pokryfka, so no. This is implementation correct. (You had me worried there for a second! 😉) This is as thread safe as an
I disagree about the "often" quantification used here. It is by far not as often as a raw struct would be copying -- each passing around and reassigning to a new variable even would technically then be copying. We're getting the best of both worlds; and simply following the known CoW semantics that are frequent in Swift.
The operations in this piece of code are safe. If they weren't then Array and all of Swift's collections are screwed as well 😉 We are providing the same semantics as those types. 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.
it used to be a "const reference" and so only the reference (pointer) was copied. I dont know how expensive is copying of a small struct, and how much more expensive is allocating the space for it on heap rather than on stack; perhaps its not a real issue at all. if the goal was to avoid copying of the lambda context, perhaps
Thank you for rechecking it. if I understand that it will be safe because struct Context {
func notSafeUpdateBaggage(_ baggage: Baggage) {
// update storage
}
// need to make a copy which will increase the storage reference counter
mutating func updateBaggage(_ baggage: Baggage) {
// update storage
}
} 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.
Yeah if it becomes a problem I think we could do that 🤔 👍
Maybe this example can illustrate: Because this is a struct, in order to change it the function must be mutating (or Then if anyone has:
they can't mutate it unless a second reference is created:
which ensures that we'll have the +1 count there and will copy on mutation. It still is possible to make bugs like:
that remains illegal as usual, but not because of the CoW or Context type being wrong but how the user uses it being unsafe. The same example is true for e.g. |
||
self.storage.baggage = newValue | ||
} else { | ||
self.storage = _Storage( | ||
baggage: newValue, | ||
invokedFunctionARN: self.storage.invokedFunctionARN, | ||
deadline: self.storage.deadline, | ||
cognitoIdentity: self.storage.cognitoIdentity, | ||
clientContext: self.storage.clientContext, | ||
_logger: self.storage._logger, | ||
eventLoop: self.storage.eventLoop, | ||
allocator: self.storage.allocator | ||
) | ||
} | ||
} | ||
} | ||
|
||
/// The request ID, which identifies the request that triggered the function invocation. | ||
public let requestID: String | ||
public var requestID: String { | ||
self.storage.baggage.lambdaRequestID | ||
} | ||
|
||
/// The AWS X-Ray tracing header. | ||
public let traceID: String | ||
public var traceID: String { | ||
self.storage.baggage.lambdaTraceID | ||
} | ||
|
||
/// The ARN of the Lambda function, version, or alias that's specified in the invocation. | ||
public let invokedFunctionARN: String | ||
public var invokedFunctionARN: String { | ||
self.storage.invokedFunctionARN | ||
} | ||
|
||
/// The timestamp that the function times out | ||
public let deadline: DispatchWallTime | ||
public var deadline: DispatchWallTime { | ||
self.storage.deadline | ||
} | ||
|
||
/// For invocations from the AWS Mobile SDK, data about the Amazon Cognito identity provider. | ||
public let cognitoIdentity: String? | ||
public var cognitoIdentity: String? { | ||
self.storage.cognitoIdentity | ||
} | ||
|
||
/// For invocations from the AWS Mobile SDK, data about the client application and device. | ||
public let clientContext: String? | ||
public var clientContext: String? { | ||
self.storage.clientContext | ||
} | ||
|
||
/// `Logger` to log with | ||
/// `Logger` to log with, it is automatically populated with `baggage` information (such as `traceID` and `requestID`). | ||
/// | ||
/// - note: The `LogLevel` can be configured using the `LOG_LEVEL` environment variable. | ||
public let logger: Logger | ||
public var logger: Logger { | ||
self.storage._logger.with(self.baggage) | ||
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. I can't remember what with() does... does it mean every logger access is a new instance? 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. Yes, every This has to be like that because we need the logger to point to the "latest" baggage values. As everything here has value semantics, we can't just pass a reference to the context or baggage into the handler, and have to pass it when we're about to use it. The Alternatively, this could be done on a 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. hmm I think this could actually add overhead for cases that log allot. I think keeping state on when baggage changed otherwise returning the existing logger could be a good optimization. maybe even coded into BaggageContext so users dont have to reinvent 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. I revisited this and will try to implement via an "materialize once" into logging metadata. You're right that this is painful if there's a lot of logging, and we should not make that painful :\ It won't be doable as a log handler I guess, so that goes out the window; really I guess what would solve the needless copying is to make Logging depend on Baggage, and then we can remove its storage for metadata and replace it with baggage, avoiding the copying alltogether. Not sure when to pitch this, it'd be a logging 2.0 I guess? 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. Addressed this -- metadata is now set eagerly and we won't get into crazy copying anymore. Thanks for reminding me about this, it's kind of obvious in hindsight but I missed it. Some numbers how the copying "eagerly" amortizes (that's for 10k logs); it's just bad for "never logs" cases but one can assume those don't really happen in a large "actual" system (vs a library) tbh. The 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. (orange/red are the |
||
} | ||
|
||
/// The `EventLoop` the Lambda is executed on. Use this to schedule work with. | ||
/// This is useful when implementing the `EventLoopLambdaHandler` protocol. | ||
/// | ||
/// - note: The `EventLoop` is shared with the Lambda runtime engine and should be handled with extra care. | ||
/// Most importantly the `EventLoop` must never be blocked. | ||
public let eventLoop: EventLoop | ||
public var eventLoop: EventLoop { | ||
self.storage.eventLoop | ||
} | ||
|
||
/// `ByteBufferAllocator` to allocate `ByteBuffer` | ||
/// This is useful when implementing `EventLoopLambdaHandler` | ||
public let allocator: ByteBufferAllocator | ||
public var allocator: ByteBufferAllocator { | ||
self.storage.allocator | ||
} | ||
|
||
internal init(requestID: String, | ||
traceID: String, | ||
|
@@ -93,20 +172,20 @@ extension Lambda { | |
logger: Logger, | ||
eventLoop: EventLoop, | ||
allocator: ByteBufferAllocator) { | ||
self.requestID = requestID | ||
self.traceID = traceID | ||
self.invokedFunctionARN = invokedFunctionARN | ||
self.cognitoIdentity = cognitoIdentity | ||
self.clientContext = clientContext | ||
self.deadline = deadline | ||
// utility | ||
self.eventLoop = eventLoop | ||
self.allocator = allocator | ||
// mutate logger with context | ||
var logger = logger | ||
logger[metadataKey: "awsRequestID"] = .string(requestID) | ||
logger[metadataKey: "awsTraceID"] = .string(traceID) | ||
self.logger = logger | ||
var baggage = Baggage.background | ||
baggage.lambdaRequestID = requestID | ||
baggage.lambdaTraceID = traceID | ||
self.storage = _Storage( | ||
baggage: baggage, | ||
invokedFunctionARN: invokedFunctionARN, | ||
deadline: deadline, | ||
cognitoIdentity: cognitoIdentity, | ||
clientContext: clientContext, | ||
// utility | ||
_logger: logger, | ||
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. not sure why we need underscore for logger or Storage 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. For logger because it's "underlying logger, don't use this, use 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. I can relate to doing so for the member, but not for the ctor argument 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. ah i see, got it will do |
||
eventLoop: eventLoop, | ||
allocator: allocator | ||
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. iirc evenloop and allocator are immutable and dont need to be at the storage level imo 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. or is this or allow them to be as part of the by-ref trick? 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. Yeah this is to keep the outer Context type small -- just a single pointer, otherwise we'd grow the struct and each time it gets |
||
) | ||
} | ||
|
||
public func getRemainingTime() -> TimeAmount { | ||
|
@@ -146,3 +225,41 @@ extension Lambda { | |
} | ||
} | ||
} | ||
|
||
// MARK: - Baggage Items | ||
|
||
extension Baggage { | ||
// MARK: - Baggage: RequestID | ||
|
||
enum LambdaRequestIDKey: Key { | ||
typealias Value = String | ||
static var name: String? { AmazonHeaders.requestID } | ||
} | ||
|
||
/// The request ID, which identifies the request that triggered the function invocation. | ||
public internal(set) var lambdaRequestID: String { | ||
get { | ||
self[LambdaRequestIDKey.self]! // !-safe, the runtime guarantees to always set an identifier, even in testing | ||
} | ||
set { | ||
self[LambdaRequestIDKey.self] = newValue | ||
} | ||
} | ||
|
||
// MARK: - Baggage: TraceID | ||
|
||
enum LambdaTraceIDKey: Key { | ||
typealias Value = String | ||
static var name: String? { AmazonHeaders.traceID } | ||
} | ||
|
||
/// The AWS X-Ray tracing header. | ||
public internal(set) var lambdaTraceID: String { | ||
get { | ||
self[LambdaTraceIDKey.self]! // !-safe, the runtime guarantees to always set an identifier, even in testing | ||
} | ||
set { | ||
self[LambdaTraceIDKey.self] = newValue | ||
} | ||
} | ||
} |
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.
Deadlines we'll eventually be able to handle in a distributed way -- so this will eventually move into the baggage.
See: Future: Deadlines and Cancelation patterns slashmo/gsoc-swift-baggage-context#24