Skip to content
This repository has been archived by the owner on Sep 26, 2023. It is now read-only.

feat: introduce HttpJsonClientCall, Listeners infrastructure and ServerStreaming support in REST transport #1599

Merged
merged 8 commits into from Jan 21, 2022

Conversation

vam-google
Copy link
Contributor

@vam-google vam-google commented Jan 12, 2022

This includes the following changes for HTTP1.1/REST transport:

  1. HttpJsonClientCall class (with HttpJsonClientCall.Listener) mimicking io.grpc.ClientCall functionality. Most of the complexity of this PR is concentrated in HttpJsonClientCallImpl class.
  2. The unary callables are rewritten to be based on HttpJsonClientCall flow (similarly to how it is already done in gRPC unary calls).
  3. Server streaming support for REST transport. The implementation is based on HttpJsonClientCall and HttpJsonClientCall.Listener (introduced in this PR), similarly to how gRPC streaming is based on io.grpc.ClientCall and io.grpc.ClientCall.Listener (implemented in grpc-java library) respectively.

The extreme similarity between HttpJsonClientCall call and io.grpc.ClientCall is intentional and crucial for consistency of the two transports and also intends simplifying creation and maintenance of multi-transport manual wrappers (like google-ads-java).

The server streaming abstractions in gax java are all based on the flow control managed by a ClientCall, so having similar set of abstractions in REST transport is necessary to reuse transport-independent portions of streaming logic in gax and maintain identical user-facing streaming surface.

This PR also builds a foundation for the soon-coming ClientInterceptor-like infrastructure in REST transport. This is specifically required to support REST transport in google-ads-java.

REST-based client-side streaming and bidirectional streaming is not implemented by this PR and most likely will never be due to limitations of the HTTP1.1/REST protocol compared to HTTP2/gRPC.

Most of the java docs in HttpJsonClientCall class is a modified version of the java docs from io.grpc.ClientCall, which is intentional, because HttpJsonClientCall is designed to be as similar to io.grpc.ClientCall in both surface and behavior as possible (while the two classes cannot be a part of the same class hierarchy, because they belong to two independent transport layers).

What server-streaming means in case of REST transport
In REST transport server-streaming methods return a JSON array of response messages (i.e. the array element type is the same one used as a returned type in the corresponding method definition in protobuf). The response is provided as as Chunck-encoded input stream, containing one big JSON array. To parse the json array we rely on JsonReader from gson library, which gax-httpjson already depended on even prior this PR (check ProtoMessageJsonStreamIterator class implementation in this PR for details). Note, we must process elements of the array one-by-one because the size of the full array may be in realm of gigabytes.

Note, ideally I need to split this PR at least in two separate ones: 1) HttpJsonClientCall stuff and unary calls based on it in one PR and then 2) server streaming feature in a second PR. Unfortunately the most reasonable way to test HttpJsonClientCall infrastructure is by doing it from server streaming logic beause most of the complexity introduced in HttpJsonClient call is induced by necessity to support streaming workflow in the first place (and to support call interceptors (not part of this PR) as a secondary goal).

Note, there are a few minor breaking changes in gax-httpjson module (and only there) inroduced in this PR. This should be ok, because unlike gax and gax-grpc, gax-httpjson is not GA yet. The breaking changes are very minor (in the space of HttpJsonCallContext and ManagedHttpJsonChannel) and are backward-compatible with java-compute (the main and only officially supported user of gax-httpjson as of now).

…erStreaming support in REST transport

This includes the following changes for HTTP1.1/REST transport:
1) `HttpJsonClientCall` class (with `HttpJsonClientCall.Listener`) mimicking [io.grpc.ClientCall](https://github.com/grpc/grpc-java/blob/master/api/src/main/java/io/grpc/ClientCall.java#L102) functionality.
2) The unary callables are rewritten to be based on `HttpJsonClientCall` flow (similarly to how it is already done in grpc unary calls).
3) Server streaming support for REST transport. The implementation is based on `HttpJsonClientCall` and `HttpJsonClientCall.Listener`, similarly to how grpc streaming is based on `io.grpc.ClientCall` and io.grpc.ClientCall.Listener` respectively.

The extreme similarity between `HttpJsonClient` call and `io.grpc.ClientCall` is intentional and crucial for consistency of the two transports and also intends simplifying creation and maintenance of multi-transport manual wrappers (like [google-ads](https://github.com/googleads/google-ads-java/blob/main/google-ads/src/main/java/com/google/ads/googleads/lib/logging/LoggingInterceptor.java#L68)).

The server streaming abstractions in gax java are all based on the flow control managed by a ClientCall, so having similar set of abstractions in REST transport is necessary to reuse transport-independent portions of streaming logic in gax and maintain identical user-facing streaming surface.

This PR also builds a foundation for the soon-coming [ClientInterceptor](https://github.com/grpc/grpc-java/blob/master/api/src/main/java/io/grpc/ClientInterceptor.java#L42)-like infrastructure in REST transport.

REST-based client-side streaming and bidirectional streaming is not implemented by this PR and most likely will never be due to limitations of the HTTP1.1/REST protocol compared to HTTP2/gRPC.

Note, ideally I need to split this PR at least in two separate ones: 1) HttpJsonClientCall stuff and unary calls based on it in one PR and then 2) server streaming feature in a second PR. Unfortunately only reasonable way to test `HttpJsonClientCall` infrastructure is by doing it from server streaming logic beause most of the complexity introduced in HttpJsonClient call is induced by necessity to support streaming workflow.
@vam-google vam-google requested review from a team as code owners January 12, 2022 04:17
@vam-google vam-google changed the title feat: Introduce HttpJsonClientCall, Listeners infrastructure and ServerStreaming support in REST transport feat: introduce HttpJsonClientCall, Listeners infrastructure and ServerStreaming support in REST transport Jan 12, 2022

Builder builder = this.toBuilder();

Instant newDeadline = inputOptions.getDeadline();
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm a bit confused about the semantics of "merge." I see it's really just forcing the input values except for when the values are null. I wonder if null should be forcible too? For example, I want to clear the deadline (i.e., set it to null), but it's not doable.

If the current implementation is intended, I think it's worth documenting the exact behavior of merge() in Javadoc.

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 merge semantics is a pattern in gax java already, a good example would be GrpcCallContext:
https://github.com/googleapis/gax-java/blob/main/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcCallContext.java#L321

To clear deadline one is expected to clear it explicitly (Builder.setDeadline(null)), it is not responsibility of merge logic.

Also, clearing something is not typical and not idiomatic in gax/grpc - usually if something becomes non-null it never gets
back to null state (null basically means never touched).

//
// A lock to guard the state of this call (and the response stream).
//
private final Lock lock = new ReentrantLock();
Copy link
Contributor

Choose a reason for hiding this comment

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

Looking at the use of lock in this class (single lock and only used for trivial mutual exclusions), I think we can simply use synchronized blocks to greatly simplify the code. Clean code without try ... finally. Even no need for a lock instance.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, you are right, thought I did it intentionally here. I'm not aware of synchronized being preferred to java.util.concurrent locks, if anything it is opposite to the best of my knowledge. Even though more sophisticated features of ReentrantLock are not used now, we may need them in the future. Basically I usually prefer using java.util.concurrent every time there is a potential of needing heavyweight synchronization, and leave synchronized stuff for trivial cases. You are right that here it is used only for mutual exclusions (not the case in earlier versions of this code btw), but there is still quite a lot of careful synchronization needed and many fields need to be syncrhonized propely. I can change it to synchronized if you have a strong opinion on it though. If you don't I would prefer keeping it as is.

Please let me know what you think. Thanks!

Copy link
Contributor

Choose a reason for hiding this comment

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

Discussed offline and decided to use synchronized.

Copy link
Contributor

@chanseokoh chanseokoh left a comment

Choose a reason for hiding this comment

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

Sorry, just throwing comments haphazardly. It's a long PR, so I think I still need a lot of time to look at various things.

* HTTP status code in RuntimeException form, for propagating status code information via
* exceptions.
*/
public class StatusRuntimeException extends RuntimeException {
Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, now I see this is for HTTP status codes. How about renaming this so that it becomes clear? Also the parameter name statusCode to, e.g., httpCode.

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 exception mimics the one in grpc: https://github.com/grpc/grpc-java/blob/master/api/src/main/java/io/grpc/StatusRuntimeException.java, thus the same name is used. I can rename it to HttpJsonStatusRuntimeException though to stay consistent with the naming scheme of prefixing stuff with HttpJson and to make it easier and less confusing for multitransport code (which may need to handle both exceptions, and one will have to be using fully-qualified name to disambiguate, which is annoying).

Copy link
Contributor

Choose a reason for hiding this comment

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

Your call, but having to use fully-qualified name makes me lean toward prefixing HttpJson, which is the current pattern as well, as you said.

Comment on lines +76 to +80
if (!hasStarted) {
numRequested += count;
} else {
clientCall.request(count);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Can request() and start() ever run concurrently? If so, this isn't going to work as inteded unless there is a proper synchronization. (For example, on line 76, hasStarted may be false, but the moment you reach line 77, the value can be flipped.) Just to confirm this doesn't happen.

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 code, as most parts of this PR mimicks grpc behavior, and this specific logic is from https://github.com/googleapis/gax-java/blob/main/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcDirectStreamController.java#L89, which is not sycnhronized either. The grpc logic has been there for a few years already and no any race conditions problems so far. Basicaly this logic is externally synchronized. I made those variables volatile just in case to make sure thread visibility. Probably I need to revert them back to non-volatile ones to eliminate confusin.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, maybe revert them back. I don't see them in the gRPC side either, and actually, it was those volatile keywords that made me to think about concurrency concerns in this class. Since you said this is externally synchronized, it should rather be explicit, I think.

import javax.annotation.concurrent.GuardedBy;

/**
* This class servers as main implementation of {@link HttpJsonClientCall} for rest transport and is
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
* This class servers as main implementation of {@link HttpJsonClientCall} for rest transport and is
* This class serves as main implementation of {@link HttpJsonClientCall} for REST transport and is

@vam-google
Copy link
Contributor Author

@chanseokoh @meltsufin PTAL

private final HttpJsonCallOptions callOptions;
@Nullable private final Duration timeout;
@Nullable private final Duration streamWaitTimeout;
@Nullable private final Duration streamIdleTimeout;
private final ImmutableMap<String, List<String>> extraHeaders;
private final ApiCallContextOptions options;
private final ApiTracer tracer;
Copy link
Contributor

Choose a reason for hiding this comment

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

tracer is also @Nullable.

Copy link
Member

@meltsufin meltsufin left a comment

Choose a reason for hiding this comment

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

LGTM!

@vam-google vam-google merged commit 3c97529 into googleapis:main Jan 21, 2022
gcf-merge-on-green bot pushed a commit that referenced this pull request Jan 21, 2022
🤖 I have created a release *beep* *boop*
---


## [2.10.0](v2.9.0...v2.10.0) (2022-01-21)


### Features

* add api key support ([#1436](#1436)) ([5081ec6](5081ec6))
* introduce HttpJsonClientCall, Listeners infrastructure and ServerStreaming support in REST transport ([#1599](#1599)) ([3c97529](3c97529))

---
This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
vam-google added a commit to googleapis/sdk-platform-java that referenced this pull request Jan 22, 2022
suztomo pushed a commit to googleapis/sdk-platform-java that referenced this pull request Dec 16, 2022
🤖 I have created a release *beep* *boop*
---


## [2.10.0](googleapis/gax-java@v2.9.0...v2.10.0) (2022-01-21)


### Features

* add api key support ([#1436](googleapis/gax-java#1436)) ([e85699a](googleapis/gax-java@e85699a))
* introduce HttpJsonClientCall, Listeners infrastructure and ServerStreaming support in REST transport ([#1599](googleapis/gax-java#1599)) ([7003903](googleapis/gax-java@7003903))

---
This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
suztomo pushed a commit to googleapis/sdk-platform-java that referenced this pull request Dec 16, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants