/*
* Copyright 2017 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package com.linecorp.armeria.client.grpc;
import static com.google.common.base.Preconditions.checkState;
import static java.util.Objects.requireNonNull;
import java.util.concurrent.CancellationException;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.linecorp.armeria.client.Client;
import com.linecorp.armeria.client.ClientRequestContext;
import com.linecorp.armeria.common.RequestContext;
import com.linecorp.armeria.common.grpc.GrpcSerializationFormats;
import com.linecorp.armeria.common.http.DefaultHttpRequest;
import com.linecorp.armeria.common.http.HttpData;
import com.linecorp.armeria.common.http.HttpHeaders;
import com.linecorp.armeria.common.http.HttpRequest;
import com.linecorp.armeria.common.http.HttpResponse;
import com.linecorp.armeria.common.util.SafeCloseable;
import com.linecorp.armeria.internal.grpc.ArmeriaMessageDeframer;
import com.linecorp.armeria.internal.grpc.ArmeriaMessageDeframer.ByteBufOrStream;
import com.linecorp.armeria.internal.grpc.ArmeriaMessageFramer;
import com.linecorp.armeria.internal.grpc.GrpcHeaderNames;
import com.linecorp.armeria.internal.grpc.GrpcMessageMarshaller;
import com.linecorp.armeria.internal.grpc.HttpStreamReader;
import com.linecorp.armeria.internal.grpc.TimeoutHeaderUtil;
import com.linecorp.armeria.internal.grpc.TransportStatusListener;
import io.grpc.CallOptions;
import io.grpc.ClientCall;
import io.grpc.Codec.Identity;
import io.grpc.Compressor;
import io.grpc.CompressorRegistry;
import io.grpc.DecompressorRegistry;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import io.grpc.Status;
import io.netty.buffer.ByteBuf;
/**
* Encapsulates the state of a single client call, writing messages from the client and reading responses
* from the server, passing to business logic via {@link ClientCall.Listener}.
*/
class ArmeriaClientCall<I, O> extends ClientCall<I, O>
implements ArmeriaMessageDeframer.Listener, TransportStatusListener {
private static final Runnable NO_OP = () -> { };
private static final Metadata EMPTY_METADATA = new Metadata();
private static final Logger logger = LoggerFactory.getLogger(ArmeriaClientCall.class);
private final ClientRequestContext ctx;
private final Client<HttpRequest, HttpResponse> httpClient;
private final DefaultHttpRequest req;
private final CallOptions callOptions;
private final ArmeriaMessageFramer messageFramer;
private final GrpcMessageMarshaller<I, O> marshaller;
private final CompressorRegistry compressorRegistry;
private final DecompressorRegistry decompressorRegistry;
private final HttpStreamReader responseReader;
@Nullable
private final Executor executor;
// Effectively final, only set once during start()
private Listener<O> listener;
private boolean cancelCalled;
ArmeriaClientCall(
ClientRequestContext ctx,
Client<HttpRequest, HttpResponse> httpClient,
DefaultHttpRequest req,
MethodDescriptor<I, O> method,
int maxOutboundMessageSizeBytes,
int maxInboundMessageSizeBytes,
CallOptions callOptions,
CompressorRegistry compressorRegistry,
DecompressorRegistry decompressorRegistry) {
this.ctx = ctx;
this.httpClient = httpClient;
this.req = req;
this.callOptions = callOptions;
this.compressorRegistry = compressorRegistry;
this.decompressorRegistry = decompressorRegistry;
this.messageFramer = new ArmeriaMessageFramer(ctx.alloc(), maxOutboundMessageSizeBytes);
this.marshaller = new GrpcMessageMarshaller<>(ctx.alloc(), GrpcSerializationFormats.PROTO, method);
responseReader = new HttpStreamReader(
decompressorRegistry,
new ArmeriaMessageDeframer(this, maxInboundMessageSizeBytes, ctx.alloc()),
this);
executor = callOptions.getExecutor();
}
@Override
public void start(Listener<O> responseListener, Metadata unused) {
requireNonNull(responseListener, "responseListener");
final Compressor compressor;
if (callOptions.getCompressor() != null) {
compressor = compressorRegistry.lookupCompressor(callOptions.getCompressor());
if (compressor == null) {
responseListener.onClose(
Status.INTERNAL.withDescription(
"Unable to find compressor by name " + callOptions.getCompressor()),
EMPTY_METADATA);
return;
}
} else {
compressor = Identity.NONE;
}
messageFramer.setCompressor(compressor);
prepareHeaders(req.headers(), compressor);
listener = responseListener;
final HttpResponse res;
try {
res = httpClient.execute(ctx, req);
} catch (Exception e) {
try (SafeCloseable ignored = RequestContext.push(ctx)) {
listener.onClose(Status.fromThrowable(e), EMPTY_METADATA);
}
return;
}
res.subscribe(responseReader);
}
@Override
public void request(int numMessages) {
if (ctx.eventLoop().inEventLoop()) {
responseReader.request(numMessages);
} else {
ctx.eventLoop().submit(() -> responseReader.request(numMessages));
}
}
@Override
public void cancel(@Nullable String message, @Nullable Throwable cause) {
if (message == null && cause == null) {
cause = new CancellationException("Cancelled without a message or cause");
logger.warn("Cancelling without a message or cause is suboptimal", cause);
}
if (cancelCalled) {
return;
}
cancelCalled = true;
Status status = Status.CANCELLED;
if (message != null) {
status = status.withDescription(message);
}
if (cause != null) {
status = status.withCause(cause);
}
responseReader.cancel();
req.close(status.asException());
if (listener != null) {
try (SafeCloseable ignored = RequestContext.push(ctx)) {
listener.onClose(status, EMPTY_METADATA);
}
notifyExecutor();
}
}
@Override
public void halfClose() {
req.close();
}
@Override
public void sendMessage(I message) {
try {
ByteBuf serialized = marshaller.serializeRequest(message);
boolean success = false;
final HttpData frame;
try {
frame = messageFramer.writePayload(serialized);
success = true;
} finally {
if (!success) {
serialized.release();
}
}
req.write(frame);
} catch (Throwable t) {
cancel(null, t);
}
}
@Override
public void setMessageCompression(boolean enabled) {
checkState(req != null, "Not started");
messageFramer.setMessageCompression(enabled);
}
@Override
public void messageRead(ByteBufOrStream message) {
try {
O msg = marshaller.deserializeResponse(message);
try (SafeCloseable ignored = RequestContext.push(ctx)) {
listener.onMessage(msg);
}
} catch (Throwable t) {
req.close(Status.fromThrowable(t).asException());
throw (t instanceof RuntimeException) ? (RuntimeException) t : new RuntimeException(t);
}
}
@Override
public void endOfStream() {
// Ignore - the client call is terminated by headers, not data.
}
@Override
public void transportReportStatus(Status status) {
responseReader.cancel();
try (SafeCloseable ignored = RequestContext.push(ctx)) {
listener.onClose(status, EMPTY_METADATA);
}
notifyExecutor();
}
private void prepareHeaders(HttpHeaders headers, Compressor compressor) {
if (compressor != Identity.NONE) {
headers.set(GrpcHeaderNames.GRPC_ENCODING, compressor.getMessageEncoding());
}
String advertisedEncodings = String.join(",", decompressorRegistry.getAdvertisedMessageEncodings());
if (!advertisedEncodings.isEmpty()) {
headers.add(GrpcHeaderNames.GRPC_ACCEPT_ENCODING, advertisedEncodings);
}
headers.add(GrpcHeaderNames.GRPC_TIMEOUT,
TimeoutHeaderUtil.toHeaderValue(
TimeUnit.MILLISECONDS.toNanos(ctx.responseTimeoutMillis())));
}
/**
* Armeria does not support {@link CallOptions} set by the user, however GRPC stubs set an {@link Executor}
* within blocking stubs which is used to notify the stub when processing is finished. It's unclear why
* the stubs use a loop and {@link java.util.concurrent.Future#isDone()} instead of just blocking on
* {@link java.util.concurrent.Future#get}, but we make sure to run the {@link Executor} so the stub can
* be notified of completion.
*/
private void notifyExecutor() {
if (executor != null) {
executor.execute(NO_OP);
}
}
}