-
Notifications
You must be signed in to change notification settings - Fork 116
[DNM][RFC] alternative api #18
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
Conversation
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.
Great progress! The typealias
es are on their way out. More comments inline.
func encode(_: Out) -> Result<[UInt8], Error> { fatalError("not implmented") } | ||
func decode(_: [UInt8]) -> Result<In, Error> { fatalError("not implmented") } | ||
func encode(_: Out) -> Result<ByteBuffer, Error> { fatalError("not implmented") } | ||
func decode(_: ByteBuffer) -> Result<In, Error> { fatalError("not implmented") } |
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.
Just out of curiosity why do we use Result
here and not plain old throws
?
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.
mostly my personal dislike of try/catch blocks
} | ||
public protocol InitializableLambdaHandler { | ||
/// Initializes the `LambdaHandler`. | ||
func initialize(promise: EventLoopPromise<Void>) |
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 can totally see the idea of using the initialize(promise: EventLoopPromise<Void>)
method to report errors during startup as discussed in #11 and I think the intention is very good.
I'm not sure though if it would be better to go all in and make the initialize
a real init
?
For example we could set it up like this
public protocol InitializableLambdaHandler {
@inlinable /// Initializes the `LambdaHandler`.
func init(promise: EventLoopPromise<Void>, eventLoopGroup: EventLoopGroup) throws
}
I can see that this looks kind of ugly injecting the EventLoopPromise
for reporting async errors and having throws
for immediate errors that prevent one from setting up the Handler object/struct. But thinking about usecases I can imagine having clean mains.
Lambda.run(MyLambdaHandler.self)
struct MyLambdaHandler: InitializableLambdaHandler, LambdaHandler {
let httpClient: AsyncHTTPClient
init(promise: EventLoopPromise<Void>, eventLoopGroup: EventLoopGroup) throws {
self.httpClient = AsyncHTTPClient(.shared(eventLoopGroup))
// other setup with eventLoopGroup: database clients, aws sdk, whatevar.
// in context of aws especially important is getting keys from secret store,
// which is why the promise might be important
promise.succeed(Void())
}
func handle() {
...
}
}
Getting access to the eventLoopGroup
created by the Lambda could be a huge benefit, especially for creating clients.
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.
on one hand I like the idea of init
instead of a separate initialize
, on the other hand this would not be a normal ctor as you would be expected to compete the promise, so would be strange API either way.
I also dont think we need to make it a throwable. if you want to return an error catch it and fail the promise
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 don't know if it would compile though. ;)
struct MyLambdaHandler: InitializableLambdaHandler, LambdaHandler {
let httpClient: AsyncHTTPClient
init(promise: EventLoopPromise<Void>, eventLoopGroup: EventLoopGroup) {
if somethingUnexpected {
return // not everything is initialized here
}
self.httpClient = AsyncHTTPClient()
}
If we want to report an error during bootup, we could also offer a static function. Then we don't have to do the init dance.
Lambda.reportBootupError(_: error)
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.
another alternative is to define an optional blocking initializer and that's it, so users won't be able to to any significant work in it but the signature can be simplified to:
init(eventLoop: EventLoop) throws
but this closes the window on doing things like getting one-time-initialization-type-data from some database or upstream service, which could be handy
or another option is to have both: aninit(eventLoop: EventLoop) throws
where you can do things like setup you http-client, and also abootstrap(eventLoop: EventLoop, promise: Promise<Void>)
for the getting one-time-initialization-type-data-from-some-database-or-upstream-service use case.
both could/should be optional. maybe that's the best?
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.
We should insert the complete EventLoopGroup
. In Lambdas with more than 1500mb ram, one gets access to two cores. Developers should be allowed to use them ;)
I don't know if we can make an init(eventLoop: EventLoop) throws
optional.
@@ -33,17 +33,17 @@ internal struct LambdaRuntimeClient { | |||
} | |||
|
|||
/// Requests work from the Runtime Engine. | |||
func requestWork(logger: Logger) -> EventLoopFuture<(LambdaContext, [UInt8])> { | |||
func requestWork(logger: Logger) -> EventLoopFuture<(LambdaContext, ByteBuffer)> { |
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.
As stated in the other pr, I'd really like to get LambdaContext out of LambdaRuntimeClient
by introducing an Invocation
struct, that reflect the API response more closely and then creating the LambdaContext
with the Invocation
in the LambdaRunner
. With this we would have a much cleaner separation of concerns.
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.
A proposal can be found here: #19
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.
+1
@@ -289,6 +277,8 @@ public struct LambdaContext { | |||
public let clientContext: String? | |||
public let deadline: String? | |||
// utliity | |||
public let eventLoop: EventLoop | |||
public let allocator: ByteBufferAllocator |
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.
+1 for having the ByteBufferAllocator
in the context as well.
guard let data = buffer.getData(at: buffer.readerIndex, length: buffer.readableBytes) else { | ||
throw Errors.invalidBuffer | ||
} | ||
return .success(try self.decoder.decode(In.self, from: data)) |
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.
You import NIOFoundationCompat
that's why you should just be able to:
return .success(try self.decoder.decode(In.self, from: buffer))
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.
iirc that returns an optional, so we would end up with same # loc
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'm afraid if something is invalid it throws, otherwise returns a nonoptional.
@@ -12,6 +12,8 @@ | |||
// | |||
//===----------------------------------------------------------------------===// | |||
|
|||
import NIO |
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.
Do we maybe want to remove this file for now? When playing around with the handler syntax, this is just a shore to fix. Could be added back later, without any problems.
/// Initializes the `LambdaHandler`. | ||
func initialize(callback: @escaping LambdaInitCallBack) | ||
func handle(context: LambdaContext, payload: [UInt8], callback: @escaping LambdaCallback) | ||
func handle(context: LambdaContext, payload: ByteBuffer, promise: EventLoopPromise<ByteBuffer>) |
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.
As stated in #11 (comment) the returning ByteBuffer
should be optional.
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.
+1
switch self.codec.decode(payload) { | ||
case .failure(let error): | ||
return callback(.failure(Errors.requestDecoding(error))) | ||
return promise.fail(Errors.requestDecoding(error)) | ||
case .success(let payloadAsCodable): | ||
self.handle(context: context, payload: payloadAsCodable) { result in |
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.
For every non EventLoopPromise based callback I'd build an offloaded sync/async version.
@fabianfett created #21 with some more ideas |
replaced by #33 |
do not merge, for discussion (#11) purpose