35
35
import static com .google .common .base .Preconditions .checkNotNull ;
36
36
import static com .google .common .base .Preconditions .checkState ;
37
37
import static com .google .common .util .concurrent .MoreExecutors .directExecutor ;
38
+ import static io .grpc .Contexts .statusFromCancelled ;
39
+ import static io .grpc .Status .DEADLINE_EXCEEDED ;
38
40
import static io .grpc .internal .GrpcUtil .ACCEPT_ENCODING_JOINER ;
39
41
import static io .grpc .internal .GrpcUtil .MESSAGE_ACCEPT_ENCODING_KEY ;
40
42
import static io .grpc .internal .GrpcUtil .MESSAGE_ENCODING_KEY ;
41
43
import static io .grpc .internal .GrpcUtil .TIMEOUT_KEY ;
42
44
import static io .grpc .internal .GrpcUtil .USER_AGENT_KEY ;
43
- import static java .util . concurrent . TimeUnit . NANOSECONDS ;
45
+ import static java .lang . Math . max ;
44
46
45
47
import com .google .common .annotations .VisibleForTesting ;
46
48
import com .google .common .base .Preconditions ;
62
64
import java .io .InputStream ;
63
65
import java .util .concurrent .Executor ;
64
66
import java .util .concurrent .ScheduledExecutorService ;
65
- import java .util .concurrent .ScheduledFuture ;
67
+ import java .util .concurrent .TimeUnit ;
68
+ import java .util .logging .Level ;
69
+ import java .util .logging .Logger ;
66
70
67
71
import javax .annotation .Nullable ;
68
72
71
75
*/
72
76
final class ClientCallImpl <ReqT , RespT > extends ClientCall <ReqT , RespT >
73
77
implements Context .CancellationListener {
78
+
79
+ private static final Logger log = Logger .getLogger (ClientCallImpl .class .getName ());
80
+
74
81
private final MethodDescriptor <ReqT , RespT > method ;
75
82
private final Executor callExecutor ;
76
- private final Context context ;
83
+ private final Context parentContext ;
84
+ private volatile Context context ;
77
85
private final boolean unaryRequest ;
78
86
private final CallOptions callOptions ;
79
87
private ClientStream stream ;
80
- private volatile ScheduledFuture <?> deadlineCancellationFuture ;
81
- private volatile boolean deadlineCancellationFutureShouldBeCancelled ;
88
+ private volatile boolean contextListenerShouldBeRemoved ;
82
89
private boolean cancelCalled ;
83
90
private boolean halfCloseCalled ;
84
91
private final ClientTransportProvider clientTransportProvider ;
85
92
private String userAgent ;
86
93
private ScheduledExecutorService deadlineCancellationExecutor ;
87
- private Compressor compressor ;
88
94
private DecompressorRegistry decompressorRegistry = DecompressorRegistry .getDefaultInstance ();
89
95
private CompressorRegistry compressorRegistry = CompressorRegistry .getDefaultInstance ();
90
96
@@ -99,7 +105,7 @@ final class ClientCallImpl<ReqT, RespT> extends ClientCall<ReqT, RespT>
99
105
? new SerializeReentrantCallsDirectExecutor ()
100
106
: new SerializingExecutor (executor );
101
107
// Propagate the context from the thread which initiated the call to all callbacks.
102
- this .context = Context .current ();
108
+ this .parentContext = Context .current ();
103
109
this .unaryRequest = method .getType () == MethodType .UNARY
104
110
|| method .getType () == MethodType .SERVER_STREAMING ;
105
111
this .callOptions = callOptions ;
@@ -109,7 +115,7 @@ final class ClientCallImpl<ReqT, RespT> extends ClientCall<ReqT, RespT>
109
115
110
116
@ Override
111
117
public void cancelled (Context context ) {
112
- stream .cancel (Status . CANCELLED . withCause (context . cancellationCause () ));
118
+ stream .cancel (statusFromCancelled (context ));
113
119
}
114
120
115
121
/**
@@ -165,19 +171,28 @@ public void start(final Listener<RespT> observer, Metadata headers) {
165
171
checkNotNull (observer , "observer" );
166
172
checkNotNull (headers , "headers" );
167
173
174
+ // Create the context
175
+ final Deadline effectiveDeadline = min (callOptions .getDeadline (), parentContext .getDeadline ());
176
+ if (effectiveDeadline != parentContext .getDeadline ()) {
177
+ context = parentContext .withDeadline (effectiveDeadline , deadlineCancellationExecutor );
178
+ } else {
179
+ context = parentContext .withCancellation ();
180
+ }
181
+
168
182
if (context .isCancelled ()) {
169
183
// Context is already cancelled so no need to create a real stream, just notify the observer
170
184
// of cancellation via callback on the executor
171
185
stream = NoopClientStream .INSTANCE ;
172
186
callExecutor .execute (new ContextRunnable (context ) {
173
187
@ Override
174
188
public void runInContext () {
175
- observer .onClose (Status . CANCELLED . withCause (context . cancellationCause () ), new Metadata ());
189
+ observer .onClose (statusFromCancelled (context ), new Metadata ());
176
190
}
177
191
});
178
192
return ;
179
193
}
180
194
final String compressorName = callOptions .getCompressor ();
195
+ Compressor compressor = null ;
181
196
if (compressorName != null ) {
182
197
compressor = compressorRegistry .lookupCompressor (compressorName );
183
198
if (compressor == null ) {
@@ -199,11 +214,14 @@ public void runInContext() {
199
214
200
215
prepareHeaders (headers , callOptions , userAgent , decompressorRegistry , compressor );
201
216
202
- if (updateTimeoutHeader (callOptions .getDeadline (), headers )) {
217
+ final boolean deadlineExceeded = effectiveDeadline != null && effectiveDeadline .isExpired ();
218
+ if (!deadlineExceeded ) {
219
+ updateTimeoutHeaders (effectiveDeadline , callOptions .getDeadline (),
220
+ parentContext .getDeadline (), headers );
203
221
ClientTransport transport = clientTransportProvider .get (callOptions );
204
222
stream = transport .newStream (method , headers );
205
223
} else {
206
- stream = new FailingClientStream (Status . DEADLINE_EXCEEDED );
224
+ stream = new FailingClientStream (DEADLINE_EXCEEDED );
207
225
}
208
226
209
227
if (callOptions .getAuthority () != null ) {
@@ -215,45 +233,66 @@ public void runInContext() {
215
233
if (compressor != Codec .Identity .NONE ) {
216
234
stream .setMessageCompression (true );
217
235
}
236
+
218
237
// Delay any sources of cancellation after start(), because most of the transports are broken if
219
238
// they receive cancel before start. Issue #1343 has more details
220
239
221
- // Start the deadline timer after stream creation because it will close the stream
222
- if (callOptions .getDeadline () != null ) {
223
- long timeoutNanos = callOptions .getDeadline ().timeRemaining (NANOSECONDS );
224
- deadlineCancellationFuture = startDeadlineTimer (timeoutNanos );
225
- if (deadlineCancellationFutureShouldBeCancelled ) {
226
- // Race detected! ClientStreamListener.closed may have been called before
227
- // deadlineCancellationFuture was set, thereby preventing the future from being cancelled.
228
- // Go ahead and cancel again, just to be sure it was cancelled.
229
- deadlineCancellationFuture .cancel (false );
230
- }
231
- }
232
240
// Propagate later Context cancellation to the remote side.
233
- this .context .addListener (this , directExecutor ());
241
+ context .addListener (this , directExecutor ());
242
+ if (contextListenerShouldBeRemoved ) {
243
+ // Race detected! ClientStreamListener.closed may have been called before
244
+ // deadlineCancellationFuture was set, thereby preventing the future from being cancelled.
245
+ // Go ahead and cancel again, just to be sure it was cancelled.
246
+ context .removeListener (this );
247
+ }
234
248
}
235
249
236
250
/**
237
251
* Based on the deadline, calculate and set the timeout to the given headers.
238
- *
239
- * @return {@code false} if deadline already exceeded
240
252
*/
241
- static boolean updateTimeoutHeader (@ Nullable Deadline deadline , Metadata headers ) {
242
- // Fill out timeout on the headers
243
- // TODO(carl-mastrangelo): Find out if this should always remove the timeout,
244
- // even when returning false.
253
+ private static void updateTimeoutHeaders (@ Nullable Deadline effectiveDeadline ,
254
+ @ Nullable Deadline callDeadline , @ Nullable Deadline outerCallDeadline , Metadata headers ) {
245
255
headers .removeAll (TIMEOUT_KEY );
246
256
247
- if (deadline != null ) {
248
- // Convert the deadline to timeout. Timeout is more favorable than deadline on the wire
249
- // because timeout tolerates the clock difference between machines.
250
- long timeoutNanos = deadline .timeRemaining (NANOSECONDS );
251
- if (timeoutNanos <= 0 ) {
252
- return false ;
253
- }
254
- headers .put (TIMEOUT_KEY , timeoutNanos );
257
+ if (effectiveDeadline == null ) {
258
+ return ;
255
259
}
256
- return true ;
260
+
261
+ long effectiveTimeout = max (0 , effectiveDeadline .timeRemaining (TimeUnit .NANOSECONDS ));
262
+ headers .put (TIMEOUT_KEY , effectiveTimeout );
263
+
264
+ logIfContextNarrowedTimeout (effectiveTimeout , effectiveDeadline , outerCallDeadline ,
265
+ callDeadline );
266
+ }
267
+
268
+ private static void logIfContextNarrowedTimeout (long effectiveTimeout ,
269
+ Deadline effectiveDeadline , @ Nullable Deadline outerCallDeadline ,
270
+ @ Nullable Deadline callDeadline ) {
271
+ if (!log .isLoggable (Level .INFO ) || outerCallDeadline != effectiveDeadline ) {
272
+ return ;
273
+ }
274
+
275
+ StringBuilder builder = new StringBuilder ();
276
+ builder .append (String .format ("Call timeout set to '%d' ns, due to context deadline." ,
277
+ effectiveTimeout ));
278
+ if (callDeadline == null ) {
279
+ builder .append (" Explicit call timeout was not set." );
280
+ } else {
281
+ long callTimeout = callDeadline .timeRemaining (TimeUnit .NANOSECONDS );
282
+ builder .append (String .format (" Explicit call timeout was '%d' ns." , callTimeout ));
283
+ }
284
+
285
+ log .info (builder .toString ());
286
+ }
287
+
288
+ private static Deadline min (@ Nullable Deadline deadline0 , @ Nullable Deadline deadline1 ) {
289
+ if (deadline0 == null ) {
290
+ return deadline1 ;
291
+ }
292
+ if (deadline1 == null ) {
293
+ return deadline0 ;
294
+ }
295
+ return deadline0 .minimum (deadline1 );
257
296
}
258
297
259
298
@ Override
@@ -276,7 +315,9 @@ public void cancel() {
276
315
stream .cancel (Status .CANCELLED );
277
316
}
278
317
} finally {
279
- context .removeListener (ClientCallImpl .this );
318
+ if (context != null ) {
319
+ context .removeListener (ClientCallImpl .this );
320
+ }
280
321
}
281
322
}
282
323
@@ -321,15 +362,6 @@ public boolean isReady() {
321
362
return stream .isReady ();
322
363
}
323
364
324
- private ScheduledFuture <?> startDeadlineTimer (long timeoutNanos ) {
325
- return deadlineCancellationExecutor .schedule (new Runnable () {
326
- @ Override
327
- public void run () {
328
- stream .cancel (Status .DEADLINE_EXCEEDED );
329
- }
330
- }, timeoutNanos , NANOSECONDS );
331
- }
332
-
333
365
private class ClientStreamListenerImpl implements ClientStreamListener {
334
366
private final Listener <RespT > observer ;
335
367
private boolean closed ;
@@ -394,13 +426,13 @@ public final void runInContext() {
394
426
395
427
@ Override
396
428
public void closed (Status status , Metadata trailers ) {
397
- if (status .getCode () == Status .Code .CANCELLED && callOptions .getDeadline () != null ) {
429
+ Deadline deadline = context .getDeadline ();
430
+ if (status .getCode () == Status .Code .CANCELLED && deadline != null ) {
398
431
// When the server's deadline expires, it can only reset the stream with CANCEL and no
399
432
// description. Since our timer may be delayed in firing, we double-check the deadline and
400
433
// turn the failure into the likely more helpful DEADLINE_EXCEEDED status.
401
- long timeoutNanos = callOptions .getDeadline ().timeRemaining (NANOSECONDS );
402
- if (timeoutNanos <= 0 ) {
403
- status = Status .DEADLINE_EXCEEDED ;
434
+ if (deadline .isExpired ()) {
435
+ status = DEADLINE_EXCEEDED ;
404
436
// Replace trailers to prevent mixing sources of status and trailers.
405
437
trailers = new Metadata ();
406
438
}
@@ -412,12 +444,7 @@ public void closed(Status status, Metadata trailers) {
412
444
public final void runInContext () {
413
445
try {
414
446
closed = true ;
415
- deadlineCancellationFutureShouldBeCancelled = true ;
416
- // manually optimize the volatile read
417
- ScheduledFuture <?> future = deadlineCancellationFuture ;
418
- if (future != null ) {
419
- future .cancel (false );
420
- }
447
+ contextListenerShouldBeRemoved = true ;
421
448
observer .onClose (savedStatus , savedTrailers );
422
449
} finally {
423
450
context .removeListener (ClientCallImpl .this );
0 commit comments