-
Notifications
You must be signed in to change notification settings - Fork 280
Allow to make future chain request with ResponseFuture #419
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
|
|
d485aff to
be63dfe
Compare
|
@mraleph hi, do you have a chance to take a look at this PR? |
mraleph
left a comment
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.
Some initial review comments.
Thank you for the contribution!
| class ResponseFuture<R> extends DelegatingFuture<R> | ||
| with _ResponseMixin<dynamic, R> { | ||
| final ClientCall<dynamic, R> _call; | ||
| final ClientCall _call; |
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.
Why did you remove <dynamic, R>? This makes code less type safe.
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.
Because of this: #419 (comment)
| ResponseFuture._clone(future, clientCall: _getClientCall(future, _call)); | ||
|
|
||
| /// clientCall maybe be lost when converting from Future to ResponseFuture | ||
| static ResponseFuture<T> wrap<T>(Future<T> future, {ClientCall clientCall}) { |
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 actually have any use for wrap beyond testing?
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 need a way to convert Future to ResponseFuture and vice versa. You can see we use it in the very first step in this example:
@override
ResponseFuture<R> interceptUnary<Q, R>(ClientMethod<Q, R> method, Q request,
CallOptions options, ClientUnaryInvoker<Q, R> invoker) {
_invocations.add(InterceptorInvocation(_id, ++_unary, _streaming));
return ResponseFuture.wrap(
Future.delayed(Duration(seconds: 1), () => 'dummy'))
.then((_) => invoker(method, request, _inject(options)))
.then((foo) async => foo)
.whenComplete(() => 'complete')
.then((bar) => bar)
.catchError((e, s) => print('$e at $s'))
.timeout(Duration(seconds: 5));
}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.
One idea- could you make an extension on Future / ResponseFuture instead?
|
|
||
| abstract class _ResponseMixin<Q, R> implements Response { | ||
| ClientCall<Q, R> get _call; | ||
| ClientCall get _call; |
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 not loose type safety.
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.
.then(...) allows to transform a ResponseFuture<R> to a ResponseFuture<S> which will lose type safety. Do you have any suggestion?
@override
ResponseFuture<S> then<S>(FutureOr<S> Function(R p1) onValue, {Function onError});
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 realized that we don't actually use the type of call for anything. So I suggest the following change: it is indeed fine to drop types here, but the we should add types in default constructor to keep initial type safety:
ResponseFuture(ClientCall<dynamic, R> call) : _call = call, super(call.response
.fold<R?>(null, _ensureOnlyOneResponse)
.then(_ensureOneResponse));
|
|
||
| @override | ||
| Future<void> cancel() => _call.cancel(); | ||
| Future<void> cancel() => _call?.cancel(); |
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 think _call becoming nullable is very unfortunate. Can we avoid that? I think it will make NNBD migration harder.
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 Future doesn't have _call, converting a Future to a ResponseFuture will make _call == null. Let me revisit the change once more time to find a better approach.
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 thing we can do is having a convention that the final ResponseFuture in the chain must have a valid _call.
ce5b881 to
74b55f9
Compare
|
Btw, as |
|
@mraleph I know that this PR is quite outdated with recent changes about NNBD support. But what do you think about making |
|
I think original gRPC library design is really gnarly here, it's kinda painting us into the corner. For example, now that I look at it with fresh-eyes I realise that in your example: @override
ResponseFuture<R> interceptUnary<Q, R>(ClientMethod<Q, R> method, Q request,
CallOptions options, ClientUnaryInvoker<Q, R> invoker) {
_invocations.add(InterceptorInvocation(_id, ++_unary, _streaming));
return ResponseFuture.wrap(
Future.delayed(Duration(seconds: 1), () => 'dummy'))
.then((_) => invoker(method, request, _inject(options)))
.then((foo) async => foo)
.whenComplete(() => 'complete')
.then((bar) => bar)
.catchError((e, s) => print('$e at $s'))
.timeout(Duration(seconds: 5));
}interceptor simply returns a So I guess the question here is: do we want to find a way to support asynchronous actions before calling In that case |
|
@rockerhieu @mraleph Hi,
Future<R> then<R>(FutureOr<R> onValue(T value), {Function? onError});Should final response = stub.sayHello(request).then( (HelloReply reply) => 'success');Wouldn't it be better to create a new method ? ResponseFuture<T> responseThen<T>(FutureOr<T> onValue(T value), {Function? onError});stub.sayHello(request)
.responseThen( (HelloReply reply) => reply);
|
The type of the call does not really matter. See my comment above: #419 (comment) I think the biggest question for this PR is how to correctly support asynchronous operations before |
|
This PR enables features that are not practical to do another way, like transforming general grpc errors to typed exceptions. One workaround could be to use stream transformations, but it seems a bit excessive. |
|
The response type shouldn't be changed over interceptor, it's obviously very bad idea, on top of that we lose type-safety. Interceptors should behave as complementary like Please just let |
@esenmx can you elaborate on the |
If you are talking about merging the @override
ResponseFuture<R> interceptUnary<Q, R>(
ClientMethod<Q, R> method, Q request, CallOptions options, ClientUnaryInvoker<Q, R> invoker) {
final stopwatch = Stopwatch()..start();
return invoker(method, request, options)
..trailers.then((trailers) {
trailers['timeout'] = stopwatch.elapsedMilliseconds.toString();
});
}
// request
final result = client.doSomething();
result.trailers.then((value) {
print('timeout: ${value['timeout']}');
}); |
|
I think instead of adding support for chaining Let me know if that would solve all of your potential use cases. |
|
I am going to close this PR because direct chaining has some hidden pitfalls (e.g. one highlighted in #419 (comment)). |
Closes #413
This PR allows to make future chain request with
ResponseFuture: