/src/grpc-swift/Sources/GRPC/GRPCKeepaliveHandlers.swift
Line | Count | Source |
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 NIOCore |
17 | | import NIOHTTP2 |
18 | | |
19 | | struct PingHandler { |
20 | | /// Opaque ping data used for keep-alive pings. |
21 | | private let pingData: HTTP2PingData |
22 | | |
23 | | /// Opaque ping data used for a ping sent after a GOAWAY frame. |
24 | | internal let pingDataGoAway: HTTP2PingData |
25 | | |
26 | | /// The amount of time to wait before sending a keepalive ping. |
27 | | private let interval: TimeAmount |
28 | | |
29 | | /// The amount of time to wait for an acknowledgment. |
30 | | /// If it does not receive an acknowledgment within this time, it will close the connection |
31 | | private let timeout: TimeAmount |
32 | | |
33 | | /// Send keepalive pings even if there are no calls in flight. |
34 | | private let permitWithoutCalls: Bool |
35 | | |
36 | | /// Maximum number of pings that can be sent when there is no data/header frame to be sent. |
37 | | private let maximumPingsWithoutData: UInt |
38 | | |
39 | | /// If there are no data/header frames being received: |
40 | | /// The minimum amount of time to wait between successive pings. |
41 | | private let minimumSentPingIntervalWithoutData: TimeAmount |
42 | | |
43 | | /// If there are no data/header frames being sent: |
44 | | /// The minimum amount of time expected between receiving successive pings. |
45 | | /// If the time between successive pings is less than this value, then the ping will be considered a bad ping from the peer. |
46 | | /// Such a ping counts as a "ping strike". |
47 | | /// Ping strikes are only applicable to server handler |
48 | | private let minimumReceivedPingIntervalWithoutData: TimeAmount? |
49 | | |
50 | | /// Maximum number of bad pings that the server will tolerate before sending an HTTP2 GOAWAY frame and closing the connection. |
51 | | /// Setting it to `0` allows the server to accept any number of bad pings. |
52 | | /// Ping strikes are only applicable to server handler |
53 | | private let maximumPingStrikes: UInt? |
54 | | |
55 | | /// When the handler started pinging |
56 | | private var startedAt: NIODeadline? |
57 | | |
58 | | /// When the last ping was received |
59 | | private var lastReceivedPingDate: NIODeadline? |
60 | | |
61 | | /// When the last ping was sent |
62 | | private var lastSentPingDate: NIODeadline? |
63 | | |
64 | | /// The number of pings sent on the transport without any data |
65 | 11.5k | private var sentPingsWithoutData = 0 |
66 | | |
67 | | /// Number of strikes |
68 | 11.5k | private var pingStrikes: UInt = 0 |
69 | | |
70 | | /// The scheduled task which will close the connection. |
71 | | private var scheduledClose: Scheduled<Void>? |
72 | | |
73 | | /// Number of active streams |
74 | 11.5k | private var activeStreams = 0 { |
75 | 128k | didSet { |
76 | 128k | if self.activeStreams > 0 { |
77 | 89.3k | self.sentPingsWithoutData = 0 |
78 | 89.3k | } |
79 | 128k | } |
80 | | } |
81 | | |
82 | | private static let goAwayFrame = HTTP2Frame.FramePayload.goAway( |
83 | | lastStreamID: .rootStream, |
84 | | errorCode: .enhanceYourCalm, |
85 | | opaqueData: nil |
86 | | ) |
87 | | |
88 | | // For testing only |
89 | | var _testingOnlyNow: NIODeadline? |
90 | | |
91 | | enum Action { |
92 | | case none |
93 | | case ack |
94 | | case schedulePing(delay: TimeAmount, timeout: TimeAmount) |
95 | | case cancelScheduledTimeout |
96 | | case reply(HTTP2Frame.FramePayload) |
97 | | case ratchetDownLastSeenStreamID |
98 | | } |
99 | | |
100 | | init( |
101 | | pingCode: UInt64, |
102 | | interval: TimeAmount, |
103 | | timeout: TimeAmount, |
104 | | permitWithoutCalls: Bool, |
105 | | maximumPingsWithoutData: UInt, |
106 | | minimumSentPingIntervalWithoutData: TimeAmount, |
107 | | minimumReceivedPingIntervalWithoutData: TimeAmount? = nil, |
108 | | maximumPingStrikes: UInt? = nil |
109 | 6.98k | ) { |
110 | 6.98k | self.pingData = HTTP2PingData(withInteger: pingCode) |
111 | 6.98k | self.pingDataGoAway = HTTP2PingData(withInteger: ~pingCode) |
112 | 6.98k | self.interval = interval |
113 | 6.98k | self.timeout = timeout |
114 | 6.98k | self.permitWithoutCalls = permitWithoutCalls |
115 | 6.98k | self.maximumPingsWithoutData = maximumPingsWithoutData |
116 | 6.98k | self.minimumSentPingIntervalWithoutData = minimumSentPingIntervalWithoutData |
117 | 6.98k | self.minimumReceivedPingIntervalWithoutData = minimumReceivedPingIntervalWithoutData |
118 | 6.98k | self.maximumPingStrikes = maximumPingStrikes |
119 | 6.98k | } |
120 | | |
121 | 16.2k | mutating func streamCreated() -> Action { |
122 | 16.2k | self.activeStreams += 1 |
123 | 16.2k | |
124 | 16.2k | if self.startedAt == nil { |
125 | 2.51k | self.startedAt = self.now() |
126 | 2.51k | return .schedulePing(delay: self.interval, timeout: self.timeout) |
127 | 13.7k | } else { |
128 | 13.7k | return .none |
129 | 13.7k | } |
130 | 16.2k | } |
131 | | |
132 | 11.5k | mutating func streamClosed() -> Action { |
133 | 11.5k | self.activeStreams -= 1 |
134 | 11.5k | return .none |
135 | 11.5k | } |
136 | | |
137 | 5.96k | mutating func read(pingData: HTTP2PingData, ack: Bool) -> Action { |
138 | 5.96k | if ack { |
139 | 4.80k | return self.handlePong(pingData) |
140 | 4.80k | } else { |
141 | 1.15k | return self.handlePing(pingData) |
142 | 1.15k | } |
143 | 5.96k | } |
144 | | |
145 | 13.2k | private func handlePong(_ pingData: HTTP2PingData) -> Action { |
146 | 13.2k | if pingData == self.pingData { |
147 | 1.51k | return .cancelScheduledTimeout |
148 | 11.7k | } else if pingData == self.pingDataGoAway { |
149 | 10.3k | // We received a pong for a ping we sent to trail a GOAWAY frame: this means we can now |
150 | 10.3k | // send another GOAWAY frame with a (possibly) lower stream ID. |
151 | 10.3k | return .ratchetDownLastSeenStreamID |
152 | 10.3k | } else { |
153 | 1.44k | return .none |
154 | 1.44k | } |
155 | 13.2k | } |
156 | | |
157 | 2.94k | private mutating func handlePing(_ pingData: HTTP2PingData) -> Action { |
158 | 2.94k | // Do we support ping strikes (only servers support ping strikes)? |
159 | 2.94k | if let maximumPingStrikes = self.maximumPingStrikes { |
160 | 2.94k | // Is this a ping strike? |
161 | 2.94k | if self.isPingStrike { |
162 | 0 | self.pingStrikes += 1 |
163 | 0 |
|
164 | 0 | // A maximum ping strike of zero indicates that we tolerate any number of strikes. |
165 | 0 | if maximumPingStrikes != 0, self.pingStrikes > maximumPingStrikes { |
166 | 0 | return .reply(PingHandler.goAwayFrame) |
167 | 0 | } else { |
168 | 0 | return .none |
169 | 0 | } |
170 | 2.94k | } else { |
171 | 2.94k | // This is a valid ping, reset our strike count and reply with a pong. |
172 | 2.94k | self.pingStrikes = 0 |
173 | 2.94k | self.lastReceivedPingDate = self.now() |
174 | 2.94k | return .ack |
175 | 2.94k | } |
176 | 2.94k | } else { |
177 | 0 | // We don't support ping strikes. We'll just reply with a pong. |
178 | 0 | // |
179 | 0 | // Note: we don't need to update `pingStrikes` or `lastReceivedPingDate` as we don't |
180 | 0 | // support ping strikes. |
181 | 0 | return .ack |
182 | 0 | } |
183 | 2.94k | } |
184 | | |
185 | 0 | mutating func pingFired() -> Action { |
186 | 0 | if self.shouldBlockPing { |
187 | 0 | return .none |
188 | 0 | } else { |
189 | 0 | return .reply(self.generatePingFrame(data: self.pingData)) |
190 | 0 | } |
191 | 0 | } |
192 | | |
193 | | private mutating func generatePingFrame( |
194 | | data: HTTP2PingData |
195 | 0 | ) -> HTTP2Frame.FramePayload { |
196 | 0 | if self.activeStreams == 0 { |
197 | 0 | self.sentPingsWithoutData += 1 |
198 | 0 | } |
199 | 0 |
|
200 | 0 | self.lastSentPingDate = self.now() |
201 | 0 | return HTTP2Frame.FramePayload.ping(data, ack: false) |
202 | 0 | } |
203 | | |
204 | | /// Returns true if, on receipt of a ping, the ping should be regarded as a ping strike. |
205 | | /// |
206 | | /// A ping is considered a 'strike' if: |
207 | | /// - There are no active streams. |
208 | | /// - We allow pings to be sent when there are no active streams (i.e. `self.permitWithoutCalls`). |
209 | | /// - The time since the last ping we received is less than the minimum allowed interval. |
210 | | /// |
211 | | /// - Precondition: Ping strikes are supported (i.e. `self.maximumPingStrikes != nil`) |
212 | 2.94k | private var isPingStrike: Bool { |
213 | 2.94k | assert( |
214 | 2.94k | self.maximumPingStrikes != nil, |
215 | 2.94k | "Ping strikes are not supported but we're checking for one" |
216 | 2.94k | ) |
217 | 2.94k | guard self.activeStreams == 0, self.permitWithoutCalls, |
218 | 2.94k | let lastReceivedPingDate = self.lastReceivedPingDate, |
219 | 2.94k | let minimumReceivedPingIntervalWithoutData = self.minimumReceivedPingIntervalWithoutData |
220 | 2.94k | else { |
221 | 2.94k | return false |
222 | 2.94k | } |
223 | 0 |
|
224 | 0 | return self.now() - lastReceivedPingDate < minimumReceivedPingIntervalWithoutData |
225 | 2.94k | } |
226 | | |
227 | 0 | private var shouldBlockPing: Bool { |
228 | 0 | // There is no active call on the transport and pings should not be sent |
229 | 0 | guard self.activeStreams > 0 || self.permitWithoutCalls else { |
230 | 0 | return true |
231 | 0 | } |
232 | 0 |
|
233 | 0 | // There is no active call on the transport but pings should be sent |
234 | 0 | if self.activeStreams == 0, self.permitWithoutCalls { |
235 | 0 | // The number of pings already sent on the transport without any data has already exceeded the limit |
236 | 0 | if self.sentPingsWithoutData > self.maximumPingsWithoutData { |
237 | 0 | return true |
238 | 0 | } |
239 | 0 |
|
240 | 0 | // The time elapsed since the previous ping is less than the minimum required |
241 | 0 | if let lastSentPingDate = self.lastSentPingDate, |
242 | 0 | self.now() - lastSentPingDate < self.minimumSentPingIntervalWithoutData |
243 | 0 | { |
244 | 0 | return true |
245 | 0 | } |
246 | 0 |
|
247 | 0 | return false |
248 | 0 | } |
249 | 0 |
|
250 | 0 | return false |
251 | 0 | } |
252 | | |
253 | 7.04k | private func now() -> NIODeadline { |
254 | 7.04k | return self._testingOnlyNow ?? .now() |
255 | 7.04k | } |
256 | | } |