Coverage Report

Created: 2025-06-24 06:59

/src/grpc-swift/Sources/GRPC/GRPCChannel/GRPCChannelBuilder.swift
Line
Count
Source (jump to first uncovered line)
1
/*
2
 * Copyright 2020, gRPC Authors All rights reserved.
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
import Dispatch
17
import Logging
18
import NIOCore
19
20
extension ClientConnection {
21
  /// Returns an insecure ``ClientConnection`` builder which is *not configured with TLS*.
22
0
  public static func insecure(group: EventLoopGroup) -> ClientConnection.Builder {
23
0
    return Builder(group: group)
24
0
  }
25
26
  /// Returns a ``ClientConnection`` builder configured with a TLS backend appropriate for the
27
  /// given `EventLoopGroup`.
28
  ///
29
  /// gRPC Swift offers two TLS 'backends'. The 'NIOSSL' backend is available on Darwin and Linux
30
  /// platforms and delegates to SwiftNIO SSL. On recent Darwin platforms (macOS 10.14+, iOS 12+,
31
  /// tvOS 12+, and watchOS 6+) the 'Network.framework' backend is available. The two backends have
32
  /// a number of incompatible configuration options and users are responsible for selecting the
33
  /// appropriate APIs. The TLS configuration options on the builder document which backends they
34
  /// support.
35
  ///
36
  /// TLS backends must also be used with an appropriate `EventLoopGroup` implementation. The
37
  /// 'NIOSSL' backend may be used either a `MultiThreadedEventLoopGroup` or a
38
  /// `NIOTSEventLoopGroup`. The 'Network.framework' backend may only be used with a
39
  /// `NIOTSEventLoopGroup`.
40
  ///
41
  /// This function returns a builder using the `NIOSSL` backend if a `MultiThreadedEventLoopGroup`
42
  /// is supplied and a 'Network.framework' backend if a `NIOTSEventLoopGroup` is used.
43
  public static func usingPlatformAppropriateTLS(
44
    for group: EventLoopGroup
45
0
  ) -> ClientConnection.Builder.Secure {
46
0
    let networkPreference = NetworkPreference.userDefined(.matchingEventLoopGroup(group))
47
0
    return Builder.Secure(
48
0
      group: group,
49
0
      tlsConfiguration: .makeClientDefault(for: networkPreference)
50
0
    )
51
0
  }
52
53
  /// Returns a ``ClientConnection`` builder configured with the TLS backend appropriate for the
54
  /// provided configuration and `EventLoopGroup`.
55
  ///
56
  /// - Important: The caller is responsible for ensuring the provided `configuration` may be used
57
  ///   the the `group`.
58
  public static func usingTLS(
59
    with configuration: GRPCTLSConfiguration,
60
    on group: EventLoopGroup
61
0
  ) -> ClientConnection.Builder.Secure {
62
0
    return Builder.Secure(group: group, tlsConfiguration: configuration)
63
0
  }
64
}
65
66
extension ClientConnection {
67
  public class Builder {
68
    private var configuration: ClientConnection.Configuration
69
0
    private var maybeTLS: GRPCTLSConfiguration? { return nil }
70
71
0
    private var connectionBackoff = ConnectionBackoff()
72
0
    private var connectionBackoffIsEnabled = true
73
74
0
    fileprivate init(group: EventLoopGroup) {
75
0
      // This is okay: the configuration is only consumed on a call to `connect` which sets the host
76
0
      // and port.
77
0
      self.configuration = .default(target: .hostAndPort("", .max), eventLoopGroup: group)
78
0
    }
79
80
0
    public func connect(host: String, port: Int) -> ClientConnection {
81
0
      // Finish setting up the configuration.
82
0
      self.configuration.target = .hostAndPort(host, port)
83
0
      self.configuration.connectionBackoff =
84
0
        self.connectionBackoffIsEnabled ? self.connectionBackoff : nil
85
0
      self.configuration.tlsConfiguration = self.maybeTLS
86
0
      return ClientConnection(configuration: self.configuration)
87
0
    }
88
89
0
    public func withConnectedSocket(_ socket: NIOBSDSocket.Handle) -> ClientConnection {
90
0
      precondition(
91
0
        !PlatformSupport.isTransportServicesEventLoopGroup(self.configuration.eventLoopGroup),
92
0
        "'\(#function)' requires 'group' to not be a 'NIOTransportServices.NIOTSEventLoopGroup' or 'NIOTransportServices.QoSEventLoop' (but was '\(type(of: self.configuration.eventLoopGroup))'"
93
0
      )
94
0
      self.configuration.target = .connectedSocket(socket)
95
0
      self.configuration.connectionBackoff =
96
0
        self.connectionBackoffIsEnabled ? self.connectionBackoff : nil
97
0
      self.configuration.tlsConfiguration = self.maybeTLS
98
0
      return ClientConnection(configuration: self.configuration)
99
0
    }
100
  }
101
}
102
103
extension ClientConnection.Builder {
104
  public class Secure: ClientConnection.Builder {
105
    internal var tls: GRPCTLSConfiguration
106
0
    override internal var maybeTLS: GRPCTLSConfiguration? {
107
0
      return self.tls
108
0
    }
109
110
0
    internal init(group: EventLoopGroup, tlsConfiguration: GRPCTLSConfiguration) {
111
0
      group.preconditionCompatible(with: tlsConfiguration)
112
0
      self.tls = tlsConfiguration
113
0
      super.init(group: group)
114
0
    }
115
116
    /// Connect to `host` on port 443.
117
0
    public func connect(host: String) -> ClientConnection {
118
0
      return self.connect(host: host, port: 443)
119
0
    }
120
  }
121
}
122
123
extension ClientConnection.Builder {
124
  /// Sets the initial connection backoff. That is, the initial time to wait before re-attempting to
125
  /// establish a connection. Jitter will *not* be applied to the initial backoff. Defaults to
126
  /// 1 second if not set.
127
  @discardableResult
128
0
  public func withConnectionBackoff(initial amount: TimeAmount) -> Self {
129
0
    self.connectionBackoff.initialBackoff = .seconds(from: amount)
130
0
    return self
131
0
  }
132
133
  /// Set the maximum connection backoff. That is, the maximum amount of time to wait before
134
  /// re-attempting to establish a connection. Note that this time amount represents the maximum
135
  /// backoff *before* jitter is applied. Defaults to 120 seconds if not set.
136
  @discardableResult
137
0
  public func withConnectionBackoff(maximum amount: TimeAmount) -> Self {
138
0
    self.connectionBackoff.maximumBackoff = .seconds(from: amount)
139
0
    return self
140
0
  }
141
142
  /// Backoff is 'jittered' to randomise the amount of time to wait before re-attempting to
143
  /// establish a connection. The jittered backoff will be no more than `jitter ⨯ unjitteredBackoff`
144
  /// from `unjitteredBackoff`. Defaults to 0.2 if not set.
145
  ///
146
  /// - Precondition: `0 <= jitter <= 1`
147
  @discardableResult
148
0
  public func withConnectionBackoff(jitter: Double) -> Self {
149
0
    self.connectionBackoff.jitter = jitter
150
0
    return self
151
0
  }
152
153
  /// The multiplier for scaling the unjittered backoff between attempts to establish a connection.
154
  /// Defaults to 1.6 if not set.
155
  @discardableResult
156
0
  public func withConnectionBackoff(multiplier: Double) -> Self {
157
0
    self.connectionBackoff.multiplier = multiplier
158
0
    return self
159
0
  }
160
161
  /// The minimum timeout to use when attempting to establishing a connection. The connection
162
  /// timeout for each attempt is the larger of the jittered backoff and the minimum connection
163
  /// timeout. Defaults to 20 seconds if not set.
164
  @discardableResult
165
0
  public func withConnectionTimeout(minimum amount: TimeAmount) -> Self {
166
0
    self.connectionBackoff.minimumConnectionTimeout = .seconds(from: amount)
167
0
    return self
168
0
  }
169
170
  /// Sets the initial and maximum backoff to given amount. Disables jitter and sets the backoff
171
  /// multiplier to 1.0.
172
  @discardableResult
173
0
  public func withConnectionBackoff(fixed amount: TimeAmount) -> Self {
174
0
    let seconds = Double.seconds(from: amount)
175
0
    self.connectionBackoff.initialBackoff = seconds
176
0
    self.connectionBackoff.maximumBackoff = seconds
177
0
    self.connectionBackoff.multiplier = 1.0
178
0
    self.connectionBackoff.jitter = 0.0
179
0
    return self
180
0
  }
181
182
  /// Sets the limit on the number of times to attempt to re-establish a connection. Defaults
183
  /// to `.unlimited` if not set.
184
  @discardableResult
185
0
  public func withConnectionBackoff(retries: ConnectionBackoff.Retries) -> Self {
186
0
    self.connectionBackoff.retries = retries
187
0
    return self
188
0
  }
189
190
  /// Sets whether the connection should be re-established automatically if it is dropped. Defaults
191
  /// to `true` if not set.
192
  @discardableResult
193
0
  public func withConnectionReestablishment(enabled: Bool) -> Self {
194
0
    self.connectionBackoffIsEnabled = enabled
195
0
    return self
196
0
  }
197
198
  /// Sets a custom configuration for keepalive
199
  /// The defaults for client and server are determined by the gRPC keepalive
200
  /// [documentation] (https://github.com/grpc/grpc/blob/master/doc/keepalive.md).
201
  @discardableResult
202
0
  public func withKeepalive(_ keepalive: ClientConnectionKeepalive) -> Self {
203
0
    self.configuration.connectionKeepalive = keepalive
204
0
    return self
205
0
  }
206
207
  /// The amount of time to wait before closing the connection. The idle timeout will start only
208
  /// if there are no RPCs in progress and will be cancelled as soon as any RPCs start. If a
209
  /// connection becomes idle, starting a new RPC will automatically create a new connection.
210
  /// Defaults to 30 minutes if not set.
211
  @discardableResult
212
0
  public func withConnectionIdleTimeout(_ timeout: TimeAmount) -> Self {
213
0
    self.configuration.connectionIdleTimeout = timeout
214
0
    return self
215
0
  }
216
217
  /// The behavior used to determine when an RPC should start. That is, whether it should wait for
218
  /// an active connection or fail quickly if no connection is currently available. Calls will
219
  /// use `.waitsForConnectivity` by default.
220
  @discardableResult
221
0
  public func withCallStartBehavior(_ behavior: CallStartBehavior) -> Self {
222
0
    self.configuration.callStartBehavior = behavior
223
0
    return self
224
0
  }
225
}
226
227
extension ClientConnection.Builder {
228
  /// Sets the client error delegate.
229
  @discardableResult
230
0
  public func withErrorDelegate(_ delegate: ClientErrorDelegate?) -> Self {
231
0
    self.configuration.errorDelegate = delegate
232
0
    return self
233
0
  }
234
}
235
236
extension ClientConnection.Builder {
237
  /// Sets the client connectivity state delegate and the `DispatchQueue` on which the delegate
238
  /// should be called. If no `queue` is provided then gRPC will create a `DispatchQueue` on which
239
  /// to run the delegate.
240
  @discardableResult
241
  public func withConnectivityStateDelegate(
242
    _ delegate: ConnectivityStateDelegate?,
243
    executingOn queue: DispatchQueue? = nil
244
0
  ) -> Self {
245
0
    self.configuration.connectivityStateDelegate = delegate
246
0
    self.configuration.connectivityStateDelegateQueue = queue
247
0
    return self
248
0
  }
249
}
250
251
// MARK: - Common TLS options
252
253
extension ClientConnection.Builder.Secure {
254
  /// Sets a server hostname override to be used for the TLS Server Name Indication (SNI) extension.
255
  /// The hostname from `connect(host:port)` is for TLS SNI if this value is not set and hostname
256
  /// verification is enabled.
257
  ///
258
  /// - Note: May be used with the 'NIOSSL' and 'Network.framework' TLS backend.
259
  /// - Note: `serverHostnameOverride` may not be `nil` when using the 'Network.framework' backend.
260
  @discardableResult
261
0
  public func withTLS(serverHostnameOverride: String?) -> Self {
262
0
    self.tls.hostnameOverride = serverHostnameOverride
263
0
    return self
264
0
  }
265
}
266
267
extension ClientConnection.Builder {
268
  /// Sets the HTTP/2 flow control target window size. Defaults to 8MB if not explicitly set.
269
  /// Values are clamped between 1 and 2^31-1 inclusive.
270
  @discardableResult
271
0
  public func withHTTPTargetWindowSize(_ httpTargetWindowSize: Int) -> Self {
272
0
    self.configuration.httpTargetWindowSize = httpTargetWindowSize
273
0
    return self
274
0
  }
275
276
  /// Sets the maximum size of an HTTP/2 frame in bytes which the client is willing to receive from
277
  /// the server. Defaults to 16384. Value are clamped between 2^14 and 2^24-1 octets inclusive
278
  /// (the minimum and maximum permitted values per RFC 7540 § 4.2).
279
  ///
280
  /// Raising this value may lower CPU usage for large message at the cost of increasing head of
281
  /// line blocking for small messages.
282
  @discardableResult
283
0
  public func withHTTPMaxFrameSize(_ httpMaxFrameSize: Int) -> Self {
284
0
    self.configuration.httpMaxFrameSize = httpMaxFrameSize
285
0
    return self
286
0
  }
287
}
288
289
extension ClientConnection.Builder {
290
  /// Sets the maximum message size the client is permitted to receive in bytes.
291
  ///
292
  /// - Precondition: `limit` must not be negative.
293
  @discardableResult
294
0
  public func withMaximumReceiveMessageLength(_ limit: Int) -> Self {
295
0
    self.configuration.maximumReceiveMessageLength = limit
296
0
    return self
297
0
  }
298
}
299
300
extension ClientConnection.Builder {
301
  /// Sets a logger to be used for background activity such as connection state changes. Defaults
302
  /// to a no-op logger if not explicitly set.
303
  ///
304
  /// Note that individual RPCs will use the logger from `CallOptions`, not the logger specified
305
  /// here.
306
  @discardableResult
307
0
  public func withBackgroundActivityLogger(_ logger: Logger) -> Self {
308
0
    self.configuration.backgroundActivityLogger = logger
309
0
    return self
310
0
  }
311
}
312
313
extension ClientConnection.Builder {
314
  /// A channel initializer which will be run after gRPC has initialized each channel. This may be
315
  /// used to add additional handlers to the pipeline and is intended for debugging.
316
  ///
317
  /// - Warning: The initializer closure may be invoked *multiple times*.
318
  @discardableResult
319
  @preconcurrency
320
  public func withDebugChannelInitializer(
321
    _ debugChannelInitializer: @Sendable @escaping (Channel) -> EventLoopFuture<Void>
322
0
  ) -> Self {
323
0
    self.configuration.debugChannelInitializer = debugChannelInitializer
324
0
    return self
325
0
  }
326
}
327
328
extension Double {
329
0
  fileprivate static func seconds(from amount: TimeAmount) -> Double {
330
0
    return Double(amount.nanoseconds) / 1_000_000_000
331
0
  }
332
}