GRPCPingHandlerTests.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  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 testPingStrikesOnServer() {
  176. // Set a maximum ping strikes of 1 without a minimum of 1 second between pings
  177. self.setupPingHandler(interval: .seconds(2), timeout: .seconds(1), permitWithoutCalls: true, minimumReceivedPingIntervalWithoutData: .seconds(1), maximumPingStrikes: 1)
  178. // Received first ping, response should be a pong
  179. var response: PingHandler.Action = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false)
  180. XCTAssertEqual(response, .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: true)))
  181. // Received another ping, which is invalid (ping strike), response should be no action
  182. response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false)
  183. XCTAssertEqual(response, .none)
  184. // Move time to 2 seconds in the future
  185. self.pingHandler._testingOnlyNow = .now() + .seconds(2)
  186. // Received another ping, which is valid now, response should be a pong
  187. response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false)
  188. XCTAssertEqual(response, .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: true)))
  189. // Received another ping, which is invalid (ping strike), response should be no action
  190. response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false)
  191. XCTAssertEqual(response, .none)
  192. // Received another ping, which is invalid (ping strike), since number of ping strikes is over the limit, response should be go away
  193. response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false)
  194. XCTAssertEqual(response, .reply(HTTP2Frame.FramePayload.goAway(lastStreamID: .rootStream, errorCode: .enhanceYourCalm, opaqueData: nil)))
  195. }
  196. private func setupPingHandler(
  197. pingCode: UInt64 = 1,
  198. interval: TimeAmount = .seconds(15),
  199. timeout: TimeAmount = .seconds(5),
  200. permitWithoutCalls: Bool = false,
  201. maximumPingsWithoutData: UInt = 2,
  202. minimumSentPingIntervalWithoutData: TimeAmount = .seconds(5),
  203. minimumReceivedPingIntervalWithoutData: TimeAmount? = nil,
  204. maximumPingStrikes: UInt? = nil
  205. ) {
  206. self.pingHandler = PingHandler(
  207. pingCode: pingCode,
  208. interval: interval,
  209. timeout: timeout,
  210. permitWithoutCalls: permitWithoutCalls,
  211. maximumPingsWithoutData: maximumPingsWithoutData,
  212. minimumSentPingIntervalWithoutData: minimumSentPingIntervalWithoutData,
  213. minimumReceivedPingIntervalWithoutData: minimumReceivedPingIntervalWithoutData,
  214. maximumPingStrikes: maximumPingStrikes)
  215. }
  216. }
  217. extension PingHandler.Action: Equatable {
  218. public static func == (lhs: PingHandler.Action, rhs: PingHandler.Action) -> Bool {
  219. switch (lhs, rhs) {
  220. case (.none, .none):
  221. return true
  222. case (let .schedulePing(lhsDelay, lhsTimeout), let .schedulePing(rhsDelay, rhsTimeout)):
  223. return lhsDelay == rhsDelay && lhsTimeout == rhsTimeout
  224. case (.cancelScheduledTimeout, .cancelScheduledTimeout):
  225. return true
  226. case (let .reply(lhsPayload), let .reply(rhsPayload)):
  227. switch (lhsPayload, rhsPayload) {
  228. case (let .ping(lhsData, ack: lhsAck), let .ping(rhsData, ack: rhsAck)):
  229. return lhsData == rhsData && lhsAck == rhsAck
  230. case (let .goAway(_, lhsErrorCode, _), let .goAway(_, rhsErrorCode, _)):
  231. return lhsErrorCode == rhsErrorCode
  232. default:
  233. return false
  234. }
  235. default:
  236. return false
  237. }
  238. }
  239. }