GRPCPingHandlerTests.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  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. @testable import GRPC
  17. import NIOCore
  18. import NIOHTTP2
  19. import XCTest
  20. class GRPCPingHandlerTests: GRPCTestCase {
  21. var pingHandler: PingHandler!
  22. func testClosingStreamWithoutPermitCalls() {
  23. // Do not allow pings without calls
  24. self.setupPingHandler(interval: .seconds(1), timeout: .seconds(1))
  25. // New stream created
  26. var response: PingHandler.Action = self.pingHandler.streamCreated()
  27. XCTAssertEqual(response, .schedulePing(delay: .seconds(1), timeout: .seconds(1)))
  28. // Stream closed
  29. response = self.pingHandler.streamClosed()
  30. XCTAssertEqual(response, .none)
  31. }
  32. func testClosingStreamWithPermitCalls() {
  33. // Allow pings without calls (since `minimumReceivedPingIntervalWithoutData` and `maximumPingStrikes` are not set, ping strikes should not have any effect)
  34. self.setupPingHandler(interval: .seconds(1), timeout: .seconds(1), permitWithoutCalls: true)
  35. // New stream created
  36. var response: PingHandler.Action = self.pingHandler.streamCreated()
  37. XCTAssertEqual(response, .schedulePing(delay: .seconds(1), timeout: .seconds(1)))
  38. // Stream closed
  39. response = self.pingHandler.streamClosed()
  40. XCTAssertEqual(response, .none)
  41. }
  42. func testIntervalWithCallInFlight() {
  43. // Do not allow pings without calls
  44. self.setupPingHandler(interval: .seconds(1), timeout: .seconds(1))
  45. // New stream created
  46. var response: PingHandler.Action = self.pingHandler.streamCreated()
  47. XCTAssertEqual(response, .schedulePing(delay: .seconds(1), timeout: .seconds(1)))
  48. // Move time to 1 second in the future
  49. self.pingHandler._testingOnlyNow = .now() + .seconds(1)
  50. // Send ping, which is valid
  51. response = self.pingHandler.pingFired()
  52. XCTAssertEqual(
  53. response,
  54. .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: false))
  55. )
  56. // Received valid pong, scheduled timeout should be cancelled
  57. response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: true)
  58. XCTAssertEqual(response, .cancelScheduledTimeout)
  59. // Stream closed
  60. response = self.pingHandler.streamClosed()
  61. XCTAssertEqual(response, .none)
  62. }
  63. func testIntervalWithoutCallsInFlight() {
  64. // Do not allow pings without calls
  65. self.setupPingHandler(interval: .seconds(1), timeout: .seconds(1))
  66. // Send ping, which is invalid
  67. let response: PingHandler.Action = self.pingHandler.pingFired()
  68. XCTAssertEqual(response, .none)
  69. }
  70. func testIntervalWithCallNoLongerInFlight() {
  71. // Do not allow pings without calls
  72. self.setupPingHandler(interval: .seconds(1), timeout: .seconds(1))
  73. // New stream created
  74. var response: PingHandler.Action = self.pingHandler.streamCreated()
  75. XCTAssertEqual(response, .schedulePing(delay: .seconds(1), timeout: .seconds(1)))
  76. // Stream closed
  77. response = self.pingHandler.streamClosed()
  78. XCTAssertEqual(response, .none)
  79. // Move time to 1 second in the future
  80. self.pingHandler._testingOnlyNow = .now() + .seconds(1)
  81. // Send ping, which is invalid
  82. response = self.pingHandler.pingFired()
  83. XCTAssertEqual(response, .none)
  84. }
  85. func testIntervalWithoutCallsInFlightButPermitted() {
  86. // Allow pings without calls (since `minimumReceivedPingIntervalWithoutData` and `maximumPingStrikes` are not set, ping strikes should not have any effect)
  87. self.setupPingHandler(interval: .seconds(1), timeout: .seconds(1), permitWithoutCalls: true)
  88. // Send ping, which is valid
  89. var response: PingHandler.Action = self.pingHandler.pingFired()
  90. XCTAssertEqual(
  91. response,
  92. .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: false))
  93. )
  94. // Received valid pong, scheduled timeout should be cancelled
  95. response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: true)
  96. XCTAssertEqual(response, .cancelScheduledTimeout)
  97. }
  98. func testIntervalWithCallNoLongerInFlightButPermitted() {
  99. // Allow pings without calls (since `minimumReceivedPingIntervalWithoutData` and `maximumPingStrikes` are not set, ping strikes should not have any effect)
  100. self.setupPingHandler(interval: .seconds(1), timeout: .seconds(1), permitWithoutCalls: true)
  101. // New stream created
  102. var response: PingHandler.Action = self.pingHandler.streamCreated()
  103. XCTAssertEqual(response, .schedulePing(delay: .seconds(1), timeout: .seconds(1)))
  104. // Stream closed
  105. response = self.pingHandler.streamClosed()
  106. XCTAssertEqual(response, .none)
  107. // Move time to 1 second in the future
  108. self.pingHandler._testingOnlyNow = .now() + .seconds(1)
  109. // Send ping, which is valid
  110. response = self.pingHandler.pingFired()
  111. XCTAssertEqual(
  112. response,
  113. .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: false))
  114. )
  115. // Received valid pong, scheduled timeout should be cancelled
  116. response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: true)
  117. XCTAssertEqual(response, .cancelScheduledTimeout)
  118. }
  119. func testIntervalTooEarlyWithCallInFlight() {
  120. // Do not allow pings without calls
  121. self.setupPingHandler(interval: .seconds(2), timeout: .seconds(1))
  122. // New stream created
  123. var response: PingHandler.Action = self.pingHandler.streamCreated()
  124. XCTAssertEqual(response, .schedulePing(delay: .seconds(2), timeout: .seconds(1)))
  125. // Send first ping
  126. response = self.pingHandler.pingFired()
  127. XCTAssertEqual(
  128. response,
  129. .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: false))
  130. )
  131. // Move time to 1 second in the future
  132. self.pingHandler._testingOnlyNow = .now() + .seconds(1)
  133. // Send another ping, which is valid since client do not check ping strikes
  134. response = self.pingHandler.pingFired()
  135. XCTAssertEqual(
  136. response,
  137. .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: false))
  138. )
  139. // Stream closed
  140. response = self.pingHandler.streamClosed()
  141. XCTAssertEqual(response, .none)
  142. }
  143. func testIntervalTooEarlyWithoutCallsInFlight() {
  144. // Allow pings without calls with a maximum pings of 2
  145. self.setupPingHandler(
  146. interval: .seconds(2),
  147. timeout: .seconds(1),
  148. permitWithoutCalls: true,
  149. maximumPingsWithoutData: 2,
  150. minimumSentPingIntervalWithoutData: .seconds(5)
  151. )
  152. // Send first ping
  153. var response: PingHandler.Action = self.pingHandler.pingFired()
  154. XCTAssertEqual(
  155. response,
  156. .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: false))
  157. )
  158. // Move time to 1 second in the future
  159. self.pingHandler._testingOnlyNow = .now() + .seconds(1)
  160. // Send another ping, but since `now` is less than the ping interval, response should be no action
  161. response = self.pingHandler.pingFired()
  162. XCTAssertEqual(response, .none)
  163. // Move time to 5 seconds in the future
  164. self.pingHandler._testingOnlyNow = .now() + .seconds(5)
  165. // Send another ping, which is valid since we waited `minimumSentPingIntervalWithoutData`
  166. response = self.pingHandler.pingFired()
  167. XCTAssertEqual(
  168. response,
  169. .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: false))
  170. )
  171. // Move time to 10 seconds in the future
  172. self.pingHandler._testingOnlyNow = .now() + .seconds(10)
  173. // Send another ping, which is valid since we waited `minimumSentPingIntervalWithoutData`
  174. response = self.pingHandler.pingFired()
  175. XCTAssertEqual(
  176. response,
  177. .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: false))
  178. )
  179. // Send another ping, but we've exceeded `maximumPingsWithoutData` so response should be no action
  180. response = self.pingHandler.pingFired()
  181. XCTAssertEqual(response, .none)
  182. // New stream created
  183. response = self.pingHandler.streamCreated()
  184. XCTAssertEqual(response, .schedulePing(delay: .seconds(2), timeout: .seconds(1)))
  185. // Send another ping, now that there is call, ping is valid
  186. response = self.pingHandler.pingFired()
  187. XCTAssertEqual(
  188. response,
  189. .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: false))
  190. )
  191. // Stream closed
  192. response = self.pingHandler.streamClosed()
  193. XCTAssertEqual(response, .none)
  194. }
  195. func testPingStrikesOnClientShouldHaveNoEffect() {
  196. // Allow pings without calls (since `minimumReceivedPingIntervalWithoutData` and `maximumPingStrikes` are not set, ping strikes should not have any effect)
  197. self.setupPingHandler(interval: .seconds(2), timeout: .seconds(1), permitWithoutCalls: true)
  198. // Received first ping, response should be a pong
  199. var response: PingHandler.Action = self.pingHandler.read(
  200. pingData: HTTP2PingData(withInteger: 1),
  201. ack: false
  202. )
  203. XCTAssertEqual(
  204. response,
  205. .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: true))
  206. )
  207. // Received another ping, response should be a pong (ping strikes not in effect)
  208. response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false)
  209. XCTAssertEqual(
  210. response,
  211. .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: true))
  212. )
  213. // Received another ping, response should be a pong (ping strikes not in effect)
  214. response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false)
  215. XCTAssertEqual(
  216. response,
  217. .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: true))
  218. )
  219. }
  220. func testPingWithoutDataResultsInPongForClient() {
  221. // Don't allow _sending_ pings when no calls are active (receiving pings should be tolerated).
  222. self.setupPingHandler(permitWithoutCalls: false)
  223. let action = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false)
  224. XCTAssertEqual(
  225. action,
  226. .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: true))
  227. )
  228. }
  229. func testPingWithoutDataResultsInPongForServer() {
  230. // Don't allow _sending_ pings when no calls are active (receiving pings should be tolerated).
  231. // Set 'minimumReceivedPingIntervalWithoutData' and 'maximumPingStrikes' so that we enable
  232. // support for ping strikes.
  233. self.setupPingHandler(
  234. permitWithoutCalls: false,
  235. minimumReceivedPingIntervalWithoutData: .seconds(5),
  236. maximumPingStrikes: 1
  237. )
  238. let action = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false)
  239. XCTAssertEqual(
  240. action,
  241. .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: true))
  242. )
  243. }
  244. func testPingStrikesOnServer() {
  245. // Set a maximum ping strikes of 1 without a minimum of 1 second between pings
  246. self.setupPingHandler(
  247. interval: .seconds(2),
  248. timeout: .seconds(1),
  249. permitWithoutCalls: true,
  250. minimumReceivedPingIntervalWithoutData: .seconds(1),
  251. maximumPingStrikes: 1
  252. )
  253. // Received first ping, response should be a pong
  254. var response: PingHandler.Action = self.pingHandler.read(
  255. pingData: HTTP2PingData(withInteger: 1),
  256. ack: false
  257. )
  258. XCTAssertEqual(
  259. response,
  260. .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: true))
  261. )
  262. // Received another ping, which is invalid (ping strike), response should be no action
  263. response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false)
  264. XCTAssertEqual(response, .none)
  265. // Move time to 2 seconds in the future
  266. self.pingHandler._testingOnlyNow = .now() + .seconds(2)
  267. // Received another ping, which is valid now, response should be a pong
  268. response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false)
  269. XCTAssertEqual(
  270. response,
  271. .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: true))
  272. )
  273. // Received another ping, which is invalid (ping strike), response should be no action
  274. response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false)
  275. XCTAssertEqual(response, .none)
  276. // Received another ping, which is invalid (ping strike), since number of ping strikes is over the limit, response should be go away
  277. response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false)
  278. XCTAssertEqual(
  279. response,
  280. .reply(HTTP2Frame.FramePayload.goAway(
  281. lastStreamID: .rootStream,
  282. errorCode: .enhanceYourCalm,
  283. opaqueData: nil
  284. ))
  285. )
  286. }
  287. private func setupPingHandler(
  288. pingCode: UInt64 = 1,
  289. interval: TimeAmount = .seconds(15),
  290. timeout: TimeAmount = .seconds(5),
  291. permitWithoutCalls: Bool = false,
  292. maximumPingsWithoutData: UInt = 2,
  293. minimumSentPingIntervalWithoutData: TimeAmount = .seconds(5),
  294. minimumReceivedPingIntervalWithoutData: TimeAmount? = nil,
  295. maximumPingStrikes: UInt? = nil
  296. ) {
  297. self.pingHandler = PingHandler(
  298. pingCode: pingCode,
  299. interval: interval,
  300. timeout: timeout,
  301. permitWithoutCalls: permitWithoutCalls,
  302. maximumPingsWithoutData: maximumPingsWithoutData,
  303. minimumSentPingIntervalWithoutData: minimumSentPingIntervalWithoutData,
  304. minimumReceivedPingIntervalWithoutData: minimumReceivedPingIntervalWithoutData,
  305. maximumPingStrikes: maximumPingStrikes
  306. )
  307. }
  308. }
  309. extension PingHandler.Action: Equatable {
  310. public static func == (lhs: PingHandler.Action, rhs: PingHandler.Action) -> Bool {
  311. switch (lhs, rhs) {
  312. case (.none, .none):
  313. return true
  314. case (let .schedulePing(lhsDelay, lhsTimeout), let .schedulePing(rhsDelay, rhsTimeout)):
  315. return lhsDelay == rhsDelay && lhsTimeout == rhsTimeout
  316. case (.cancelScheduledTimeout, .cancelScheduledTimeout):
  317. return true
  318. case let (.reply(lhsPayload), .reply(rhsPayload)):
  319. switch (lhsPayload, rhsPayload) {
  320. case (let .ping(lhsData, ack: lhsAck), let .ping(rhsData, ack: rhsAck)):
  321. return lhsData == rhsData && lhsAck == rhsAck
  322. case (let .goAway(_, lhsErrorCode, _), let .goAway(_, rhsErrorCode, _)):
  323. return lhsErrorCode == rhsErrorCode
  324. default:
  325. return false
  326. }
  327. default:
  328. return false
  329. }
  330. }
  331. }