mirror of https://github.com/grpc/grpc-java.git
core: Add DelayedClientCall
Adding `DelayedClientCall` in preparation of implementing `ConfigSelector` in core. `DelayedClientCall` is implemented exactly in the same way as `DelayedStream`. Only added logic to monitor initial DEADLINE. Note that `ClientCall.cancel()` is not thread-safe and will cause exceptions if trying to start call after it, which is different from in the stream where cancel() is thread-safe and wouldn't trigger any checkState()s. The initial DEADLINE monitor should not call `ClientCall.cancel()` directly.
This commit is contained in:
parent
c116d6846b
commit
afcce8d3c0
|
|
@ -0,0 +1,527 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 The gRPC Authors
|
||||||
|
*
|
||||||
|
* Licensed 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 io.grpc.internal;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
import static com.google.common.base.Preconditions.checkState;
|
||||||
|
import static java.util.concurrent.TimeUnit.NANOSECONDS;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import com.google.common.base.MoreObjects;
|
||||||
|
import io.grpc.Attributes;
|
||||||
|
import io.grpc.ClientCall;
|
||||||
|
import io.grpc.Context;
|
||||||
|
import io.grpc.Deadline;
|
||||||
|
import io.grpc.Metadata;
|
||||||
|
import io.grpc.Status;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.ScheduledFuture;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import javax.annotation.concurrent.GuardedBy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A call that queues requests before the transport is available, and delegates to a real call
|
||||||
|
* implementation when the transport is available.
|
||||||
|
*
|
||||||
|
* <p>{@code ClientCall} itself doesn't require thread-safety. However, the state of {@code
|
||||||
|
* DelayedCall} may be internally altered by different threads, thus internal synchronization is
|
||||||
|
* necessary.
|
||||||
|
*/
|
||||||
|
final class DelayedClientCall<ReqT, RespT> extends ClientCall<ReqT, RespT> {
|
||||||
|
private static final Logger logger = Logger.getLogger(DelayedClientCall.class.getName());
|
||||||
|
/**
|
||||||
|
* A timer to monitor the initial deadline. The timer must be cancelled on transition to the real
|
||||||
|
* call.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
private final ScheduledFuture<?> initialDeadlineMonitor;
|
||||||
|
private final Executor callExecutor;
|
||||||
|
private final Context context;
|
||||||
|
/** {@code true} once realCall is valid and all pending calls have been drained. */
|
||||||
|
private volatile boolean passThrough;
|
||||||
|
/**
|
||||||
|
* Non-{@code null} iff start has been called. Used to assert methods are called in appropriate
|
||||||
|
* order, but also used if an error occurs before {@code realCall} is set.
|
||||||
|
*/
|
||||||
|
private Listener<RespT> listener;
|
||||||
|
// Must hold {@code this} lock when setting.
|
||||||
|
private ClientCall<ReqT, RespT> realCall;
|
||||||
|
@GuardedBy("this")
|
||||||
|
private Status error;
|
||||||
|
@GuardedBy("this")
|
||||||
|
private List<Runnable> pendingRunnables = new ArrayList<>();
|
||||||
|
@GuardedBy("this")
|
||||||
|
private DelayedListener<RespT> delayedListener;
|
||||||
|
|
||||||
|
DelayedClientCall(
|
||||||
|
Executor callExecutor, ScheduledExecutorService scheduler, @Nullable Deadline deadline) {
|
||||||
|
this.callExecutor = checkNotNull(callExecutor, "callExecutor");
|
||||||
|
checkNotNull(scheduler, "scheduler");
|
||||||
|
context = Context.current();
|
||||||
|
initialDeadlineMonitor = scheduleDeadlineIfNeeded(scheduler, deadline);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private ScheduledFuture<?> scheduleDeadlineIfNeeded(
|
||||||
|
ScheduledExecutorService scheduler, @Nullable Deadline deadline) {
|
||||||
|
Deadline contextDeadline = context.getDeadline();
|
||||||
|
if (deadline == null && contextDeadline == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
long remainingNanos = Long.MAX_VALUE;
|
||||||
|
if (deadline != null) {
|
||||||
|
remainingNanos = Math.min(remainingNanos, deadline.timeRemaining(NANOSECONDS));
|
||||||
|
}
|
||||||
|
if (contextDeadline != null && contextDeadline.timeRemaining(NANOSECONDS) < remainingNanos) {
|
||||||
|
remainingNanos = contextDeadline.timeRemaining(NANOSECONDS);
|
||||||
|
if (logger.isLoggable(Level.FINE)) {
|
||||||
|
StringBuilder builder =
|
||||||
|
new StringBuilder(
|
||||||
|
String.format(
|
||||||
|
"Call timeout set to '%d' ns, due to context deadline.", remainingNanos));
|
||||||
|
if (deadline == null) {
|
||||||
|
builder.append(" Explicit call timeout was not set.");
|
||||||
|
} else {
|
||||||
|
long callTimeout = deadline.timeRemaining(TimeUnit.NANOSECONDS);
|
||||||
|
builder.append(String.format(" Explicit call timeout was '%d' ns.", callTimeout));
|
||||||
|
}
|
||||||
|
logger.fine(builder.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
long seconds = Math.abs(remainingNanos) / TimeUnit.SECONDS.toNanos(1);
|
||||||
|
long nanos = Math.abs(remainingNanos) % TimeUnit.SECONDS.toNanos(1);
|
||||||
|
final StringBuilder buf = new StringBuilder();
|
||||||
|
if (remainingNanos < 0) {
|
||||||
|
buf.append("ClientCall started after deadline exceeded. Deadline exceeded after -");
|
||||||
|
} else {
|
||||||
|
buf.append("Deadline exceeded after ");
|
||||||
|
}
|
||||||
|
buf.append(seconds);
|
||||||
|
buf.append(String.format(".%09d", nanos));
|
||||||
|
buf.append("s. ");
|
||||||
|
/** Cancels the call if deadline exceeded prior to the real call being set. */
|
||||||
|
class DeadlineExceededRunnable implements Runnable {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
cancel(
|
||||||
|
Status.DEADLINE_EXCEEDED.withDescription(buf.toString()),
|
||||||
|
// We should not cancel the call if the realCall is set because there could be a
|
||||||
|
// race between cancel() and realCall.start(). The realCall will handle deadline by
|
||||||
|
// itself.
|
||||||
|
/* onlyCancelPendingCall= */ true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return scheduler.schedule(new DeadlineExceededRunnable(), remainingNanos, NANOSECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transfers all pending and future requests and mutations to the given call.
|
||||||
|
*
|
||||||
|
* <p>No-op if either this method or {@link #cancel} have already been called.
|
||||||
|
*/
|
||||||
|
// When this method returns, passThrough is guaranteed to be true
|
||||||
|
final void setCall(ClientCall<ReqT, RespT> call) {
|
||||||
|
synchronized (this) {
|
||||||
|
// If realCall != null, then either setCall() or cancel() has been called.
|
||||||
|
if (realCall != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setRealCall(checkNotNull(call, "call"));
|
||||||
|
}
|
||||||
|
drainPendingCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start(Listener<RespT> listener, final Metadata headers) {
|
||||||
|
checkState(this.listener == null, "already started");
|
||||||
|
Status savedError;
|
||||||
|
boolean savedPassThrough;
|
||||||
|
synchronized (this) {
|
||||||
|
this.listener = checkNotNull(listener, "listener");
|
||||||
|
// If error != null, then cancel() has been called and was unable to close the listener
|
||||||
|
savedError = error;
|
||||||
|
savedPassThrough = passThrough;
|
||||||
|
if (!savedPassThrough) {
|
||||||
|
listener = delayedListener = new DelayedListener<>(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (savedError != null) {
|
||||||
|
callExecutor.execute(new CloseListenerRunnable(listener, savedError));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (savedPassThrough) {
|
||||||
|
realCall.start(listener, headers);
|
||||||
|
} else {
|
||||||
|
final Listener<RespT> finalListener = listener;
|
||||||
|
delayOrExecute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
realCall.start(finalListener, headers);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When this method returns, passThrough is guaranteed to be true
|
||||||
|
@Override
|
||||||
|
public void cancel(@Nullable final String message, @Nullable final Throwable cause) {
|
||||||
|
Status status = Status.CANCELLED;
|
||||||
|
if (message != null) {
|
||||||
|
status = status.withDescription(message);
|
||||||
|
} else {
|
||||||
|
status = status.withDescription("Call cancelled without message");
|
||||||
|
}
|
||||||
|
if (cause != null) {
|
||||||
|
status = status.withCause(cause);
|
||||||
|
}
|
||||||
|
cancel(status, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels the call unless {@code realCall} is set and {@code onlyCancelPendingCall} is true.
|
||||||
|
*/
|
||||||
|
private void cancel(final Status status, boolean onlyCancelPendingCall) {
|
||||||
|
boolean delegateToRealCall = true;
|
||||||
|
Listener<RespT> listenerToClose = null;
|
||||||
|
synchronized (this) {
|
||||||
|
// If realCall != null, then either setCall() or cancel() has been called
|
||||||
|
if (realCall == null) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
ClientCall<ReqT, RespT> noopCall = (ClientCall<ReqT, RespT>) NOOP_CALL;
|
||||||
|
setRealCall(noopCall);
|
||||||
|
delegateToRealCall = false;
|
||||||
|
// If listener == null, then start() will later call listener with 'error'
|
||||||
|
listenerToClose = listener;
|
||||||
|
error = status;
|
||||||
|
} else if (onlyCancelPendingCall) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (delegateToRealCall) {
|
||||||
|
delayOrExecute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
realCall.cancel(status.getDescription(), status.getCause());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (listenerToClose != null) {
|
||||||
|
callExecutor.execute(new CloseListenerRunnable(listenerToClose, status));
|
||||||
|
}
|
||||||
|
drainPendingCalls();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void delayOrExecute(Runnable runnable) {
|
||||||
|
synchronized (this) {
|
||||||
|
if (!passThrough) {
|
||||||
|
pendingRunnables.add(runnable);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
runnable.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called to transition {@code passThrough} to {@code true}. This method is not safe to be called
|
||||||
|
* multiple times; the caller must ensure it will only be called once, ever. {@code this} lock
|
||||||
|
* should not be held when calling this method.
|
||||||
|
*/
|
||||||
|
private void drainPendingCalls() {
|
||||||
|
assert realCall != null;
|
||||||
|
assert !passThrough;
|
||||||
|
List<Runnable> toRun = new ArrayList<>();
|
||||||
|
DelayedListener<RespT> delayedListener ;
|
||||||
|
while (true) {
|
||||||
|
synchronized (this) {
|
||||||
|
if (pendingRunnables.isEmpty()) {
|
||||||
|
pendingRunnables = null;
|
||||||
|
passThrough = true;
|
||||||
|
delayedListener = this.delayedListener;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Since there were pendingCalls, we need to process them. To maintain ordering we can't set
|
||||||
|
// passThrough=true until we run all pendingCalls, but new Runnables may be added after we
|
||||||
|
// drop the lock. So we will have to re-check pendingCalls.
|
||||||
|
List<Runnable> tmp = toRun;
|
||||||
|
toRun = pendingRunnables;
|
||||||
|
pendingRunnables = tmp;
|
||||||
|
}
|
||||||
|
for (Runnable runnable : toRun) {
|
||||||
|
// Must not call transport while lock is held to prevent deadlocks.
|
||||||
|
// TODO(ejona): exception handling
|
||||||
|
runnable.run();
|
||||||
|
}
|
||||||
|
toRun.clear();
|
||||||
|
}
|
||||||
|
if (delayedListener != null) {
|
||||||
|
final DelayedListener<RespT> listener = delayedListener;
|
||||||
|
class DrainListenerRunnable extends ContextRunnable {
|
||||||
|
DrainListenerRunnable() {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void runInContext() {
|
||||||
|
listener.drainPendingCallbacks();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callExecutor.execute(new DrainListenerRunnable());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GuardedBy("this")
|
||||||
|
private void setRealCall(ClientCall<ReqT, RespT> realCall) {
|
||||||
|
checkState(this.realCall == null, "realCall already set to %s", this.realCall);
|
||||||
|
if (initialDeadlineMonitor != null) {
|
||||||
|
initialDeadlineMonitor.cancel(false);
|
||||||
|
}
|
||||||
|
this.realCall = realCall;
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
ClientCall<ReqT, RespT> getRealCall() {
|
||||||
|
return realCall;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendMessage(final ReqT message) {
|
||||||
|
if (passThrough) {
|
||||||
|
realCall.sendMessage(message);
|
||||||
|
} else {
|
||||||
|
delayOrExecute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
realCall.sendMessage(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setMessageCompression(final boolean enable) {
|
||||||
|
if (passThrough) {
|
||||||
|
realCall.setMessageCompression(enable);
|
||||||
|
} else {
|
||||||
|
delayOrExecute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
realCall.setMessageCompression(enable);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void request(final int numMessages) {
|
||||||
|
if (passThrough) {
|
||||||
|
realCall.request(numMessages);
|
||||||
|
} else {
|
||||||
|
delayOrExecute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
realCall.request(numMessages);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void halfClose() {
|
||||||
|
delayOrExecute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
realCall.halfClose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isReady() {
|
||||||
|
if (passThrough) {
|
||||||
|
return realCall.isReady();
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Attributes getAttributes() {
|
||||||
|
ClientCall<ReqT, RespT> savedRealCall;
|
||||||
|
synchronized (this) {
|
||||||
|
savedRealCall = realCall;
|
||||||
|
}
|
||||||
|
if (savedRealCall != null) {
|
||||||
|
return savedRealCall.getAttributes();
|
||||||
|
} else {
|
||||||
|
return Attributes.EMPTY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return MoreObjects.toStringHelper(this)
|
||||||
|
.add("realCall", realCall)
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class CloseListenerRunnable extends ContextRunnable {
|
||||||
|
final Listener<RespT> listener;
|
||||||
|
final Status status;
|
||||||
|
|
||||||
|
CloseListenerRunnable(Listener<RespT> listener, Status status) {
|
||||||
|
super(context);
|
||||||
|
this.listener = listener;
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void runInContext() {
|
||||||
|
listener.onClose(status, new Metadata());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class DelayedListener<RespT> extends Listener<RespT> {
|
||||||
|
private final Listener<RespT> realListener;
|
||||||
|
private volatile boolean passThrough;
|
||||||
|
@GuardedBy("this")
|
||||||
|
private List<Runnable> pendingCallbacks = new ArrayList<>();
|
||||||
|
|
||||||
|
public DelayedListener(Listener<RespT> listener) {
|
||||||
|
this.realListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void delayOrExecute(Runnable runnable) {
|
||||||
|
synchronized (this) {
|
||||||
|
if (!passThrough) {
|
||||||
|
pendingCallbacks.add(runnable);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
runnable.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onHeaders(final Metadata headers) {
|
||||||
|
if (passThrough) {
|
||||||
|
realListener.onHeaders(headers);
|
||||||
|
} else {
|
||||||
|
delayOrExecute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
realListener.onHeaders(headers);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(final RespT message) {
|
||||||
|
if (passThrough) {
|
||||||
|
realListener.onMessage(message);
|
||||||
|
} else {
|
||||||
|
delayOrExecute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
realListener.onMessage(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClose(final Status status, final Metadata trailers) {
|
||||||
|
delayOrExecute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
realListener.onClose(status, trailers);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReady() {
|
||||||
|
if (passThrough) {
|
||||||
|
realListener.onReady();
|
||||||
|
} else {
|
||||||
|
delayOrExecute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
realListener.onReady();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void drainPendingCallbacks() {
|
||||||
|
assert !passThrough;
|
||||||
|
List<Runnable> toRun = new ArrayList<>();
|
||||||
|
while (true) {
|
||||||
|
synchronized (this) {
|
||||||
|
if (pendingCallbacks.isEmpty()) {
|
||||||
|
pendingCallbacks = null;
|
||||||
|
passThrough = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Since there were pendingCallbacks, we need to process them. To maintain ordering we
|
||||||
|
// can't set passThrough=true until we run all pendingCallbacks, but new Runnables may be
|
||||||
|
// added after we drop the lock. So we will have to re-check pendingCallbacks.
|
||||||
|
List<Runnable> tmp = toRun;
|
||||||
|
toRun = pendingCallbacks;
|
||||||
|
pendingCallbacks = tmp;
|
||||||
|
}
|
||||||
|
for (Runnable runnable : toRun) {
|
||||||
|
// Avoid calling listener while lock is held to prevent deadlocks.
|
||||||
|
// TODO(ejona): exception handling
|
||||||
|
runnable.run();
|
||||||
|
}
|
||||||
|
toRun.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final ClientCall<Object, Object> NOOP_CALL = new ClientCall<Object, Object>() {
|
||||||
|
@Override
|
||||||
|
public void start(Listener<Object> responseListener, Metadata headers) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void request(int numMessages) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cancel(String message, Throwable cause) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void halfClose() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendMessage(Object message) {}
|
||||||
|
|
||||||
|
// Always returns {@code false}, since this is only used when the startup of the call fails.
|
||||||
|
@Override
|
||||||
|
public boolean isReady() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 The gRPC Authors
|
||||||
|
*
|
||||||
|
* Licensed 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 io.grpc.internal;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
|
import io.grpc.ClientCall;
|
||||||
|
import io.grpc.ClientCall.Listener;
|
||||||
|
import io.grpc.Deadline;
|
||||||
|
import io.grpc.ForwardingTestUtil;
|
||||||
|
import io.grpc.Metadata;
|
||||||
|
import io.grpc.Status;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.lang.reflect.Modifier;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.JUnit4;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Captor;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.MockitoJUnit;
|
||||||
|
import org.mockito.junit.MockitoRule;
|
||||||
|
|
||||||
|
/** Tests for {@link DelayedClientCall}. */
|
||||||
|
@RunWith(JUnit4.class)
|
||||||
|
public class DelayedClientCallTest {
|
||||||
|
@Rule
|
||||||
|
public final MockitoRule mockitoRule = MockitoJUnit.rule();
|
||||||
|
@Mock
|
||||||
|
private ClientCall<String, Integer> mockRealCall;
|
||||||
|
@Mock
|
||||||
|
private ClientCall.Listener<Integer> listener;
|
||||||
|
@Captor
|
||||||
|
ArgumentCaptor<Status> statusCaptor;
|
||||||
|
|
||||||
|
private final FakeClock fakeClock = new FakeClock();
|
||||||
|
private final Executor callExecutor = MoreExecutors.directExecutor();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void allMethodsForwarded() throws Exception {
|
||||||
|
DelayedClientCall<String, Integer> delayedClientCall =
|
||||||
|
new DelayedClientCall<>(callExecutor, fakeClock.getScheduledExecutorService(), null);
|
||||||
|
delayedClientCall.setCall(mockRealCall);
|
||||||
|
ForwardingTestUtil.testMethodsForwarded(
|
||||||
|
ClientCall.class,
|
||||||
|
mockRealCall,
|
||||||
|
delayedClientCall,
|
||||||
|
Arrays.asList(ClientCall.class.getMethod("toString")),
|
||||||
|
new ForwardingTestUtil.ArgumentProvider() {
|
||||||
|
@Override
|
||||||
|
public Object get(Method method, int argPos, Class<?> clazz) {
|
||||||
|
if (!Modifier.isFinal(clazz.getModifiers())) {
|
||||||
|
return mock(clazz);
|
||||||
|
}
|
||||||
|
if (clazz.equals(String.class)) {
|
||||||
|
return "message";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Coverage for deadline exceeded before call started is enforced by
|
||||||
|
// AbstractInteropTest.deadlineInPast().
|
||||||
|
@Test
|
||||||
|
public void deadlineExceededWhileCallIsStartedButStillPending() {
|
||||||
|
DelayedClientCall<String, Integer> delayedClientCall = new DelayedClientCall<>(
|
||||||
|
callExecutor, fakeClock.getScheduledExecutorService(), Deadline.after(10, SECONDS));
|
||||||
|
|
||||||
|
delayedClientCall.start(listener, new Metadata());
|
||||||
|
fakeClock.forwardTime(10, SECONDS);
|
||||||
|
verify(listener).onClose(statusCaptor.capture(), any(Metadata.class));
|
||||||
|
assertThat(statusCaptor.getValue().getCode()).isEqualTo(Status.Code.DEADLINE_EXCEEDED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void listenerEventsPropagated() {
|
||||||
|
DelayedClientCall<String, Integer> delayedClientCall = new DelayedClientCall<>(
|
||||||
|
callExecutor, fakeClock.getScheduledExecutorService(), Deadline.after(10, SECONDS));
|
||||||
|
delayedClientCall.start(listener, new Metadata());
|
||||||
|
delayedClientCall.setCall(mockRealCall);
|
||||||
|
ArgumentCaptor<Listener<Integer>> listenerCaptor = ArgumentCaptor.forClass(null);
|
||||||
|
verify(mockRealCall).start(listenerCaptor.capture(), any(Metadata.class));
|
||||||
|
Listener<Integer> realCallListener = listenerCaptor.getValue();
|
||||||
|
Metadata metadata = new Metadata();
|
||||||
|
metadata.put(Metadata.Key.of("key", Metadata.ASCII_STRING_MARSHALLER), "value");
|
||||||
|
realCallListener.onHeaders(metadata);
|
||||||
|
verify(listener).onHeaders(metadata);
|
||||||
|
realCallListener.onMessage(3);
|
||||||
|
verify(listener).onMessage(3);
|
||||||
|
realCallListener.onReady();
|
||||||
|
verify(listener).onReady();
|
||||||
|
Metadata trailer = new Metadata();
|
||||||
|
trailer.put(Metadata.Key.of("key2", Metadata.ASCII_STRING_MARSHALLER), "value2");
|
||||||
|
realCallListener.onClose(Status.DATA_LOSS, trailer);
|
||||||
|
verify(listener).onClose(statusCaptor.capture(), eq(trailer));
|
||||||
|
assertThat(statusCaptor.getValue().getCode()).isEqualTo(Status.Code.DATA_LOSS);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue