GRPCPingHandlerTests.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  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 NIO
  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(response, .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: false)))
  53. // Received valid pong, scheduled timeout should be cancelled
  54. response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: true)
  55. XCTAssertEqual(response, .cancelScheduledTimeout)
  56. // Stream closed
  57. response = self.pingHandler.streamClosed()
  58. XCTAssertEqual(response, .none)
  59. }
  60. func testIntervalWithoutCallsInFlight() {
  61. // Do not allow pings without calls
  62. self.setupPingHandler(interval: .seconds(1), timeout: .seconds(1))
  63. // Send ping, which is invalid
  64. let response: PingHandler.Action = self.pingHandler.pingFired()
  65. XCTAssertEqual(response, .none)
  66. }
  67. func testIntervalWithCallNoLongerInFlight() {
  68. // Do not allow pings without calls
  69. self.setupPingHandler(interval: .seconds(1), timeout: .seconds(1))
  70. // New stream created
  71. var response: PingHandler.Action = self.pingHandler.streamCreated()
  72. XCTAssertEqual(response, .schedulePing(delay: .seconds(1), timeout: .seconds(1)))
  73. // Stream closed
  74. response = self.pingHandler.streamClosed()
  75. XCTAssertEqual(response, .none)
  76. // Move time to 1 second in the future
  77. self.pingHandler._testingOnlyNow = .now() + .seconds(1)
  78. // Send ping, which is invalid
  79. response = self.pingHandler.pingFired()
  80. XCTAssertEqual(response, .none)
  81. }
  82. func testIntervalWithoutCallsInFlightButPermitted() {
  83. // Allow pings without calls (since `minimumReceivedPingIntervalWithoutData` and `maximumPingStrikes` are not set, ping strikes should not have any effect)
  84. self.setupPingHandler(interval: .seconds(1), timeout: .seconds(1), permitWithoutCalls: true)
  85. // Send ping, which is valid
  86. var response: PingHandler.Action = self.pingHandler.pingFired()
  87. XCTAssertEqual(response, .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: false)))
  88. // Received valid pong, scheduled timeout should be cancelled
  89. response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: true)
  90. XCTAssertEqual(response, .cancelScheduledTimeout)
  91. }
  92. func testIntervalWithCallNoLongerInFlightButPermitted() {
  93. // Allow pings without calls (since `minimumReceivedPingIntervalWithoutData` and `maximumPingStrikes` are not set, ping strikes should not have any effect)
  94. self.setupPingHandler(interval: .seconds(1), timeout: .seconds(1), permitWithoutCalls: true)
  95. // New stream created
  96. var response: PingHandler.Action = self.pingHandler.streamCreated()
  97. XCTAssertEqual(response, .schedulePing(delay: .seconds(1), timeout: .seconds(1)))
  98. // Stream closed
  99. response = self.pingHandler.streamClosed()
  100. XCTAssertEqual(response, .none)
  101. // Move time to 1 second in the future
  102. self.pingHandler._testingOnlyNow = .now() + .seconds(1)
  103. // Send ping, which is valid
  104. response = self.pingHandler.pingFired()
  105. XCTAssertEqual(response, .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: false)))
  106. // Received valid pong, scheduled timeout should be cancelled
  107. response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: true)
  108. XCTAssertEqual(response, .cancelScheduledTimeout)
  109. }
  110. func testIntervalTooEarlyWithCallInFlight() {
  111. // Do not allow pings without calls
  112. self.setupPingHandler(interval: .seconds(2), timeout: .seconds(1))
  113. // New stream created
  114. var response: PingHandler.Action = self.pingHandler.streamCreated()
  115. XCTAssertEqual(response, .schedulePing(delay: .seconds(2), timeout: .seconds(1)))
  116. // Send first ping
  117. response = self.pingHandler.pingFired()
  118. XCTAssertEqual(response, .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: false)))
  119. // Move time to 1 second in the future
  120. self.pingHandler._testingOnlyNow = .now() + .seconds(1)
  121. // Send another ping, which is valid since client do not check ping strikes
  122. response = self.pingHandler.pingFired()
  123. XCTAssertEqual(response, .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: false)))
  124. // Stream closed
  125. response = self.pingHandler.streamClosed()
  126. XCTAssertEqual(response, .none)
  127. }
  128. func testIntervalTooEarlyWithoutCallsInFlight() {
  129. // Allow pings without calls with a maximum pings of 2
  130. self.setupPingHandler(interval: .seconds(2), timeout: .seconds(1), permitWithoutCalls: true, maximumPingsWithoutData: 2, minimumSentPingIntervalWithoutData: .seconds(5))
  131. // Send first ping
  132. var response: PingHandler.Action = self.pingHandler.pingFired()
  133. XCTAssertEqual(response, .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: false)))
  134. // Move time to 1 second in the future
  135. self.pingHandler._testingOnlyNow = .now() + .seconds(1)
  136. // Send another ping, but since `now` is less than the ping interval, response should be no action
  137. response = self.pingHandler.pingFired()
  138. XCTAssertEqual(response, .none)
  139. // Move time to 5 seconds in the future
  140. self.pingHandler._testingOnlyNow = .now() + .seconds(5)
  141. // Send another ping, which is valid since we waited `minimumSentPingIntervalWithoutData`
  142. response = self.pingHandler.pingFired()
  143. XCTAssertEqual(response, .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: false)))
  144. // Move time to 10 seconds in the future
  145. self.pingHandler._testingOnlyNow = .now() + .seconds(10)
  146. // Send another ping, which is valid since we waited `minimumSentPingIntervalWithoutData`
  147. response = self.pingHandler.pingFired()
  148. XCTAssertEqual(response, .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: false)))
  149. // Send another ping, but we've exceeded `maximumPingsWithoutData` so response should be no action
  150. response = self.pingHandler.pingFired()
  151. XCTAssertEqual(response, .none)
  152. // New stream created
  153. response = self.pingHandler.streamCreated()
  154. XCTAssertEqual(response, .schedulePing(delay: .seconds(2), timeout: .seconds(1)))
  155. // Send another ping, now that there is call, ping is valid
  156. response = self.pingHandler.pingFired()
  157. XCTAssertEqual(response, .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: false)))
  158. // Stream closed
  159. response = self.pingHandler.streamClosed()
  160. XCTAssertEqual(response, .none)
  161. }
  162. func testPingStrikesOnClientShouldHaveNoEffect() {
  163. // Allow pings without calls (since `minimumReceivedPingIntervalWithoutData` and `maximumPingStrikes` are not set, ping strikes should not have any effect)
  164. self.setupPingHandler(interval: .seconds(2), timeout: .seconds(1), permitWithoutCalls: true)
  165. // Received first ping, response should be a pong
  166. var response: PingHandler.Action = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false)
  167. XCTAssertEqual(response, .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: true)))
  168. // Received another ping, response should be a pong (ping strikes not in effect)
  169. response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false)
  170. XCTAssertEqual(response, .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: true)))
  171. // Received another ping, response should be a pong (ping strikes not in effect)
  172. response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false)
  173. XCTAssertEqual(response, .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: true)))
  174. }
  175. func testPingWithoutDataResultsInPongForClient() {
  176. // Don't allow _sending_ pings when no calls are active (receiving pings should be tolerated).
  177. self.setupPingHandler(permitWithoutCalls: false)
  178. let action = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false)
  179. XCTAssertEqual(action, .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: true)))
  180. }
  181. func testPingWithoutDataResultsInPongForServer() {
  182. // Don't allow _sending_ pings when no calls are active (receiving pings should be tolerated).
  183. // Set 'minimumReceivedPingIntervalWithoutData' and 'maximumPingStrikes' so that we enable
  184. // support for ping strikes.
  185. self.setupPingHandler(
  186. permitWithoutCalls: false,
  187. minimumReceivedPingIntervalWithoutData: .seconds(5),
  188. maximumPingStrikes: 1
  189. )
  190. let action = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false)
  191. XCTAssertEqual(action, .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: true)))
  192. }
  193. func testPingStrikesOnServer() {
  194. // Set a maximum ping strikes of 1 without a minimum of 1 second between pings
  195. self.setupPingHandler(interval: .seconds(2), timeout: .seconds(1), permitWithoutCalls: true, minimumReceivedPingIntervalWithoutData: .seconds(1), maximumPingStrikes: 1)
  196. // Received first ping, response should be a pong
  197. var response: PingHandler.Action = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false)
  198. XCTAssertEqual(response, .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: true)))
  199. // Received another ping, which is invalid (ping strike), response should be no action
  200. response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false)
  201. XCTAssertEqual(response, .none)
  202. // Move time to 2 seconds in the future
  203. self.pingHandler._testingOnlyNow = .now() + .seconds(2)
  204. // Received another ping, which is valid now, response should be a pong
  205. response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false)
  206. XCTAssertEqual(response, .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: true)))
  207. // Received another ping, which is invalid (ping strike), response should be no action
  208. response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false)
  209. XCTAssertEqual(response, .none)
  210. // Received another ping, which is invalid (ping strike), since number of ping strikes is over the limit, response should be go away
  211. response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false)
  212. XCTAssertEqual(response, .reply(HTTP2Frame.FramePayload.goAway(lastStreamID: .rootStream, errorCode: .enhanceYourCalm, opaqueData: nil)))
  213. }
  214. private func setupPingHandler(
  215. pingCode: UInt64 = 1,
  216. interval: TimeAmount = .seconds(15),
  217. timeout: TimeAmount = .seconds(5),
  218. permitWithoutCalls: Bool = false,
  219. maximumPingsWithoutData: UInt = 2,
  220. minimumSentPingIntervalWithoutData: TimeAmount = .seconds(5),
  221. minimumReceivedPingIntervalWithoutData: TimeAmount? = nil,
  222. maximumPingStrikes: UInt? = nil
  223. ) {
  224. self.pingHandler = PingHandler(
  225. pingCode: pingCode,
  226. interval: interval,
  227. timeout: timeout,
  228. permitWithoutCalls: permitWithoutCalls,
  229. maximumPingsWithoutData: maximumPingsWithoutData,
  230. minimumSentPingIntervalWithoutData: minimumSentPingIntervalWithoutData,
  231. minimumReceivedPingIntervalWithoutData: minimumReceivedPingIntervalWithoutData,
  232. maximumPingStrikes: maximumPingStrikes)
  233. }
  234. }
  235. extension PingHandler.Action: Equatable {
  236. public static func == (lhs: PingHandler.Action, rhs: PingHandler.Action) -> Bool {
  237. switch (lhs, rhs) {
  238. case (.none, .none):
  239. return true
  240. case (let .schedulePing(lhsDelay, lhsTimeout), let .schedulePing(rhsDelay, rhsTimeout)):
  241. return lhsDelay == rhsDelay && lhsTimeout == rhsTimeout
  242. case (.cancelScheduledTimeout, .cancelScheduledTimeout):
  243. return true
  244. case (let .reply(lhsPayload), let .reply(rhsPayload)):
  245. switch (lhsPayload, rhsPayload) {
  246. case (let .ping(lhsData, ack: lhsAck), let .ping(rhsData, ack: rhsAck)):
  247. return lhsData == rhsData && lhsAck == rhsAck
  248. case (let .goAway(_, lhsErrorCode, _), let .goAway(_, rhsErrorCode, _)):
  249. return lhsErrorCode == rhsErrorCode
  250. default:
  251. return false
  252. }
  253. default:
  254. return false
  255. }
  256. }
  257. }