Coverage Report

Created: 2025-06-24 06:59

/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
}