Browse Source

Fix up some docs, add an faqs doc (#1121)

Motivation:

Documentation is -- generally speaking -- useful. More so when it's
correct.

Modifications:

- Add some missing documentation. There's still more to go but it's a
  start.
- Add an 'faqs' doc which answers some questions which aren't necessarily
  obvious from reading source documentation. It's in no way
  comprehensive and is intended to be built upon over time.
- Remove some dead code.

Result:

Better docs, hopefully.
George Barnett 4 years ago
parent
commit
c85a7efe87

+ 0 - 0
Sources/GRPC/ClientOptions.swift → Sources/GRPC/CallOptions.swift


+ 31 - 23
Sources/GRPC/ClientConnection.swift

@@ -22,23 +22,33 @@ import NIOTLS
 import NIOTransportServices
 import SwiftProtobuf
 
-/// Provides a single, managed connection to a server.
+/// Provides a single, managed connection to a server which is guaranteed to always use the same
+/// `EventLoop`.
 ///
-/// The connection to the server is provided by a single channel which will attempt to reconnect
-/// to the server if the connection is dropped. This connection is guaranteed to always use the same
-/// event loop.
+/// The connection to the server is provided by a single channel which will attempt to reconnect to
+/// the server if the connection is dropped. When either the client or server detects that the
+/// connection has become idle -- that is, there are no outstanding RPCs and the idle timeout has
+/// passed (5 minutes, by default) -- the underlying channel will be closed. The client will not
+/// idle the connection if any RPC exists, even if there has been no activity on the RPC for the
+/// idle timeout. Long-lived, low activity RPCs may benefit from configuring keepalive (see
+/// `ClientConnectionKeepalive`) which periodically pings the server to ensure that the connection
+/// is not dropped. If the connection is idle a new channel will be created on-demand when the next
+/// RPC is made.
 ///
-/// The connection is initially setup with a handler to verify that TLS was established
-/// successfully (assuming TLS is being used).
+/// The state of the connection can be observed using a `ConnectivityStateDelegate`.
+///
+/// Since the connection is managed, and may potentially spend long periods of time waiting for a
+/// connection to come up (cellular connections, for example), different behaviors may be used when
+/// starting a call. The different behaviors are detailed in the `CallStartBehavior` documentation.
+///
+/// ### Channel Pipeline
+///
+/// The `NIO.ChannelPipeline` for the connection is configured as such:
 ///
 ///               ┌──────────────────────────┐
 ///               │  DelegatingErrorHandler  │
 ///               └──────────▲───────────────┘
 ///                HTTP2Frame│
-///               ┌──────────┴───────────────┐
-///               │ SettingsObservingHandler │
-///               └──────────▲───────────────┘
-///                HTTP2Frame│
 ///                          │                ⠇ ⠇   ⠇ ⠇
 ///                          │               ┌┴─▼┐ ┌┴─▼┐
 ///                          │               │   | │   | HTTP/2 streams
@@ -49,11 +59,11 @@ import SwiftProtobuf
 ///                        └─▲───────────────────────┬─┘
 ///                HTTP2Frame│                       │HTTP2Frame
 ///                        ┌─┴───────────────────────▼─┐
-///                        │       NIOHTTP2Handler     │
+///                        │       GRPCIdleHandler     │
 ///                        └─▲───────────────────────┬─┘
-///                ByteBuffer│                       │ByteBuffer
+///                HTTP2Frame│                       │HTTP2Frame
 ///                        ┌─┴───────────────────────▼─┐
-///                        │   TLSVerificationHandler
+///                        │       NIOHTTP2Handler   
 ///                        └─▲───────────────────────┬─┘
 ///                ByteBuffer│                       │ByteBuffer
 ///                        ┌─┴───────────────────────▼─┐
@@ -62,15 +72,9 @@ import SwiftProtobuf
 ///                ByteBuffer│                       │ByteBuffer
 ///                          │                       ▼
 ///
-/// The `TLSVerificationHandler` observes the outcome of the SSL handshake and determines
-/// whether a `ClientConnection` should be returned to the user. In either eventuality, the
-/// handler removes itself from the pipeline once TLS has been verified. There is also a handler
-/// after the multiplexer for observing the initial settings frame, after which it determines that
-/// the connection state is `.ready` and removes itself from the channel. Finally there is a
-/// delegated error handler which uses the error delegate associated with this connection
-/// (see `DelegatingErrorHandler`).
-///
-/// See `BaseClientCall` for a description of the pipelines associated with each HTTP/2 stream.
+/// The 'GRPCIdleHandler' intercepts HTTP/2 frames and various events and is responsible for
+/// informing and controlling the state of the connection (idling and keepalive). The HTTP/2 streams
+/// are used to handle individual RPCs.
 public class ClientConnection {
   private let connectionManager: ConnectionManager
 
@@ -82,7 +86,10 @@ public class ClientConnection {
   /// The configuration for this client.
   internal let configuration: Configuration
 
+  /// The scheme of the URI for each RPC, i.e. 'http' or 'https'.
   internal let scheme: String
+
+  /// The authority of the URI for each RPC.
   internal let authority: String
 
   /// A monitor for the connectivity state.
@@ -264,7 +271,8 @@ public struct CallStartBehavior: Hashable {
 }
 
 extension ClientConnection {
-  /// The configuration for a connection.
+  /// Configuration for a `ClientConnection`. Users should prefer using one of the
+  /// `ClientConnection` builders: `ClientConnection.secure(_:)` or `ClientConnection.insecure(_:)`.
   public struct Configuration {
     /// The target to connect to.
     public var target: ConnectionTarget

+ 7 - 1
Sources/GRPC/Compression/MessageEncoding.swift

@@ -56,13 +56,16 @@ extension Compression {
   }
 }
 
+/// Whether compression is enabled or disabled for a client.
 public enum ClientMessageEncoding {
+  /// Compression is enabled with the given configuration.
   case enabled(Configuration)
+  /// Compression is disabled.
   case disabled
 }
 
 extension ClientMessageEncoding {
-  var enabledForRequests: Bool {
+  internal var enabledForRequests: Bool {
     switch self {
     case let .enabled(configuration):
       return configuration.outbound != nil
@@ -112,8 +115,11 @@ extension ClientMessageEncoding {
   }
 }
 
+/// Whether compression is enabled or disabled on the server.
 public enum ServerMessageEncoding {
+  /// Compression is supported with this configuration.
   case enabled(Configuration)
+  /// Compression is not enabled. However, 'identity' compression is still supported.
   case disabled
 
   @usableFromInline

+ 13 - 0
Sources/GRPC/GRPCClient.swift

@@ -166,8 +166,21 @@ extension GRPCClient {
 
 /// A client which has no generated stubs and may be used to create gRPC calls manually.
 /// See `GRPCClient` for details.
+///
+/// Example:
+///
+/// ```
+/// let client = AnyServiceClient(channel: channel)
+/// let rpc: UnaryCall<Request, Response> = client.makeUnaryCall(
+///   path: "/serviceName/methodName",
+///   request: .with { ... },
+/// }
+/// ```
 public final class AnyServiceClient: GRPCClient {
+  /// The gRPC channel over which RPCs are sent and received.
   public let channel: GRPCChannel
+
+  /// The default options passed to each RPC unless passed for each RPC.
   public var defaultCallOptions: CallOptions
 
   /// Creates a client which may be used to call any service.

+ 16 - 14
Sources/GRPC/_GRPCClientChannelHandler.swift → Sources/GRPC/GRPCClientChannelHandler.swift

@@ -36,6 +36,7 @@ public enum _GRPCClientRequestPart<Request> {
 }
 
 /// As `_GRPCClientRequestPart` but messages are serialized.
+/// - Important: This is **NOT** part of the public API.
 public typealias _RawGRPCClientRequestPart = _GRPCClientRequestPart<ByteBuffer>
 
 /// A gRPC client response message part.
@@ -56,6 +57,7 @@ public enum _GRPCClientResponsePart<Response> {
 }
 
 /// As `_GRPCClientResponsePart` but messages are serialized.
+/// - Important: This is **NOT** part of the public API.
 public typealias _RawGRPCClientResponsePart = _GRPCClientResponsePart<ByteBuffer>
 
 /// - Important: This is **NOT** part of the public API. It is declared as
@@ -281,10 +283,7 @@ public enum GRPCCallType {
 ///   return channel.pipeline.addHandler(clientChannelHandler)
 /// }
 /// ```
-///
-/// - Important: This is **NOT** part of the public API. It is declared as
-///   `public` because it is used within performance tests.
-public final class _GRPCClientChannelHandler {
+internal final class GRPCClientChannelHandler {
   private let logger: Logger
   private var stateMachine: GRPCClientStateMachine
 
@@ -293,7 +292,7 @@ public final class _GRPCClientChannelHandler {
   /// - Parameters:
   ///   - callType: Type of RPC call being made.
   ///   - logger: Logger.
-  public init(callType: GRPCCallType, logger: Logger) {
+  internal init(callType: GRPCCallType, logger: Logger) {
     self.logger = logger
     switch callType {
     case .unary:
@@ -310,11 +309,11 @@ public final class _GRPCClientChannelHandler {
 
 // MARK: - GRPCClientChannelHandler: Inbound
 
-extension _GRPCClientChannelHandler: ChannelInboundHandler {
-  public typealias InboundIn = HTTP2Frame.FramePayload
-  public typealias InboundOut = _RawGRPCClientResponsePart
+extension GRPCClientChannelHandler: ChannelInboundHandler {
+  internal typealias InboundIn = HTTP2Frame.FramePayload
+  internal typealias InboundOut = _RawGRPCClientResponsePart
 
-  public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
+  internal func channelRead(context: ChannelHandlerContext, data: NIOAny) {
     let payload = self.unwrapInboundIn(data)
     switch payload {
     case let .headers(content):
@@ -470,12 +469,15 @@ extension _GRPCClientChannelHandler: ChannelInboundHandler {
 
 // MARK: - GRPCClientChannelHandler: Outbound
 
-extension _GRPCClientChannelHandler: ChannelOutboundHandler {
-  public typealias OutboundIn = _RawGRPCClientRequestPart
-  public typealias OutboundOut = HTTP2Frame.FramePayload
+extension GRPCClientChannelHandler: ChannelOutboundHandler {
+  internal typealias OutboundIn = _RawGRPCClientRequestPart
+  internal typealias OutboundOut = HTTP2Frame.FramePayload
 
-  public func write(context: ChannelHandlerContext, data: NIOAny,
-                    promise: EventLoopPromise<Void>?) {
+  internal func write(
+    context: ChannelHandlerContext,
+    data: NIOAny,
+    promise: EventLoopPromise<Void>?
+  ) {
     switch self.unwrapOutboundIn(data) {
     case let .head(requestHead):
       // Feed the request into the state machine:

+ 1 - 1
Sources/GRPC/Interceptor/ClientTransportFactory.swift

@@ -226,7 +226,7 @@ private struct HTTP2ClientTransportFactory<Request, Response> {
 
         multiplexer.createStreamChannel(promise: streamPromise) { streamChannel in
           streamChannel.pipeline.addHandlers([
-            _GRPCClientChannelHandler(
+            GRPCClientChannelHandler(
               callType: transport.callDetails.type,
               logger: transport.logger
             ),

+ 10 - 38
Sources/GRPC/ServerCallContexts/ServerCallContext.swift

@@ -28,7 +28,7 @@ public protocol ServerCallContext: AnyObject {
   /// Request headers for this request.
   var headers: HPACKHeaders { get }
 
-  /// A 'UserInfo' dictionary.
+  /// A 'UserInfo' dictionary which is shared with the interceptor contexts for this RPC.
   var userInfo: UserInfo { get set }
 
   /// The logger used for this call.
@@ -42,9 +42,18 @@ public protocol ServerCallContext: AnyObject {
 
 /// Base class providing data provided to the framework user for all server calls.
 open class ServerCallContextBase: ServerCallContext {
+  /// The event loop this call is served on.
   public let eventLoop: EventLoop
+
+  /// Request headers for this request.
   public let headers: HPACKHeaders
+
+  /// The logger used for this call.
   public let logger: Logger
+
+  /// Whether compression should be enabled for responses, defaulting to `true`. Note that for
+  /// this value to take effect compression must have been enabled on the server and a compression
+  /// algorithm must have been negotiated with the client.
   public var compressionEnabled: Bool = true
 
   /// - Important: While `UserInfo` has value-semantics, this property retrieves from, and sets a
@@ -89,41 +98,4 @@ open class ServerCallContextBase: ServerCallContext {
     self.userInfoRef = userInfoRef
     self.logger = logger
   }
-
-  /// Processes an error, transforming it into a 'GRPCStatus' and any trailers to send to the peer.
-  internal func processObserverError(
-    _ error: Error,
-    delegate: ServerErrorDelegate?
-  ) -> (GRPCStatus, HPACKHeaders) {
-    // Observe the error if we have a delegate.
-    delegate?.observeRequestHandlerError(error, headers: self.headers)
-
-    // What status are we terminating this RPC with?
-    // - If we have a delegate, try transforming the error. If the delegate returns trailers, merge
-    //   them with any on the call context.
-    // - If we don't have a delegate, then try to transform the error to a status.
-    // - Fallback to a generic error.
-    let status: GRPCStatus
-    let trailers: HPACKHeaders
-
-    if let transformed = delegate?.transformRequestHandlerError(error, headers: self.headers) {
-      status = transformed.status
-      if var transformedTrailers = transformed.trailers {
-        // The delegate returned trailers: merge in those from the context as well.
-        transformedTrailers.add(contentsOf: self.trailers)
-        trailers = transformedTrailers
-      } else {
-        trailers = self.trailers
-      }
-    } else if let grpcStatusTransformable = error as? GRPCStatusTransformable {
-      status = grpcStatusTransformable.makeGRPCStatus()
-      trailers = self.trailers
-    } else {
-      // Eh... well, we don't what status to use. Use a generic one.
-      status = .processingError
-      trailers = self.trailers
-    }
-
-    return (status, trailers)
-  }
 }

+ 9 - 8
Sources/GRPC/ServerCallContexts/StreamingResponseCallContext.swift

@@ -20,15 +20,15 @@ import NIOHPACK
 import NIOHTTP1
 import SwiftProtobuf
 
-/// Abstract base class exposing a method to send multiple messages over the wire and a promise for the final RPC status.
-///
-/// - When `statusPromise` is fulfilled, the call is closed and the provided status transmitted.
-/// - If `statusPromise` is failed and the error is of type `GRPCStatusTransformable`,
-///   the result of `error.asGRPCStatus()` will be returned to the client.
-/// - If `error.asGRPCStatus()` is not available, `GRPCStatus.processingError` is returned to the client.
+/// An abstract base class for a context provided to handlers for RPCs which may return multiple
+/// responses, i.e. server streaming and bidirectional streaming RPCs.
 open class StreamingResponseCallContext<ResponsePayload>: ServerCallContextBase {
-  typealias WrappedResponse = GRPCServerResponsePart<ResponsePayload>
-
+  /// A promise for the `GRPCStatus`, the end of the response stream. This must be completed by
+  /// bidirectional streaming RPC handlers to end the RPC.
+  ///
+  /// Note that while this is also present for server streaming RPCs, it is not necessary to
+  /// complete this promise: instead, an `EventLoopFuture<GRPCStatus>` must be returned from the
+  /// handler.
   public let statusPromise: EventLoopPromise<GRPCStatus>
 
   public convenience init(
@@ -114,6 +114,7 @@ open class StreamingResponseCallContext<ResponsePayload>: ServerCallContextBase
   }
 }
 
+/// A concrete implementation of `StreamingResponseCallContext` used internally.
 @usableFromInline
 internal final class _StreamingResponseCallContext<Request, Response>:
   StreamingResponseCallContext<Response> {

+ 20 - 18
Sources/GRPC/ServerCallContexts/UnaryResponseCallContext.swift

@@ -20,19 +20,21 @@ import NIOHPACK
 import NIOHTTP1
 import SwiftProtobuf
 
-/// Abstract base class exposing a method that exposes a promise for the RPC response.
+/// A context provided to handlers for RPCs which return a single response, i.e. unary and client
+/// streaming RPCs.
 ///
-/// - When `responsePromise` is fulfilled, the call is closed and the provided response transmitted with status `responseStatus` (`.ok` by default).
-/// - If `statusPromise` is failed and the error is of type `GRPCStatusTransformable`,
-///   the result of `error.asGRPCStatus()` will be returned to the client.
-/// - If `error.asGRPCStatus()` is not available, `GRPCStatus.processingError` is returned to the client.
-///
-/// For unary calls, the response is not actually provided by fulfilling `responsePromise`, but instead by completing
-/// the future returned by `UnaryCallHandler.EventObserver`.
-open class UnaryResponseCallContext<ResponsePayload>: ServerCallContextBase, StatusOnlyCallContext {
-  typealias WrappedResponse = GRPCServerResponsePart<ResponsePayload>
+/// For client streaming RPCs the handler must complete the `responsePromise` to return the response
+/// to the client. Unary RPCs do complete the promise directly: they are provided an
+/// `StatusOnlyCallContext` view of this context where the `responsePromise` is not exposed. Instead
+/// they must return an `EventLoopFuture<Response>` from the method they are implementing.
+open class UnaryResponseCallContext<Response>: ServerCallContextBase, StatusOnlyCallContext {
+  /// A promise for a single response message. This must be completed to send a response back to the
+  /// client. If the promise is failed, the failure value will be converted to `GRPCStatus` and
+  /// used as the final status for the RPC.
+  public let responsePromise: EventLoopPromise<Response>
 
-  public let responsePromise: EventLoopPromise<ResponsePayload>
+  /// The status sent back to the client at the end of the RPC, providing the `responsePromise` was
+  /// completed successfully.
   public var responseStatus: GRPCStatus = .ok
 
   public convenience init(
@@ -59,18 +61,18 @@ open class UnaryResponseCallContext<ResponsePayload>: ServerCallContextBase, Sta
 /// Protocol variant of `UnaryResponseCallContext` that only exposes the `responseStatus` and `trailingMetadata`
 /// fields, but not `responsePromise`.
 ///
-/// Motivation: `UnaryCallHandler` already asks the call handler return an `EventLoopFuture<ResponsePayload>` which
-/// is automatically cascaded into `UnaryResponseCallContext.responsePromise`, so that promise does not (and should not)
-/// be fulfilled by the user.
-///
-/// We can use a protocol (instead of an abstract base class) here because removing the generic `responsePromise` field
-/// lets us avoid associated-type requirements on the protocol.
+/// We can use a protocol (instead of an abstract base class) here because removing the generic
+/// `responsePromise` field lets us avoid associated-type requirements on the protocol.
 public protocol StatusOnlyCallContext: ServerCallContext {
+  /// The status sent back to the client at the end of the RPC, providing the `responsePromise` was
+  /// completed successfully.
   var responseStatus: GRPCStatus { get set }
+
+  /// Metadata to return at the end of the RPC.
   var trailers: HPACKHeaders { get set }
 }
 
 /// Concrete implementation of `UnaryResponseCallContext` used for testing.
 ///
 /// Only provided to make it clear in tests that no "real" implementation is used.
-open class UnaryResponseCallContextTestStub<ResponsePayload>: UnaryResponseCallContext<ResponsePayload> {}
+open class UnaryResponseCallContextTestStub<Response>: UnaryResponseCallContext<Response> {}

+ 3 - 0
Sources/GRPC/UserInfo.swift

@@ -17,6 +17,9 @@
 /// `UserInfo` is a dictionary for heterogeneously typed values with type safe access to the stored
 /// values.
 ///
+/// `UserInfo` is shared between server interceptor contexts and server handlers, this is on a
+/// per-RPC basis. `UserInfo` is *not* shared across a connection.
+///
 /// Values are keyed by a type conforming to the `UserInfo.Key` protocol. The protocol requires an
 /// `associatedtype`: the type of the value the key is paired with. A key can be created using a
 /// caseless `enum`, for example:

+ 1 - 1
Sources/GRPC/_EmbeddedThroughput.swift

@@ -28,7 +28,7 @@ extension EmbeddedChannel {
     responseType: Response.Type = Response.self
   ) -> EventLoopFuture<Void> {
     return self.pipeline.addHandlers([
-      _GRPCClientChannelHandler(callType: callType, logger: logger),
+      GRPCClientChannelHandler(callType: callType, logger: logger),
       GRPCClientCodecHandler(
         serializer: ProtobufSerializer<Request>(),
         deserializer: ProtobufDeserializer<Response>()

+ 1 - 1
Tests/GRPCTests/GRPCStatusCodeTests.swift

@@ -29,7 +29,7 @@ class GRPCStatusCodeTests: GRPCTestCase {
   override func setUp() {
     super.setUp()
 
-    let handler = _GRPCClientChannelHandler(callType: .unary, logger: self.logger)
+    let handler = GRPCClientChannelHandler(callType: .unary, logger: self.logger)
     self.channel = EmbeddedChannel(handler: handler)
   }
 

+ 136 - 0
docs/faqs.md

@@ -0,0 +1,136 @@
+# FAQs
+
+## Logging / Tracing
+
+### Is Logging supported?
+
+Logging is supported by providing a `Logger` from [SwiftLog][swift-log] to the
+relevant configuration object.
+
+For the client:
+
+- `ClientConnection.Builder.withBackgroundActivityLogger` allows a logger to be
+  supplied which logs information at the connection level, relating to things
+  such as connectivity state changes, or connection errors.
+- `CallOptions` allows `logger` to be specified. It is used to log information
+  at the RPC level, such as changes to the state of the RPC. Any connection
+  level metadata will be attached to the logger so that RPC logs may be
+  correlated with connection level changes.
+
+For the server:
+
+- `Server.Builder.withLogger` allows a `logger` to be specified. It is used as a
+  root logger and is passed down to each accepted connection and in turn each
+  RPC on a connection. The logger is made available in the `context` of each
+  RPC handler.
+
+Note that gRPC will not emit logs unless a logger is explicitly provided.
+
+### How can RPCs be traced?
+
+For the client:
+
+If `logger` is set in the `CallOptions` for an RPC then gRPC may attach a
+request ID to the metadata of that logger. This is determined by the value of
+`requestIDProvider` in `CallOptions`. The options for `requestIDProvider`
+include:
+
+- `autogenerated`: a new UUID will be generated (default).
+- `generated(_:)`: the provided callback will be called to generate a new ID.
+- `userDefined(_:)`: the provided value will be used. Note: this option should
+  not be set as a default option on a client as all RPCs would used the same ID.
+- `none`: no request ID will be attached to the logger, this can be useful if a
+  logger already has a request ID associated with it.
+
+If a request ID is attached to the logger's metadata it will use the key
+`grpc_request_id`.
+
+If a request ID is provided and the `requestIDHeader` option is set then gRPC
+will add the ID to the request headers. By default `requestIDHeader` is not set.
+
+## Client Connection Lifecycle
+
+### Is the client's connection long-lived?
+
+The `ClientConnection` is intended to be used as a long-living connection to a
+remote peer. It will manage the underlying network resources automatically,
+creating a connection when necessary and dropping it when it is no longer
+required. However, the user must `close()` the connection when finished with it.
+
+The underlying connection may be in any of the following states:
+
+- Idle: there is no underlying connection.
+- Connecting: an attempt to establish a connection is being made.
+- Ready: the connection has been established, a TLS handshake has completed
+  (if applicable) and the first HTTP/2 settings frame has been received.
+- Transient failure: A transient error occurred either from a ready connection
+  or from a connection attempt. An new connection will be established after some
+  time. (See later sections on connection backoff for more details.)
+- Shutdown: The application requested that the connection be closed, or a
+  connection error occurred and connection re-establishment was disabled. This
+  state is terminal.
+
+The gRPC library [documents][grpc-conn-states] these states in more details.
+Note that not all information linked is applicable to gRPC Swift.
+
+### How can connection states be observed?
+
+A connectivity state delegate may be set using
+`withConnectivityStateDelegate(_:executingOn:)` on the
+`ClientConnection.Builder`.
+
+The delegate is called on the `DispatchQueue` specified by the `executingOn`
+parameter. gRPC will create a `DispatchQueue` if one isn't otherwise specified.
+
+These state changes will also be logged by the `backgroundActivityLogger` (see
+above).
+
+### When will the connection idle?
+
+The connection will be idled (i.e. the underlying connection closed and moved to
+the 'idle' state) if there are no outstanding RPCs for 5 minutes (by default).
+The connection will _not_ be idled if there outstanding RPCs which are not
+sending or receiving messages. This option may be configured using
+`withConnectionIdleTimeout` on the `ClientConnection.Builder`.
+
+Any RPC called after the connection has idled will trigger a connection
+attempt.
+
+### How can I keep a connection alive?
+
+For long-lived, low-activity RPCs it may be beneficial to configure keepalive.
+Doing so will periodically send pings to the remote peer. It may also be used to
+detect unresponsive peers.
+
+See the [gRPC Keepalive][grpc-keepalive] documentation for details.
+
+## RPC Lifecycle
+
+### How do I start an RPC?
+
+RPCs are usually started by invoking a method on a generated client. Each
+generated client relies on an underlying `GRPCChannel` to provide transport.
+
+RPCs can also be made without a generated client by using `AnyServiceClient`.
+This requires the user know the path (i.e. '/echo/Get') and request and response
+types for the RPC.
+
+### Are failing RPCs retried automatically?
+
+RPCs are never automatically retried by gRPC Swift.
+
+The framework cannot determine whether your RPC is idempotent, it is therefore
+not safe for gRPC Swift to automatically retry RPCs for you.
+
+### Deadlines and Timeouts
+
+It's recommended that deadlines are used to enforce a limit on the duration of
+an RPC. Users may set a time limit (either a deadline or a timeout) on the
+`CallOptions` for each call, or as a default on a client. RPCs which have not
+completed before the time limit will be failed with status code 4
+('deadline exceeded').
+
+
+[grpc-conn-states]: connectivity-semantics-and-api.md
+[grpc-keepalive]: keepalive.md
+[swift-log]: https://github.com/apple/swift-log