xref: /aosp_15_r20/external/grpc-grpc/src/objective-c/GRPCClient/GRPCCallLegacy.mm (revision cc02d7e222339f7a4f6ba5f422e6413f4bd931f2)
1/*
2 *
3 * Copyright 2019 gRPC authors.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 *
17 */
18
19#import "GRPCCallLegacy.h"
20
21#import "GRPCCall+OAuth2.h"
22#import "GRPCCallOptions.h"
23#import "GRPCTypes.h"
24
25#import "private/GRPCCore/GRPCChannelPool.h"
26#import "private/GRPCCore/GRPCCompletionQueue.h"
27#import "private/GRPCCore/GRPCHost.h"
28#import "private/GRPCCore/GRPCWrappedCall.h"
29#import "private/GRPCCore/NSData+GRPC.h"
30
31#import <RxLibrary/GRXBufferedPipe.h>
32#import <RxLibrary/GRXConcurrentWriteable.h>
33#import <RxLibrary/GRXImmediateSingleWriter.h>
34#import <RxLibrary/GRXWriter+Immediate.h>
35
36#include <grpc/grpc.h>
37
38const char *kCFStreamVarName = "grpc_cfstream";
39static NSMutableDictionary *callFlags;
40
41// At most 6 ops can be in an op batch for a client: SEND_INITIAL_METADATA,
42// SEND_MESSAGE, SEND_CLOSE_FROM_CLIENT, RECV_INITIAL_METADATA, RECV_MESSAGE,
43// and RECV_STATUS_ON_CLIENT.
44NSInteger kMaxClientBatch = 6;
45
46static NSString *const kAuthorizationHeader = @"authorization";
47static NSString *const kBearerPrefix = @"Bearer ";
48
49@interface GRPCCall () <GRXWriteable>
50// Make them read-write.
51@property(atomic, copy) NSDictionary *responseHeaders;
52@property(atomic, copy) NSDictionary *responseTrailers;
53
54- (void)receiveNextMessages:(NSUInteger)numberOfMessages;
55
56@end
57
58// The following methods of a C gRPC call object aren't reentrant, and thus
59// calls to them must be serialized:
60// - start_batch
61// - destroy
62//
63// start_batch with a SEND_MESSAGE argument can only be called after the
64// OP_COMPLETE event for any previous write is received. This is achieved by
65// pausing the requests writer immediately every time it writes a value, and
66// resuming it again when OP_COMPLETE is received.
67//
68// Similarly, start_batch with a RECV_MESSAGE argument can only be called after
69// the OP_COMPLETE event for any previous read is received.This is easier to
70// enforce, as we're writing the received messages into the writeable:
71// start_batch is enqueued once upon receiving the OP_COMPLETE event for the
72// RECV_METADATA batch, and then once after receiving each OP_COMPLETE event for
73// each RECV_MESSAGE batch.
74@implementation GRPCCall {
75  dispatch_queue_t _callQueue;
76
77  NSString *_host;
78  NSString *_path;
79  GRPCCallSafety _callSafety;
80  GRPCCallOptions *_callOptions;
81  GRPCWrappedCall *_wrappedCall;
82
83  // The C gRPC library has less guarantees on the ordering of events than we
84  // do. Particularly, in the face of errors, there's no ordering guarantee at
85  // all. This wrapper over our actual writeable ensures thread-safety and
86  // correct ordering.
87  GRXConcurrentWriteable *_responseWriteable;
88
89  // The network thread wants the requestWriter to resume (when the server is ready for more input),
90  // or to stop (on errors), concurrently with user threads that want to start it, pause it or stop
91  // it. Because a writer isn't thread-safe, we'll synchronize those operations on it.
92  // We don't use a dispatch queue for that purpose, because the writer can call writeValue: or
93  // writesFinishedWithError: on this GRPCCall as part of those operations. We want to be able to
94  // pause the writer immediately on writeValue:, so we need our locking to be recursive.
95  GRXWriter *_requestWriter;
96
97  // To create a retain cycle when a call is started, up until it finishes. See
98  // |startWithWriteable:| and |finishWithError:|. This saves users from having to retain a
99  // reference to the call object if all they're interested in is the handler being executed when
100  // the response arrives.
101  GRPCCall *_retainSelf;
102
103  GRPCRequestHeaders *_requestHeaders;
104
105  // In the case that the call is a unary call (i.e. the writer to GRPCCall is of type
106  // GRXImmediateSingleWriter), GRPCCall will delay sending ops (not send them to C core
107  // immediately) and buffer them into a batch _unaryOpBatch. The batch is sent to C core when
108  // the SendClose op is added.
109  BOOL _unaryCall;
110  NSMutableArray *_unaryOpBatch;
111
112  // The dispatch queue to be used for enqueuing responses to user. Defaulted to the main dispatch
113  // queue
114  dispatch_queue_t _responseQueue;
115
116  // The OAuth2 token fetched from a token provider.
117  NSString *_fetchedOauth2AccessToken;
118
119  // The callback to be called when a write message op is done.
120  void (^_writeDone)(void);
121
122  // Indicate a read request to core is pending.
123  BOOL _pendingCoreRead;
124
125  // Indicate pending read message request from user.
126  NSUInteger _pendingReceiveNextMessages;
127}
128
129@synthesize state = _state;
130
131+ (void)initialize {
132  // Guarantees the code in {} block is invoked only once. See ref at:
133  // https://developer.apple.com/documentation/objectivec/nsobject/1418639-initialize?language=objc
134  if (self == [GRPCCall self]) {
135    grpc_init();
136    callFlags = [NSMutableDictionary dictionary];
137  }
138}
139
140+ (void)setCallSafety:(GRPCCallSafety)callSafety host:(NSString *)host path:(NSString *)path {
141  if (host.length == 0 || path.length == 0) {
142    return;
143  }
144  NSString *hostAndPath = [NSString stringWithFormat:@"%@/%@", host, path];
145  @synchronized(callFlags) {
146    switch (callSafety) {
147      case GRPCCallSafetyDefault:
148        callFlags[hostAndPath] = @0;
149        break;
150      default:
151        break;
152    }
153  }
154}
155
156+ (uint32_t)callFlagsForHost:(NSString *)host path:(NSString *)path {
157  NSString *hostAndPath = [NSString stringWithFormat:@"%@/%@", host, path];
158  @synchronized(callFlags) {
159    return [callFlags[hostAndPath] intValue];
160  }
161}
162
163- (instancetype)initWithHost:(NSString *)host
164                        path:(NSString *)path
165              requestsWriter:(GRXWriter *)requestWriter {
166  return [self initWithHost:host
167                       path:path
168                 callSafety:GRPCCallSafetyDefault
169             requestsWriter:requestWriter
170                callOptions:nil
171                  writeDone:nil];
172}
173
174- (instancetype)initWithHost:(NSString *)host
175                        path:(NSString *)path
176                  callSafety:(GRPCCallSafety)safety
177              requestsWriter:(GRXWriter *)requestsWriter
178                 callOptions:(GRPCCallOptions *)callOptions
179                   writeDone:(void (^)(void))writeDone {
180  // Purposely using pointer rather than length (host.length == 0) for backwards compatibility.
181  NSAssert(host != nil && path != nil, @"Neither host nor path can be nil.");
182  NSAssert(safety <= GRPCCallSafetyDefault, @"Invalid call safety value.");
183  NSAssert(requestsWriter.state == GRXWriterStateNotStarted,
184           @"The requests writer can't be already started.");
185  if (!host || !path) {
186    return nil;
187  }
188  if (requestsWriter.state != GRXWriterStateNotStarted) {
189    return nil;
190  }
191
192  if ((self = [super init])) {
193    _host = [host copy];
194    _path = [path copy];
195    _callSafety = safety;
196    _callOptions = [callOptions copy];
197
198    // Serial queue to invoke the non-reentrant methods of the grpc_call object.
199    _callQueue = dispatch_queue_create("io.grpc.call", DISPATCH_QUEUE_SERIAL);
200
201    _requestWriter = requestsWriter;
202    _requestHeaders = [[GRPCRequestHeaders alloc] initWithCall:self];
203    _writeDone = writeDone;
204
205    if ([requestsWriter isKindOfClass:[GRXImmediateSingleWriter class]]) {
206      _unaryCall = YES;
207      _unaryOpBatch = [NSMutableArray arrayWithCapacity:kMaxClientBatch];
208    }
209
210    _responseQueue = dispatch_get_main_queue();
211
212    // do not start a read until initial metadata is received
213    _pendingReceiveNextMessages = 0;
214    _pendingCoreRead = YES;
215  }
216  return self;
217}
218
219- (void)setResponseDispatchQueue:(dispatch_queue_t)queue {
220  @synchronized(self) {
221    if (_state != GRXWriterStateNotStarted) {
222      return;
223    }
224    _responseQueue = queue;
225  }
226}
227
228#pragma mark Finish
229
230// This function should support being called within a @synchronized(self) block in another function
231// Should not manipulate _requestWriter for deadlock prevention.
232- (void)finishWithError:(NSError *)errorOrNil {
233  @synchronized(self) {
234    if (_state == GRXWriterStateFinished) {
235      return;
236    }
237    _state = GRXWriterStateFinished;
238
239    if (errorOrNil) {
240      [_responseWriteable cancelWithError:errorOrNil];
241    } else {
242      [_responseWriteable enqueueSuccessfulCompletion];
243    }
244
245    // If the call isn't retained anywhere else, it can be deallocated now.
246    _retainSelf = nil;
247  }
248}
249
250- (void)cancel {
251  @synchronized(self) {
252    if (_state == GRXWriterStateFinished) {
253      return;
254    }
255    [self finishWithError:[NSError
256                              errorWithDomain:kGRPCErrorDomain
257                                         code:GRPCErrorCodeCancelled
258                                     userInfo:@{NSLocalizedDescriptionKey : @"Canceled by app"}]];
259    [_wrappedCall cancel];
260  }
261  _requestWriter.state = GRXWriterStateFinished;
262}
263
264- (void)dealloc {
265  if (_callQueue) {
266    __block GRPCWrappedCall *wrappedCall = _wrappedCall;
267    dispatch_async(_callQueue, ^{
268      wrappedCall = nil;
269    });
270  } else {
271    _wrappedCall = nil;
272  }
273}
274
275#pragma mark Read messages
276
277// Only called from the call queue.
278// The handler will be called from the network queue.
279- (void)startReadWithHandler:(void (^)(grpc_byte_buffer *))handler {
280  // TODO(jcanizales): Add error handlers for async failures
281  [_wrappedCall startBatchWithOperations:@[ [[GRPCOpRecvMessage alloc] initWithHandler:handler] ]];
282}
283
284// Called initially from the network queue once response headers are received,
285// then "recursively" from the responseWriteable queue after each response from the
286// server has been written.
287// If the call is currently paused, this is a noop. Restarting the call will invoke this
288// method.
289// TODO(jcanizales): Rename to readResponseIfNotPaused.
290- (void)maybeStartNextRead {
291  @synchronized(self) {
292    if (_state != GRXWriterStateStarted) {
293      return;
294    }
295    if (_callOptions.flowControlEnabled && (_pendingCoreRead || _pendingReceiveNextMessages == 0)) {
296      return;
297    }
298    _pendingCoreRead = YES;
299    _pendingReceiveNextMessages--;
300  }
301
302  dispatch_async(_callQueue, ^{
303    __weak GRPCCall *weakSelf = self;
304    [self startReadWithHandler:^(grpc_byte_buffer *message) {
305      if (message == NULL) {
306        // No more messages from the server
307        return;
308      }
309      __strong GRPCCall *strongSelf = weakSelf;
310      if (strongSelf == nil) {
311        grpc_byte_buffer_destroy(message);
312        return;
313      }
314      NSData *data = [NSData grpc_dataWithByteBuffer:message];
315      grpc_byte_buffer_destroy(message);
316      if (!data) {
317        // The app doesn't have enough memory to hold the server response. We
318        // don't want to throw, because the app shouldn't crash for a behavior
319        // that's on the hands of any server to have. Instead we finish and ask
320        // the server to cancel.
321        @synchronized(strongSelf) {
322          strongSelf->_pendingCoreRead = NO;
323          [strongSelf
324              finishWithError:[NSError errorWithDomain:kGRPCErrorDomain
325                                                  code:GRPCErrorCodeResourceExhausted
326                                              userInfo:@{
327                                                NSLocalizedDescriptionKey :
328                                                    @"Client does not have enough memory to "
329                                                    @"hold the server response."
330                                              }]];
331          [strongSelf->_wrappedCall cancel];
332        }
333        strongSelf->_requestWriter.state = GRXWriterStateFinished;
334      } else {
335        @synchronized(strongSelf) {
336          [strongSelf->_responseWriteable enqueueValue:data
337                                     completionHandler:^{
338                                       __strong GRPCCall *strongSelf = weakSelf;
339                                       if (strongSelf) {
340                                         @synchronized(strongSelf) {
341                                           strongSelf->_pendingCoreRead = NO;
342                                           [strongSelf maybeStartNextRead];
343                                         }
344                                       }
345                                     }];
346        }
347      }
348    }];
349  });
350}
351
352#pragma mark Send headers
353
354- (void)sendHeaders {
355  // TODO (mxyan): Remove after deprecated methods are removed
356  uint32_t callSafetyFlags = 0;
357
358  NSMutableDictionary *headers = [_requestHeaders mutableCopy];
359  NSString *fetchedOauth2AccessToken;
360  @synchronized(self) {
361    fetchedOauth2AccessToken = _fetchedOauth2AccessToken;
362  }
363  if (fetchedOauth2AccessToken != nil) {
364    headers[@"authorization"] = [kBearerPrefix stringByAppendingString:fetchedOauth2AccessToken];
365  } else if (_callOptions.oauth2AccessToken != nil) {
366    headers[@"authorization"] =
367        [kBearerPrefix stringByAppendingString:_callOptions.oauth2AccessToken];
368  }
369
370  // TODO(jcanizales): Add error handlers for async failures
371  GRPCOpSendMetadata *op = [[GRPCOpSendMetadata alloc]
372      initWithMetadata:headers
373                 flags:callSafetyFlags
374               handler:nil];  // No clean-up needed after SEND_INITIAL_METADATA
375  dispatch_async(_callQueue, ^{
376    if (!self->_unaryCall) {
377      [self->_wrappedCall startBatchWithOperations:@[ op ]];
378    } else {
379      [self->_unaryOpBatch addObject:op];
380    }
381  });
382}
383
384- (void)receiveNextMessages:(NSUInteger)numberOfMessages {
385  if (numberOfMessages == 0) {
386    return;
387  }
388  @synchronized(self) {
389    _pendingReceiveNextMessages += numberOfMessages;
390
391    if (_state != GRXWriterStateStarted || !_callOptions.flowControlEnabled) {
392      return;
393    }
394    [self maybeStartNextRead];
395  }
396}
397
398#pragma mark GRXWriteable implementation
399
400// Only called from the call queue. The error handler will be called from the
401// network queue if the write didn't succeed.
402// If the call is a unary call, parameter \a errorHandler will be ignored and
403// the error handler of GRPCOpSendClose will be executed in case of error.
404- (void)writeMessage:(NSData *)message withErrorHandler:(void (^)(void))errorHandler {
405  __weak GRPCCall *weakSelf = self;
406  void (^resumingHandler)(void) = ^{
407    // Resume the request writer.
408    GRPCCall *strongSelf = weakSelf;
409    if (strongSelf) {
410      strongSelf->_requestWriter.state = GRXWriterStateStarted;
411      if (strongSelf->_writeDone) {
412        strongSelf->_writeDone();
413      }
414    }
415  };
416  GRPCOpSendMessage *op = [[GRPCOpSendMessage alloc] initWithMessage:message
417                                                             handler:resumingHandler];
418  if (!_unaryCall) {
419    [_wrappedCall startBatchWithOperations:@[ op ] errorHandler:errorHandler];
420  } else {
421    // Ignored errorHandler since it is the same as the one for GRPCOpSendClose.
422    // TODO (mxyan): unify the error handlers of all Ops into a single closure.
423    [_unaryOpBatch addObject:op];
424  }
425}
426
427- (void)writeValue:(id)value {
428  NSAssert([value isKindOfClass:[NSData class]], @"value must be of type NSData");
429
430  @synchronized(self) {
431    if (_state == GRXWriterStateFinished) {
432      return;
433    }
434  }
435
436  // Pause the input and only resume it when the C layer notifies us that writes
437  // can proceed.
438  _requestWriter.state = GRXWriterStatePaused;
439
440  dispatch_async(_callQueue, ^{
441    // Write error is not processed here. It is handled by op batch of GRPC_OP_RECV_STATUS_ON_CLIENT
442    [self writeMessage:value withErrorHandler:nil];
443  });
444}
445
446// Only called from the call queue. The error handler will be called from the
447// network queue if the requests stream couldn't be closed successfully.
448- (void)finishRequestWithErrorHandler:(void (^)(void))errorHandler {
449  if (!_unaryCall) {
450    [_wrappedCall startBatchWithOperations:@[ [[GRPCOpSendClose alloc] init] ]
451                              errorHandler:errorHandler];
452  } else {
453    [_unaryOpBatch addObject:[[GRPCOpSendClose alloc] init]];
454    [_wrappedCall startBatchWithOperations:_unaryOpBatch errorHandler:errorHandler];
455  }
456}
457
458- (void)writesFinishedWithError:(NSError *)errorOrNil {
459  if (errorOrNil) {
460    [self cancel];
461  } else {
462    dispatch_async(_callQueue, ^{
463      // EOS error is not processed here. It is handled by op batch of GRPC_OP_RECV_STATUS_ON_CLIENT
464      [self finishRequestWithErrorHandler:nil];
465    });
466  }
467}
468
469#pragma mark Invoke
470
471// Both handlers will eventually be called, from the network queue. Writes can start immediately
472// after this.
473// The first one (headersHandler), when the response headers are received.
474// The second one (completionHandler), whenever the RPC finishes for any reason.
475- (void)invokeCallWithHeadersHandler:(void (^)(NSDictionary *))headersHandler
476                   completionHandler:(void (^)(NSError *, NSDictionary *))completionHandler {
477  dispatch_async(_callQueue, ^{
478    // TODO(jcanizales): Add error handlers for async failures
479    [self->_wrappedCall
480        startBatchWithOperations:@[ [[GRPCOpRecvMetadata alloc] initWithHandler:headersHandler] ]];
481    [self->_wrappedCall
482        startBatchWithOperations:@[ [[GRPCOpRecvStatus alloc] initWithHandler:completionHandler] ]];
483  });
484}
485
486- (void)invokeCall {
487  __weak GRPCCall *weakSelf = self;
488  [self
489      invokeCallWithHeadersHandler:^(NSDictionary *headers) {
490        // Response headers received.
491        __strong GRPCCall *strongSelf = weakSelf;
492        if (strongSelf) {
493          @synchronized(strongSelf) {
494            // it is ok to set nil because headers are only received once
495            strongSelf.responseHeaders = nil;
496            // copy the header so that the GRPCOpRecvMetadata object may be dealloc'ed
497            NSDictionary *copiedHeaders = [[NSDictionary alloc] initWithDictionary:headers
498                                                                         copyItems:YES];
499            strongSelf.responseHeaders = copiedHeaders;
500            strongSelf->_pendingCoreRead = NO;
501            [strongSelf maybeStartNextRead];
502          }
503        }
504      }
505      completionHandler:^(NSError *error, NSDictionary *trailers) {
506        __strong GRPCCall *strongSelf = weakSelf;
507        if (strongSelf) {
508          strongSelf.responseTrailers = trailers;
509
510          if (error) {
511            NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
512            if (error.userInfo) {
513              [userInfo addEntriesFromDictionary:error.userInfo];
514            }
515            userInfo[kGRPCTrailersKey] = strongSelf.responseTrailers;
516            // Since gRPC core does not guarantee the headers block being called before this block,
517            // responseHeaders might be nil.
518            userInfo[kGRPCHeadersKey] = strongSelf.responseHeaders;
519            error = [NSError errorWithDomain:error.domain code:error.code userInfo:userInfo];
520          }
521          [strongSelf finishWithError:error];
522          strongSelf->_requestWriter.state = GRXWriterStateFinished;
523        }
524      }];
525}
526
527#pragma mark GRXWriter implementation
528
529// Lock acquired inside startWithWriteable:
530- (void)startCallWithWriteable:(id<GRXWriteable>)writeable {
531  @synchronized(self) {
532    if (_state == GRXWriterStateFinished) {
533      return;
534    }
535
536    _responseWriteable = [[GRXConcurrentWriteable alloc] initWithWriteable:writeable
537                                                             dispatchQueue:_responseQueue];
538
539    GRPCPooledChannel *channel = [[GRPCChannelPool sharedInstance] channelWithHost:_host
540                                                                       callOptions:_callOptions];
541    _wrappedCall = [channel wrappedCallWithPath:_path
542                                completionQueue:[GRPCCompletionQueue completionQueue]
543                                    callOptions:_callOptions];
544
545    if (_wrappedCall == nil) {
546      [self finishWithError:[NSError errorWithDomain:kGRPCErrorDomain
547                                                code:GRPCErrorCodeUnavailable
548                                            userInfo:@{
549                                              NSLocalizedDescriptionKey :
550                                                  @"Failed to create call or channel."
551                                            }]];
552      return;
553    }
554
555    [self sendHeaders];
556    [self invokeCall];
557  }
558
559  // Now that the RPC has been initiated, request writes can start.
560  [_requestWriter startWithWriteable:self];
561}
562
563- (void)startWithWriteable:(id<GRXWriteable>)writeable {
564  id<GRPCAuthorizationProtocol> tokenProvider = nil;
565  @synchronized(self) {
566    _state = GRXWriterStateStarted;
567
568    // Create a retain cycle so that this instance lives until the RPC finishes (or is cancelled).
569    // This makes RPCs in which the call isn't externally retained possible (as long as it is
570    // started before being autoreleased). Care is taken not to retain self strongly in any of the
571    // blocks used in this implementation, so that the life of the instance is determined by this
572    // retain cycle.
573    _retainSelf = self;
574
575    // If _callOptions is nil, people must be using the deprecated v1 interface. In this case,
576    // generate the call options from the corresponding GRPCHost configs and apply other options
577    // that are not covered by GRPCHost.
578    if (_callOptions == nil) {
579      GRPCMutableCallOptions *callOptions = [[GRPCHost callOptionsForHost:_host] mutableCopy];
580      if (_serverName.length != 0) {
581        callOptions.serverAuthority = _serverName;
582      }
583      if (_timeout > 0) {
584        callOptions.timeout = _timeout;
585      }
586
587      id<GRPCAuthorizationProtocol> tokenProvider = self.tokenProvider;
588      if (tokenProvider != nil) {
589        callOptions.authTokenProvider = tokenProvider;
590      }
591      _callOptions = callOptions;
592    }
593
594    NSAssert(_callOptions.authTokenProvider == nil || _callOptions.oauth2AccessToken == nil,
595             @"authTokenProvider and oauth2AccessToken cannot be set at the same time");
596
597    tokenProvider = _callOptions.authTokenProvider;
598  }
599
600  if (tokenProvider != nil) {
601    __weak auto weakSelf = self;
602    [tokenProvider getTokenWithHandler:^(NSString *token) {
603      __strong auto strongSelf = weakSelf;
604      if (strongSelf) {
605        BOOL startCall = NO;
606        @synchronized(strongSelf) {
607          if (strongSelf->_state != GRXWriterStateFinished) {
608            startCall = YES;
609            if (token) {
610              strongSelf->_fetchedOauth2AccessToken = [token copy];
611            }
612          }
613        }
614        if (startCall) {
615          [strongSelf startCallWithWriteable:writeable];
616        }
617      }
618    }];
619  } else {
620    [self startCallWithWriteable:writeable];
621  }
622}
623
624- (void)setState:(GRXWriterState)newState {
625  @synchronized(self) {
626    // Manual transitions are only allowed from the started or paused states.
627    if (_state == GRXWriterStateNotStarted || _state == GRXWriterStateFinished) {
628      return;
629    }
630
631    switch (newState) {
632      case GRXWriterStateFinished:
633        _state = newState;
634        // Per GRXWriter's contract, setting the state to Finished manually
635        // means one doesn't wish the writeable to be messaged anymore.
636        [_responseWriteable cancelSilently];
637        _responseWriteable = nil;
638        return;
639      case GRXWriterStatePaused:
640        _state = newState;
641        return;
642      case GRXWriterStateStarted:
643        if (_state == GRXWriterStatePaused) {
644          _state = newState;
645          [self maybeStartNextRead];
646        }
647        return;
648      case GRXWriterStateNotStarted:
649        return;
650    }
651  }
652}
653
654@end
655