• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

grpc / grpc-java / #20525

13 Oct 2026 04:05PM UTC coverage: 89.449% (+0.03%) from 89.422%
#20525

push

github

web-flow
core: Delayed deserialization for unary/server-streaming calls (#13113)

Restore delayed deserialization and lazy decompression for unary and
server-streaming calls (originally introduced in #13004 and reverted in
#13107).

Additionally, convert ServerCall.request(1) to stream.request(2) for
unary and server-streaming calls (clientSendsOneMessage()) to prevent
transport deadlocks if multiple messages arrive on the stream.

39354 of 43996 relevant lines covered (89.45%)

0.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.9
/../core/src/main/java/io/grpc/internal/ServerCallImpl.java
1
/*
2
 * Copyright 2015 The gRPC Authors
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16

17
package io.grpc.internal;
18

19
import static com.google.common.base.Preconditions.checkArgument;
20
import static com.google.common.base.Preconditions.checkNotNull;
21
import static com.google.common.base.Preconditions.checkState;
22
import static io.grpc.internal.GrpcAttributes.ATTR_SECURITY_LEVEL;
23
import static io.grpc.internal.GrpcUtil.ACCEPT_ENCODING_SPLITTER;
24
import static io.grpc.internal.GrpcUtil.CONTENT_LENGTH_KEY;
25
import static io.grpc.internal.GrpcUtil.MESSAGE_ACCEPT_ENCODING_KEY;
26
import static io.grpc.internal.GrpcUtil.MESSAGE_ENCODING_KEY;
27

28
import com.google.common.annotations.VisibleForTesting;
29
import com.google.common.base.Throwables;
30
import com.google.common.util.concurrent.MoreExecutors;
31
import io.grpc.Attributes;
32
import io.grpc.Codec;
33
import io.grpc.Compressor;
34
import io.grpc.CompressorRegistry;
35
import io.grpc.Context;
36
import io.grpc.DecompressorRegistry;
37
import io.grpc.InternalDecompressorRegistry;
38
import io.grpc.InternalStatus;
39
import io.grpc.Metadata;
40
import io.grpc.MethodDescriptor;
41
import io.grpc.SecurityLevel;
42
import io.grpc.ServerCall;
43
import io.grpc.Status;
44
import io.grpc.StatusRuntimeException;
45
import io.perfmark.PerfMark;
46
import io.perfmark.Tag;
47
import io.perfmark.TaskCloseable;
48
import java.io.InputStream;
49
import java.util.logging.Level;
50
import java.util.logging.Logger;
51

52
final class ServerCallImpl<ReqT, RespT> extends ServerCall<ReqT, RespT> {
53

54
  private static final Logger log = Logger.getLogger(ServerCallImpl.class.getName());
1 ✔
55

56
  @VisibleForTesting
57
  static final String TOO_MANY_RESPONSES = "Too many responses";
58
  @VisibleForTesting
59
  static final String MISSING_RESPONSE = "Completed without a response";
60

61
  private final ServerStream stream;
62
  private final MethodDescriptor<ReqT, RespT> method;
63
  private final Tag tag;
64
  private final Context.CancellableContext context;
65
  private final byte[] messageAcceptEncoding;
66
  private final DecompressorRegistry decompressorRegistry;
67
  private final CompressorRegistry compressorRegistry;
68
  private CallTracer serverCallTracer;
69

70
  // state
71
  private volatile boolean cancelled;
72
  private boolean sendHeadersCalled;
73
  private boolean closeCalled;
74
  private Compressor compressor;
75
  private boolean messageSent;
76

77
  ServerCallImpl(ServerStream stream, MethodDescriptor<ReqT, RespT> method,
78
      Metadata inboundHeaders, Context.CancellableContext context,
79
      DecompressorRegistry decompressorRegistry, CompressorRegistry compressorRegistry,
80
      CallTracer serverCallTracer, Tag tag) {
1 ✔
81
    this.stream = stream;
1 ✔
82
    this.method = method;
1 ✔
83
    this.context = context;
1 ✔
84
    this.messageAcceptEncoding = inboundHeaders.get(MESSAGE_ACCEPT_ENCODING_KEY);
1 ✔
85
    this.decompressorRegistry = decompressorRegistry;
1 ✔
86
    this.compressorRegistry = compressorRegistry;
1 ✔
87
    this.serverCallTracer = serverCallTracer;
1 ✔
88
    this.serverCallTracer.reportCallStarted();
1 ✔
89
    this.tag = tag;
1 ✔
90
  }
1 ✔
91

92
  @Override
93
  public void request(int numMessages) {
94
    try (TaskCloseable ignore = PerfMark.traceTask("ServerCall.request")) {
1 ✔
95
      PerfMark.attachTag(tag);
1 ✔
96
      if (method.getType().clientSendsOneMessage() && numMessages == 1) {
1 ✔
97
        numMessages = 2;
1 ✔
98
      }
99
      stream.request(numMessages);
1 ✔
100
    }
101
  }
1 ✔
102

103
  @Override
104
  public void sendHeaders(Metadata headers) {
105
    try (TaskCloseable ignore = PerfMark.traceTask("ServerCall.sendHeaders")) {
1 ✔
106
      PerfMark.attachTag(tag);
1 ✔
107
      sendHeadersInternal(headers);
1 ✔
108
    }
109
  }
1 ✔
110

111
  private void sendHeadersInternal(Metadata headers) {
112
    checkState(!sendHeadersCalled, "sendHeaders has already been called");
1 ✔
113
    checkState(!closeCalled, "call is closed");
1 ✔
114

115
    headers.discardAll(CONTENT_LENGTH_KEY);
1 ✔
116
    headers.discardAll(MESSAGE_ENCODING_KEY);
1 ✔
117
    if (compressor == null) {
1 ✔
118
      compressor = Codec.Identity.NONE;
1 ✔
119
    } else {
120
      if (messageAcceptEncoding != null) {
1 ✔
121
        // TODO(carl-mastrangelo): remove the string allocation.
122
        if (!GrpcUtil.iterableContains(
1 ✔
123
            ACCEPT_ENCODING_SPLITTER.split(new String(messageAcceptEncoding, GrpcUtil.US_ASCII)),
1 ✔
124
            compressor.getMessageEncoding())) {
1 ✔
125
          // resort to using no compression.
126
          compressor = Codec.Identity.NONE;
1 ✔
127
        }
128
      } else {
129
        compressor = Codec.Identity.NONE;
1 ✔
130
      }
131
    }
132

133
    // Always put compressor, even if it's identity.
134
    headers.put(MESSAGE_ENCODING_KEY, compressor.getMessageEncoding());
1 ✔
135

136
    stream.setCompressor(compressor);
1 ✔
137

138
    headers.discardAll(MESSAGE_ACCEPT_ENCODING_KEY);
1 ✔
139
    byte[] advertisedEncodings =
1 ✔
140
        InternalDecompressorRegistry.getRawAdvertisedMessageEncodings(decompressorRegistry);
1 ✔
141
    if (advertisedEncodings.length != 0) {
1 ✔
142
      headers.put(MESSAGE_ACCEPT_ENCODING_KEY, advertisedEncodings);
1 ✔
143
    }
144

145
    // Don't check if sendMessage has been called, since it requires that sendHeaders was already
146
    // called.
147
    sendHeadersCalled = true;
1 ✔
148
    stream.writeHeaders(headers, !getMethodDescriptor().getType().serverSendsOneMessage());
1 ✔
149
  }
1 ✔
150

151
  @Override
152
  public void sendMessage(RespT message) {
153
    try (TaskCloseable ignore = PerfMark.traceTask("ServerCall.sendMessage")) {
1 ✔
154
      PerfMark.attachTag(tag);
1 ✔
155
      sendMessageInternal(message);
1 ✔
156
    }
157
  }
1 ✔
158

159
  private void sendMessageInternal(RespT message) {
160
    checkState(sendHeadersCalled, "sendHeaders has not been called");
1 ✔
161
    checkState(!closeCalled, "call is closed");
1 ✔
162

163
    if (method.getType().serverSendsOneMessage() && messageSent) {
1 ✔
164
      handleInternalError(Status.INTERNAL.withDescription(TOO_MANY_RESPONSES).asRuntimeException());
1 ✔
165
      return;
1 ✔
166
    }
167

168
    messageSent = true;
1 ✔
169
    try {
170
      InputStream resp = method.streamResponse(message);
1 ✔
171
      stream.writeMessage(resp);
1 ✔
172
      if (!getMethodDescriptor().getType().serverSendsOneMessage()) {
1 ✔
173
        stream.flush();
1 ✔
174
      }
175
    } catch (RuntimeException e) {
1 ✔
176
      handleInternalError(e);
1 ✔
177
    } catch (Error e) {
×
178
      close(
×
179
          Status.CANCELLED.withDescription("Server sendMessage() failed with Error"),
×
180
          new Metadata());
181
      throw e;
×
182
    }
1 ✔
183
  }
1 ✔
184

185
  @Override
186
  public void setMessageCompression(boolean enable) {
187
    stream.setMessageCompression(enable);
1 ✔
188
  }
1 ✔
189

190
  @Override
191
  public void setOnReadyThreshold(int numBytes) {
192
    stream.setOnReadyThreshold(numBytes);
1 ✔
193
  }
1 ✔
194

195
  @Override
196
  public void setCompression(String compressorName) {
197
    // Added here to give a better error message.
198
    checkState(!sendHeadersCalled, "sendHeaders has been called");
1 ✔
199

200
    compressor = compressorRegistry.lookupCompressor(compressorName);
1 ✔
201
    checkArgument(compressor != null, "Unable to find compressor by name %s", compressorName);
1 ✔
202
  }
1 ✔
203

204
  @Override
205
  public boolean isReady() {
206
    if (closeCalled) {
1 ✔
207
      return false;
1 ✔
208
    }
209
    return stream.isReady();
1 ✔
210
  }
211

212
  @Override
213
  public void close(Status status, Metadata trailers) {
214
    try (TaskCloseable ignore = PerfMark.traceTask("ServerCall.close")) {
1 ✔
215
      PerfMark.attachTag(tag);
1 ✔
216
      closeInternal(status, trailers);
1 ✔
217
    }
218
  }
1 ✔
219

220
  private void closeInternal(Status status, Metadata trailers) {
221
    checkState(!closeCalled, "call already closed");
1 ✔
222
    try {
223
      closeCalled = true;
1 ✔
224

225
      if (status.isOk() && method.getType().serverSendsOneMessage() && !messageSent) {
1 ✔
226
        handleInternalError(Status.INTERNAL.withDescription(MISSING_RESPONSE).asRuntimeException());
1 ✔
227
        return;
1 ✔
228
      }
229

230
      stream.close(status, trailers);
1 ✔
231
    } finally {
232
      serverCallTracer.reportCallEnded(status.isOk());
1 ✔
233
    }
234
  }
1 ✔
235

236
  @Override
237
  public boolean isCancelled() {
238
    return cancelled;
1 ✔
239
  }
240

241
  ServerStreamListener newServerStreamListener(ServerCall.Listener<ReqT> listener) {
242
    return new ServerStreamListenerImpl<>(this, listener, context);
1 ✔
243
  }
244

245
  @Override
246
  public Attributes getAttributes() {
247
    return stream.getAttributes();
1 ✔
248
  }
249

250
  @Override
251
  public String getAuthority() {
252
    return stream.getAuthority();
1 ✔
253
  }
254

255
  @Override
256
  public MethodDescriptor<ReqT, RespT> getMethodDescriptor() {
257
    return method;
1 ✔
258
  }
259

260
  @Override
261
  public void triggerEvent(Object event) {
262
    try (TaskCloseable ignore = PerfMark.traceTask("ServerCall.triggerEvent")) {
1 ✔
263
      PerfMark.attachTag(tag);
1 ✔
264
      stream.triggerEvent(event);
1 ✔
265
    }
266
  }
1 ✔
267

268
  @Override
269
  public SecurityLevel getSecurityLevel() {
270
    final Attributes attributes = getAttributes();
1 ✔
271
    if (attributes == null) {
1 ✔
272
      return super.getSecurityLevel();
1 ✔
273
    }
274
    final SecurityLevel securityLevel = attributes.get(ATTR_SECURITY_LEVEL);
1 ✔
275
    return securityLevel == null ? super.getSecurityLevel() : securityLevel;
1 ✔
276
  }
277

278
  /**
279
   * Close the {@link ServerStream} because an internal error occurred. Allow the application to
280
   * run until completion, but silently ignore interactions with the {@link ServerStream} from now
281
   * on.
282
   */
283
  private void handleInternalError(Throwable internalError) {
284
    log.log(Level.WARNING, "Cancelling the stream because of internal error", internalError);
1 ✔
285
    Status status = (internalError instanceof StatusRuntimeException)
1 ✔
286
        ? ((StatusRuntimeException) internalError).getStatus()
1 ✔
287
        : Status.INTERNAL.withCause(internalError)
1 ✔
288
            .withDescription("Internal error so cancelling stream.");
1 ✔
289
    stream.cancel(status);
1 ✔
290
    serverCallTracer.reportCallEnded(false); // error so always false
1 ✔
291
  }
1 ✔
292

293
  /**
294
   * All of these callbacks are assumed to called on an application thread, and the caller is
295
   * responsible for handling thrown exceptions.
296
   */
297
  @VisibleForTesting
298
  static final class ServerStreamListenerImpl<ReqT> implements ServerStreamListener {
299
    private final ServerCallImpl<ReqT, ?> call;
300
    private final ServerCall.Listener<ReqT> listener;
301
    private final Context.CancellableContext context;
302
    private InputStream delayedMessage;
303

304
    public ServerStreamListenerImpl(
305
        ServerCallImpl<ReqT, ?> call, ServerCall.Listener<ReqT> listener,
306
        Context.CancellableContext context) {
1 ✔
307
      this.call = checkNotNull(call, "call");
1 ✔
308
      this.listener = checkNotNull(listener, "listener must not be null");
1 ✔
309
      this.context = checkNotNull(context, "context");
1 ✔
310
      // Wire ourselves up so that if the context is cancelled, our flag call.cancelled also
311
      // reflects the new state. Use a DirectExecutor so that it happens in the same thread
312
      // as the caller of {@link Context#cancel}.
313
      this.context.addListener(
1 ✔
314
          new Context.CancellationListener() {
1 ✔
315
            @Override
316
            public void cancelled(Context context) {
317
              // If the context has a cancellation cause then something exceptional happened
318
              // and we should also mark the call as cancelled.
319
              if (context.cancellationCause() != null) {
1 ✔
320
                ServerStreamListenerImpl.this.call.cancelled = true;
1 ✔
321
              }
322
            }
1 ✔
323
          },
324
          MoreExecutors.directExecutor());
1 ✔
325
    }
1 ✔
326

327
    @Override
328
    public void messagesAvailable(MessageProducer producer) {
329
      try (TaskCloseable ignore = PerfMark.traceTask("ServerStreamListener.messagesAvailable")) {
1 ✔
330
        PerfMark.attachTag(call.tag);
1 ✔
331
        messagesAvailableInternal(producer);
1 ✔
332
      }
333
    }
1 ✔
334

335
    @SuppressWarnings("Finally") // The code avoids suppressing the exception thrown from try
336
    private void messagesAvailableInternal(final MessageProducer producer) {
337
      if (call.cancelled) {
1 ✔
338
        GrpcUtil.closeQuietly(producer);
1 ✔
339
        return;
1 ✔
340
      }
341

342
      InputStream message;
343
      try {
344
        while ((message = producer.next()) != null) {
1 ✔
345
          // TODO: Consider forcing this check to be done in the transport (MessageDeframer)
346
          // https://github.com/grpc/grpc-java/pull/13004/changes#r3939373996
347
          if (call.method.getType().clientSendsOneMessage()) {
1 ✔
348
            if (delayedMessage != null) {
1 ✔
349
              GrpcUtil.closeQuietly(message);
1 ✔
350
              call.stream.cancel(Status.INTERNAL.withDescription("Too many requests"));
1 ✔
351
              GrpcUtil.closeQuietly(delayedMessage);
1 ✔
352
              delayedMessage = null;
1 ✔
353
              call.cancelled = true;
1 ✔
354
              return;
1 ✔
355
            }
356
            delayedMessage = message;
1 ✔
357
          } else {
358
            try {
359
              listener.onMessage(call.method.parseRequest(message));
1 ✔
360
            } catch (Throwable t) {
1 ✔
361
              GrpcUtil.closeQuietly(message);
1 ✔
362
              throw t;
1 ✔
363
            }
1 ✔
364
            message.close();
1 ✔
365
          }
366
        }
367
      } catch (Throwable t) {
1 ✔
368
        GrpcUtil.closeQuietly(producer);
1 ✔
369
        Throwables.throwIfUnchecked(t);
×
370
        throw new RuntimeException(t);
×
371
      }
1 ✔
372
    }
1 ✔
373

374
    @Override
375
    public void halfClosed() {
376
      try (TaskCloseable ignore = PerfMark.traceTask("ServerStreamListener.halfClosed")) {
1 ✔
377
        PerfMark.attachTag(call.tag);
1 ✔
378
        if (call.cancelled) {
1 ✔
379
          return;
1 ✔
380
        }
381

382
        if (delayedMessage != null) {
1 ✔
383
          InputStream message = delayedMessage;
1 ✔
384
          delayedMessage = null;
1 ✔
385
          try {
386
            listener.onMessage(call.method.parseRequest(message));
1 ✔
387
          } catch (Throwable t) {
1 ✔
388
            GrpcUtil.closeQuietly(message);
1 ✔
389
            Throwables.throwIfUnchecked(t);
×
390
            throw new RuntimeException(t);
×
391
          }
1 ✔
392
          GrpcUtil.closeQuietly(message);
1 ✔
393
        }
394

395
        listener.onHalfClose();
1 ✔
396
      }
397
    }
1 ✔
398

399
    @Override
400
    public void closed(Status status) {
401
      try (TaskCloseable ignore = PerfMark.traceTask("ServerStreamListener.closed")) {
1 ✔
402
        PerfMark.attachTag(call.tag);
1 ✔
403
        closedInternal(status);
1 ✔
404
      }
405
    }
1 ✔
406

407
    private void closedInternal(Status status) {
408
      if (delayedMessage != null) {
1 ✔
409
        GrpcUtil.closeQuietly(delayedMessage);
1 ✔
410
        delayedMessage = null;
1 ✔
411
      }
412
      Throwable cancelCause = null;
1 ✔
413
      try {
414
        if (status.isOk()) {
1 ✔
415
          listener.onComplete();
1 ✔
416
        } else {
417
          call.cancelled = true;
1 ✔
418
          listener.onCancel();
1 ✔
419
          // The status will not have a cause in all failure scenarios, but we want to make sure
420
          // we always cancel the context with one to keep the context cancelled state consistent.
421
          cancelCause = InternalStatus.asRuntimeExceptionWithoutStacktrace(
1 ✔
422
              Status.CANCELLED.withDescription("RPC cancelled"), null);
1 ✔
423
        }
424
      } finally {
425
        // Cancel context after delivering RPC closure notification to allow the application to
426
        // clean up and update any state based on whether onComplete or onCancel was called.
427
        context.cancel(cancelCause);
1 ✔
428
      }
429
    }
1 ✔
430

431
    @Override
432
    public void onReady() {
433
      try (TaskCloseable ignore = PerfMark.traceTask("ServerStreamListener.onReady")) {
1 ✔
434
        PerfMark.attachTag(call.tag);
1 ✔
435
        if (call.cancelled) {
1 ✔
436
          return;
1 ✔
437
        }
438
        listener.onReady();
1 ✔
439
      }
440
    }
1 ✔
441

442
    @Override
443
    public void triggerEvent(Object event) {
444
      try (TaskCloseable ignore = PerfMark.traceTask("ServerStreamListener.triggerEvent")) {
1 ✔
445
        PerfMark.attachTag(call.tag);
1 ✔
446
        if (call.cancelled) {
1 ✔
447
          return;
1 ✔
448
        }
449
        listener.onEvent(event);
1 ✔
450
      }
451
    }
1 ✔
452
  }
453
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc