-
Notifications
You must be signed in to change notification settings - Fork 651
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
[NodeJS] Promises and async/await #54
Comments
Thank you for moving this. I think this is an interesting idea that is worth exploring, but there are also some complications you haven't addressed. On the client side, these functions already have return values, which would be shadowed by a naive implementation of this change, or would need to be merged with the promise object. In fact, it is already possible to get this basic functionality and shadow the return value by using let call = client.sayHello(request);
call.on('metadata', metadata => {
// Handle metadata
});
response = await call; // An error would be thrown, not returned This isn't so bad, because simply ignoring the metadata and awaiting the return value directly would still work. However, client streaming calls would have the additional issue that you have to write messages and end writing before you can get the response. that code would look more like this: let call = client.clientStreamingCall();
call.write(message1);
call.write(message2);
call.end();
response = await call; In this case, directly awaiting the return value would not work, and the function would actually never continue, because the client generally needs to call On the server side, promise-based unary handlers should be just fine, but for streaming handlers, they should expect to be calling So, I think there's some work to be done here, but if you (or anyone else) submits a fleshed-out proposal for this API addition to https://github.com/grpc/proposal, I would be happy to add that change. |
Hello, Just wanted to add to the discussion. While I agree that this would be useful and nice, I think it should be approached carefully so we can get it "right" and in line with however Node core approaches async API's as that would probably be the more future-proof and idiomatic mechanism. I would still think for the time being that we have callback based API by default, at least while Node core is still callback-based by default. Expose async API to users explicitly if they ask for it. For example see this experimental proposal for promisified |
@bojand I was excited to try out grpc-caller as it seemed to solve the async/await usage with grpc in node. However I came across bojand/grpc-inspect#10 which failed to load my protos containing nested packages. Hopefully there is some way to fix grpc-inspect to resolve the nested packages correctly. |
How about making them observables using something like RxJS @murgatroid99? I'm not an expert on them, but I believe they would support streaming and metadata a little more cleanly, and would be promise-compatible. (https://medium.com/@benlesh/rxjs-observable-interop-with-promises-and-async-await-bebb05306875) https://github.com/kondi/rxjs-grpc has an implementation of this kind of thing. (I currently do promisification on the client with bluebird, but because i'm using typescript, i can't use |
I agree this is an important feature. I'm using https://github.com/carlessistare/grpc-promise#readme and it works as expected so far. I'm not a hardcore gRPC user, however, so I'm not sure what kind of "here be dragons" issues may pop up, but seems worth it for now! |
Promise is already must-have feature. For me callbacks are something from dawn of the dinosaurs :) |
Hey guys, I have just created the grpc-helper, a wrapper for the client side. The promise API: Unary call: const message = await helper.SayHello({
name: 'foo',
});
// withFullResponse: true
const { message, peer, status, metadata } = await helper.SayHello({
name: 'foo',
}); Client stream call: const stream = new stream.PassThrough({ objectMode: true });
const promise = helper.SayMultiHello(stream);
stream.write({name: 'foo1'});
stream.write({name: 'foo2'});
stream.write({name: 'foo3'});
stream.end();
// resolveFullResponse: true
const { message, peer, status, metadata } = await promise;
// { message: 'hello foo1,foo2,foo3' } Besides, I add the features like dns service discovery, load balance and circuit break etc. Currently in beta, you might as well give it a try, comments and PRs are welcome. |
This issue open for a long time. Has any roadmap for async/await which is important today? |
Three or four different approaches for using async/await with gRPC have been suggested in this issue so far. I am going to officially recommend that you use one of those. In my earlier comment I pointed out some blocking issues with integrating promise support directly into the main API. None of those have changed in the intervening time, and I have not seen any proposals for resolving them. |
FWIW, I did suggest observables, which seem to be steaming compatible and promise compatible, and potentially worthy of exploration. |
It's not really clear to me what an observable-based API would look like, so I can't tell how effective it would be at solving the problems I brought up. In addition, the current API uses standard built in APIs like callbacks and streams, and using a non-standard library like observables has the downside that Node developers will be less familiar with the general APIs. Because of that downside, I would need a particularly good reason to switch to an observable-based API. In the meantime, anyone who wants an observable-based API can use your rxjs-grpc library. |
it's still not implemented. what year is it? |
my workaround is making a util script to modify the generated service.d.ts: for any method like this: login(
requestMessage: proto_user_pb.UserLoginRequest,
metadata: grpc.Metadata,
callback: (error: ServiceError|null, responseMessage: proto_user_pb.UserLoginResult|null) => void
): UnaryResponse; can be converted to its async version: async loginAsync(
requestMessage: proto_user_pb.UserLoginRequest,
metadata: grpc.Metadata,
): Promise<{
err:ServiceError|null;
value:proto_user_pb.UserLoginResult|null;
}>; it's simple to use a regex replace: require("replace")({
paths: [p],
regex: /(.+)\(\n(.+)\n(.+)\n.+responseMessage: (.+)\).+\n.+: UnaryResponse;/gm,
replacement: (m, p1, p2, p3, p4) => {
return (
m +
`
async ${p1}Async(
${p2}
${p3}
): Promise<{
err:ServiceError|null;
value:${p4};
}>;
`
);
}
}); with this runtime util: export function promisifyGrpc(client) {
//
Object.keys(Object.getPrototypeOf(client)).forEach(function(functionName) {
const originalFunction = client[functionName];
// console.log(functionName, originalFunction);
let newFunc = async (req, mt) => {
return new Promise(fill => {
//console.log('call me', functionName, req, mt);
originalFunction.bind(client)(req, mt, (err, res) => {
fill({ err, value: res });
});
});
};
client[functionName + 'Async'] = newFunc;
});
} |
As I stated in my first comment in this thread, if someone is willing to write up a proposal that solves the problems I brought up, preferably without breaking the API, we will implement that. Until then, the current code works just fine, and multiple solutions have been suggested in this thread for using alternative types of APIs. |
Is there any update related to promises/async-await? I love using promises and hate callbacks(). I know there are many wrappers but is there any update in grpc library? |
Here is a proposal:
|
First, as mentioned in my first comment, the "proposal" I was asking for something more fleshed out that follows the format in the gRPC proposal repository. In addition, you say "result message" singular, so I assume you're referring to unary and client streaming requests, which have exactly one response message. The pattern there seems a bit odd: you make a request and pass a callback, then call Similarly, I don't really understand how |
Regarding an official proposal, I understand but working on a proposal that goes in the wrong direction would definitely be unproductive, and that's highly likely given that I've only just now started thinking about this issue 😄 You would not pass a callback to make the request. There resulting UnaryCall, ServerStream, ClientStream or Duplex instances would be amended to have a Unary call example: let call = await client.sayHello(request).toPromise()
// call.response, call.metadata call.status all available in the promise For streaming calls, Client streaming example let call = client.clientStreamingCall()
for (let message of messages) {
await call.writeAsync(message);
}
let result = call.toPromise(); If If you want only the response and don't care about status and metadata, you could have an additional convenience method e.g.
let call = client.duplexCall()
for (let message of messages) {
await call.writeAsync(message);
}
for await (let responseMessage of call) {
// process response message here.
}
There is an issue with node streams, unfortunately. Node isn't going to add the methods necessary to improve promise ergonomics to node streams, as they would risk breaking a lot of libraries that add their own methods. grpc-node can't switch from node streams to something else because that would also break backward compatibility. As such, the only viable option is probably to extend the grpc-returned stream objects with more methods. |
I'm still not following why you would want to connect getting the response as a promise to finishing writing to a stream. It seems to me that the user could just call From the current Stream documentation it looks like Streams already handle reading and writing asynchronously using async iterators, so I think it's not necessary to add additional explicit support for promise-based operations there. |
a) Is it possible to continue writing once you request to get the entire response? If not, there is no reason not to end the writing when requesting the response. Is it possible to get the entire response without ending the writing, or will it result with a forever-pending promise? If its forever-pending, that's even more reason not to allow this invalid state. b) No, async iterators only handle async reading. Async writing is not handled, it still requires plenty of choreography to do properly e.g. Basically, node streams missed the chance to have a rich API. If they were like Dart streams and had a couple of convenience properties and methods, like say https://api.dart.dev/stable/2.7.1/dart-async/Stream-class.html#instance-properties there would've been no need to do anything. But they're missing, and that its highly unlikely node core will ever add them. See for example: nodejs/readable-stream#403 |
Writing the request messages and setting up a handler for response messages are independent actions. In the current unary and client streaming APIs, the callback to handle the response is passed when starting the call. That doesn't close the call for writing, it just sets up a handler to process when the response does eventually arrive. Any promise-based API should similarly create a Promise that will eventually handle the response when it arrives, but creating that Promise shouldn't actually change the state of the call. As for async writing, the Streams documentation page has an entire section called Piping to Writable Streams from Async Iterators. I believe that the patterns there are sufficient for most use cases, and I think there is significant value to staying consistent with the built in streams API, so I don't think there is a good reason to add new async writing APIs just for grpc streams. |
Promise APIs are typically designed differently. Its quite unusual to have the promise call be passive i.e. simply provide a promise without doing anything to create it. When such APIs are designed, they're typically exposed as a property, which you can use like so:
A notable popular example for this are the DOM document readiness promises as described here: whatwg/html#1936 (API has been agreed on, implementation priority is low so it hasn't been implemented yet) A popular general pattern when promisifying APIs is to take the callback-based API and add the async suffix. There is even an automated (albeit unfortunately not type-safe in TypeScript, its dependent types are not powerful enough to express it) way to do this included in one of the most popular libraries bluebird: http://bluebirdjs.com/docs/api/promise.promisifyall.html If we take that pattern and apply it to streams, we get With the above, we have multiple options that conform to those principles:
The main reason I proposed the name
I guess my fundamental disagreement is that there is value in staying consistent with the streams API. It's pretty bad and is slowly being abandoned by modern userland libraries in recent years (e.g. see NestJS gRPC client implementation). It's not Node core's fault, it's just impossible to evolve its ergonomics without breaking userland libraries that extend the API, so it's not going to happen. More proof that it's already too late for node streams; here is how to detect the end of a stream: https://github.com/mafintosh/end-of-stream/blob/master/index.js Sticking to this API practically guarantees bad ergonomics. Which I guess doesn't make extending it a good idea, necessarily, but I'm sorry to say I don't have any better ideas to offer. |
in this page there's example how to promisify grpc client, i think it's good alternative until grpc client support promise natively |
I understand the need for holding back until the best way is decided, but in terms of how people on the JS side work with things, at the very least I would think a simple util.promisify would would suffice. However, this leads to numerous issues. Firstly, if you just do something like
the context of myClient gets lost, and you end up with some error in client.js callProperties
ultimately requiring you to bind, like so:
However, even after this, your results are some strange object:
I do not want to be the type to add to a list of complaints, but I am an experienced JS dev with a pretty good handle on callbacks, promises, and async/await, and getting this library to cooperate has been quite an uphill battle. Utilizing callbacks today is essentially a non-starter for most JS developers, due to the guarantee that it will ultimately land you in callback hell, so I am not too surprised about the complaints here, but I think if util.promisify actually was usable here, it would satisfy a large portion of JS devs needs. Do you have any insight into this strange and unexpected behavior with util.promisify? Quick Edit: that result is actually just the expected response obj, with all of the correct accessor methods. Remind me not to code too late at night :) anyhow, I feel your pain. gRPCs streaming and duplexes do put a wrench in standard usage of promises. Ideally all unary calls would be awaitable/thennable by default, with the other types of streaming requests as event emitters maybe. Either way, it looks like as long as you use util.promisify and bind to myClient, you will get what you expect - I am |
I found that this question https://stackoverflow.com/questions/39439653/events-vs-streams-vs-observables-vs-async-iterators/47214496 provides some answers as to how to convert the callback API into something more useful. I find the pull API makes more sense for both client and server streaming and unary behaviours of GRPC. |
I wrote a library on top of |
I think most, if not all, solutions for issues raised by @murgatroid99 in the first comment can be found in the design of the 1
For the cancellation feature, the client methods should accept a signal argument: const c = new AbortController()
const res = await client.sayHello(msg, c.signal) Maybe the extra arguments should be grouped, since there are many of them: const c = new AbortController()
const res = await client.sayHello(msg, { signal: c.signal, metadata, options }) 2
IIUIC, metadata is an one-off event? Why not directly return it? const { response, metadata, status, peer } = await client.sayHello(msg) Very similar to the design came up by @xizhibei Or if it really has be to async const result = await client.sayHello(msg)
const metadata = await result.metadata() // or await result.response() Very similar to 3
@xizhibei's stream design also solves this problem pretty elegantly: const promise = helper.SayMultiHello(stream);
stream.write({
name: 'foo1',
});
stream.write({
name: 'foo2',
});
stream.write({
name: 'foo3',
});
stream.end();
const result = await promise; Taken directly from grpc-helper's readme. 4
With all due respect, the problem with such recommendation is that it's very likely to end up recommending abandonware. |
|
For who are interested in how to promisify: const parseArgs = require("minimist")
import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
import { promisify } from 'util'
const helloworldProtp = __dirname + "/protos/helloworld_service.proto"
import { ProtoGrpcType as helloworldProtoGrpcType } from './protos-type/helloworld_service'
const helloworldPackageDefinition = protoLoader.loadSync(helloworldProtp, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
})
const helloworldObject = grpc.loadPackageDefinition(helloworldPackageDefinition) as unknown as helloworldProtoGrpcType;
const helloworld = helloworldObject.helloworld
function runClient() {
const argv = parseArgs(process.argv.slice(2), {
string: "target",
})
let target
if (argv.target) {
target = argv.target
} else {
target = "127.0.0.1:50059"
}
let user
if (argv._.length > 0) {
user = argv._[0]
} else {
user = "world"
}
const client = new helloworld.Greeter(target, grpc.credentials.createInsecure())
const sayHello = promisify(client.sayHello).bind(client)
const sayHelloAgain = promisify(client.sayHelloAgain).bind(client)
const call_service = async (req, service_name) => {
try {
const res = await service_name(req)
console.log("sayHello_Reply:", res.new_name, res.new_age, "typeof int64:", typeof res.new_age)
} catch (error) {
console.log("error", error);
}
}
call_service({ name: "Alan", age: 20 }, sayHello)
call_service({ name: "John", age: 30 }, sayHelloAgain)
}
runClient() |
Regarding servers with TypeScript, I think I wrote a hack-ish function promiseWrap<T, U>(call: (origCall: grpc.ServerUnaryCall<T, U>) => Promise<U>): grpc.handleUnaryCall<T, U> {
return (oc: grpc.ServerUnaryCall<T, U>, cb: sendUnaryData<U>) => {
call(oc).then((rval) => cb(null, rval)).catch(cb);
}
} Usage: const methods: YourHandlers = {
YourMethod: promiseWrap(async (call) => {
// Your code here
}),
}; I suppose similar could not be done for Bidi/Unidirectional streaming calls for somewhat obvious reasons (event-based-ness 😄) |
To my taste it is way better to create a typescript wrapper for the client directly. const client = new SearchServiceClient('host', ChannelCredentials.createInsecure());
const searchServiceClient: Promisified<SearchServiceClient> = promisify(client);
const request = new SearchRequest().setQuery(query);
const response = await searchServiceClient.search(request); This preserves all original signatures (with callback changed to Promise), so it still allows to use absolutely the same interceptors, metadata, etc, and that's why not invasive |
Hi guys, thats works very good for me. ` const client = new mongoProto.MongoService(host grpc.credentials.createInsecure()) |
This problem can be solved in a more sophisticated way with typescript, the code: import { credentials, Metadata } from '@grpc/grpc-js';
import { promisify } from 'util';
import { SayHelloClient, sayHelloRequest, sayHelloResponse } from '../../models/sayHello';
class ClientServiceGrpc {
private readonly client: SayHelloClient = new SayHelloClient('localhost:50051', credentials.createInsecure());
public async sayHello(param: sayHelloRequest, metadata: Metadata = new Metadata()): Promise<sayHelloResponse> {
return promisify<sayHelloRequest, Metadata, sayHelloResponse>(this.client.sayHello.bind(this.client))(param, metadata);
}
}
export const clientServiceGrpc: ClientServiceGrpc = new ClientServiceGrpc(); Good luck 😋 |
Still no official support for async/await/Promises? This is a serious issue. People have been trying to not write callbacks in node for the last 5 years. Makes applications hard to read and maintain. Adding noisy promisify boilerplate everywhere is also not ideal. That plain looks like you are using legacy deprecated tech. |
2022, please add promise. |
October 2022, please add promise. |
@murgatroid99 What do you think about just having the Unary calls as promises and other to be as is? I am more than willing to submit an official proposal/pull request. To me there is no need to change the way the streaming calls work in any way. I will later submit my way of doing this. So who agrees with me on this? |
I would prefer a solution here to cover both unary and client streaming requests, because those two currently handle results in the same way. But I will not reject a proposal that only changes unary. Please send your proposal so that we can all evaluate the details of your proposed change. |
@ErickJ3 class ClientServiceGrpc {
private readonly client: SayHelloClient = new SayHelloClient('localhost:50051', credentials.createInsecure());
public async sayHello(param: sayHelloRequest, metadata: Metadata = new Metadata()): Promise<sayHelloResponse> {
return promisify(this.client.sayHello.bind(this.client, param, metadata, {}))();
}
} by passing all possible |
We chose to create a simple wrapper export const grpc = new AppClient(url, creds());
type Call<Input, Ret> = (
input: Input,
callback: (err: ServiceError | null, ret?: Ret) => void,
) => ClientUnaryCall;
export const call = <Input, Ret>(call: Call<Input, Ret>, input: Input): Promise<Ret | undefined> => promisify(call).call(grpc, input) And use: const res = await call(grpc.sayHello, {name: "Gui"}); |
已经收到您的来信,我会尽快回复。
|
This inspired me to write a full wrapper for all four method types, based on promises and async generators: https://gist.github.com/paskozdilar/196b212a1df66463487fa2b75dde049c This wrapper also uses |
It would be nice if there was a
grpc-tools
and/orgrpc_node_plugin
option that would generate client and server code that follows the ES6 Promise and ES7 async/await model.For example, instead of:
https://github.com/grpc/grpc/blob/98f8989a6712118549c432c9f91b6a516250014d/examples/node/static_codegen/greeter_client.js#L35-L37
We could write:
Or something similar. On the server side, instead of:
https://github.com/grpc/grpc/blob/98f8989a6712118549c432c9f91b6a516250014d/examples/node/static_codegen/greeter_server.js#L27-L31
We could write:
Again, just ideas.
This feature would eliminate the need for wrapper code for users wanting to use this pattern. I would love to know if anyone is interested in this feature or planning on adding it. Thanks!
(moved from grpc/grpc#12751)
The text was updated successfully, but these errors were encountered: