/src/grpc-swift/Sources/GRPC/ConnectivityState.swift
Line | Count | Source (jump to first uncovered line) |
1 | | /* |
2 | | * Copyright 2019, 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 Foundation |
17 | | import Logging |
18 | | import NIOConcurrencyHelpers |
19 | | import NIOCore |
20 | | |
21 | | /// The connectivity state of a client connection. Note that this is heavily lifted from the gRPC |
22 | | /// documentation: https://github.com/grpc/grpc/blob/master/doc/connectivity-semantics-and-api.md. |
23 | | public enum ConnectivityState: Sendable { |
24 | | /// This is the state where the channel has not yet been created. |
25 | | case idle |
26 | | |
27 | | /// The channel is trying to establish a connection and is waiting to make progress on one of the |
28 | | /// steps involved in name resolution, TCP connection establishment or TLS handshake. |
29 | | case connecting |
30 | | |
31 | | /// The channel has successfully established a connection all the way through TLS handshake (or |
32 | | /// equivalent) and protocol-level (HTTP/2, etc) handshaking. |
33 | | case ready |
34 | | |
35 | | /// There has been some transient failure (such as a TCP 3-way handshake timing out or a socket |
36 | | /// error). Channels in this state will eventually switch to the ``connecting`` state and try to |
37 | | /// establish a connection again. Since retries are done with exponential backoff, channels that |
38 | | /// fail to connect will start out spending very little time in this state but as the attempts |
39 | | /// fail repeatedly, the channel will spend increasingly large amounts of time in this state. |
40 | | case transientFailure |
41 | | |
42 | | /// This channel has started shutting down. Any new RPCs should fail immediately. Pending RPCs |
43 | | /// may continue running till the application cancels them. Channels may enter this state either |
44 | | /// because the application explicitly requested a shutdown or if a non-recoverable error has |
45 | | /// happened during attempts to connect. Channels that have entered this state will never leave |
46 | | /// this state. |
47 | | case shutdown |
48 | | } |
49 | | |
50 | | public protocol ConnectivityStateDelegate: AnyObject, GRPCPreconcurrencySendable { |
51 | | /// Called when a change in ``ConnectivityState`` has occurred. |
52 | | /// |
53 | | /// - Parameter oldState: The old connectivity state. |
54 | | /// - Parameter newState: The new connectivity state. |
55 | | func connectivityStateDidChange(from oldState: ConnectivityState, to newState: ConnectivityState) |
56 | | |
57 | | /// Called when the connection has started quiescing, that is, the connection is going away but |
58 | | /// existing RPCs may continue to run. |
59 | | /// |
60 | | /// - Important: When this is called no new RPCs may be created until the connectivity state |
61 | | /// changes to 'idle' (the connection successfully quiesced) or 'transientFailure' (the |
62 | | /// connection was closed before quiescing completed). Starting RPCs before these state changes |
63 | | /// will lead to a connection error and the immediate failure of any outstanding RPCs. |
64 | | func connectionStartedQuiescing() |
65 | | } |
66 | | |
67 | | extension ConnectivityStateDelegate { |
68 | 0 | public func connectionStartedQuiescing() {} |
69 | | } |
70 | | |
71 | | // Unchecked because all mutable state is protected by locks. |
72 | | public class ConnectivityStateMonitor: @unchecked Sendable { |
73 | 0 | private let stateLock = NIOLock() |
74 | 0 | private var _state: ConnectivityState = .idle |
75 | | |
76 | 0 | private let delegateLock = NIOLock() |
77 | | private var _delegate: ConnectivityStateDelegate? |
78 | | private let delegateCallbackQueue: DispatchQueue |
79 | | |
80 | | /// Creates a new connectivity state monitor. |
81 | | /// |
82 | | /// - Parameter delegate: A delegate to call when the connectivity state changes. |
83 | | /// - Parameter queue: The `DispatchQueue` on which the delegate will be called. |
84 | 0 | init(delegate: ConnectivityStateDelegate?, queue: DispatchQueue?) { |
85 | 0 | self._delegate = delegate |
86 | 0 | self.delegateCallbackQueue = DispatchQueue(label: "io.grpc.connectivity", target: queue) |
87 | 0 | } |
88 | | |
89 | | /// The current state of connectivity. |
90 | 0 | public var state: ConnectivityState { |
91 | 0 | return self.stateLock.withLock { |
92 | 0 | self._state |
93 | 0 | } |
94 | 0 | } |
95 | | |
96 | | /// A delegate to call when the connectivity state changes. |
97 | | public var delegate: ConnectivityStateDelegate? { |
98 | 0 | get { |
99 | 0 | return self.delegateLock.withLock { |
100 | 0 | return self._delegate |
101 | 0 | } |
102 | 0 | } |
103 | 0 | set { |
104 | 0 | self.delegateLock.withLock { |
105 | 0 | self._delegate = newValue |
106 | 0 | } |
107 | 0 | } |
108 | | } |
109 | | |
110 | 0 | internal func updateState(to newValue: ConnectivityState, logger: Logger) { |
111 | 0 | let change: (ConnectivityState, ConnectivityState)? = self.stateLock.withLock { |
112 | 0 | let oldValue = self._state |
113 | 0 |
|
114 | 0 | if oldValue != newValue { |
115 | 0 | self._state = newValue |
116 | 0 | return (oldValue, newValue) |
117 | 0 | } else { |
118 | 0 | return nil |
119 | 0 | } |
120 | 0 | } |
121 | 0 |
|
122 | 0 | if let (oldState, newState) = change { |
123 | 0 | logger.debug( |
124 | 0 | "connectivity state change", |
125 | 0 | metadata: [ |
126 | 0 | "old_state": "\(oldState)", |
127 | 0 | "new_state": "\(newState)", |
128 | 0 | ] |
129 | 0 | ) |
130 | 0 |
|
131 | 0 | self.delegateCallbackQueue.async { |
132 | 0 | if let delegate = self.delegate { |
133 | 0 | delegate.connectivityStateDidChange(from: oldState, to: newState) |
134 | 0 | } |
135 | 0 | } |
136 | 0 | } |
137 | 0 | } |
138 | | |
139 | 0 | internal func beginQuiescing() { |
140 | 0 | self.delegateCallbackQueue.async { |
141 | 0 | if let delegate = self.delegate { |
142 | 0 | delegate.connectionStartedQuiescing() |
143 | 0 | } |
144 | 0 | } |
145 | 0 | } |
146 | | } |
147 | | |
148 | | extension ConnectivityStateMonitor: ConnectionManagerConnectivityDelegate { |
149 | | internal func connectionStateDidChange( |
150 | | _ connectionManager: ConnectionManager, |
151 | | from oldState: _ConnectivityState, |
152 | | to newState: _ConnectivityState |
153 | 0 | ) { |
154 | 0 | self.updateState(to: ConnectivityState(newState), logger: connectionManager.logger) |
155 | 0 | } |
156 | | |
157 | 0 | internal func connectionIsQuiescing(_ connectionManager: ConnectionManager) { |
158 | 0 | self.beginQuiescing() |
159 | 0 | } |
160 | | } |