Skip to content
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

Fix premature leaving of context due to improper Http2ServerCallStream handling #2501

Merged
merged 4 commits into from Jul 13, 2023

Conversation

CedricKassen
Copy link
Contributor

To provide context and metadata in our internal gRPC handling framework, we utilize nodejs's asynchronous context tracking, specifically metadata namespaces similar to cls-hooked, implemented with AsyncLocalStorage.

Due to the asynchronous nature of the Http2ServerCallStream, the synchronous handling of receiving unary messages introduced in #2249 causes unexpected behavior. Handling the stream outside of a Promise makes node leave the context prematurely before continuing the message handling, therefore rendering access to the information stored with AsyncLocalStorage impossible.

@linux-foundation-easycla
Copy link

linux-foundation-easycla bot commented Jul 12, 2023

CLA Signed

The committers listed above are authorized under a signed CLA.

const call = this;
const body: Buffer[] = [];
const limit = this.maxReceiveMessageSize;
async function onData(chunk: Buffer) {
Copy link
Member

Choose a reason for hiding this comment

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

Making a function async has two main effects: it can await things and it returns a promise. This function does not await anything and it is used as an event listener so its return value is not used. So, I don't see any reason to make that change.

onEnd has the same issue.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, this was a slight oversight. I halfway through opted to make this change as little as possible and overlooked this

resolve: (
value: void | RequestType | PromiseLike<void | RequestType>
) => void,
reject: (reason: any) => void
Copy link
Member

Choose a reason for hiding this comment

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

I don't like passing resolve and reject in here. It's not a normal async flow and it makes the code logic more complicated. It would be better for this function to return a promise itself. In fact, I think that if you make this function async and replace the entire function body with return this.deserializeMessage(buffer), any error will bubble up as a promise rejection, which can be handled by the caller. Alternatively, you could keep the try...catch, but throw the object that is currently passed to reject.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was in an attempt to make this method behave in the same way as before. I can see how this definitely makes it less understandable. I opted to the second option, keeping the try...catch, because I want to keep the Error mapping, and otherwise I'd have to do it twice .

Changing the implementation in this way did make the name of the method untrue, which is why I renamed it now to deserializeMessageWithInternalError. The name is a little on the longer side, but it's a private method and in my opinion definitely increases the readability.

packages/grpc-js/src/server.ts Outdated Show resolved Hide resolved
compressedMessageEncoding
);
if (Buffer.isBuffer(decompressedMessage)) {
call.deserializeMessageWithInternalError(decompressedMessage);
Copy link
Member

Choose a reason for hiding this comment

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

Now that this method returns the value, you have to do something with that value here and at the other call site. The way it currently works, I think you need another try/catch block here to pass the result into resolve or reject as appropriate. However, I think if you make deserializeMessageWithInternalError into an async function, it will automatically translate the throw there into a rejected promise. Then, since resolve here accepts a PromiseLike<RequestType | void>, you can pass the result of that call directly to resolve and it will handle both cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh yeah you're right, I neglected the resolve there. I now turned it into an async function and specifically specified the resolve and catch at the call accordingly to make it very clear what is happening to the result. Otherwise it might become unclear to some, why deserializeMessageWithInternalError is async.

I thought if awaiting it might matter, because it would make the handling more complicated. But I don't think it makes a difference, because we're inside of another promise that only resolves after this call is finished anyway.

call
.deserializeMessageWithInternalError(decompressedMessage)
.then(resolve)
.catch(reject);
Copy link
Member

Choose a reason for hiding this comment

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

I'm pretty sure this can just be resolve(call.deserializeMessageWithInternalError(decompressedMessage)). The output of that function is now a promise-like object, which can be passed directly to resolve.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I actually preferred the readability of explicitly passing resolve and reject, to actually show why we might want a Promise here, but I can also understand why you would want to keep this as simple as possible.
I'll push a change for that as well.

Copy link
Member

Choose a reason for hiding this comment

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

From my point of view, part of the benefit of using promises is the ability to pass them around transparently in APIs that handle them. If you're concerned about communicating something to future readers, you can add a comment wherever you think it's relevant to explain it explicitly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not really concerned, it's still very readable. I just personally prefer writing explicit code, but as I said I also understand why keeping it simplistic has its benefits, and I definitely want to adhere to the code style this repository aims to maintain.

Copy link
Member

@murgatroid99 murgatroid99 left a comment

Choose a reason for hiding this comment

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

Thank you for your contribution.

@murgatroid99 murgatroid99 merged commit 9ce6e49 into grpc:master Jul 13, 2023
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants