GRPCPingHandlerTests.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  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 NIOEmbedded
  18. import NIOHTTP2
  19. import XCTest
  20. @testable import GRPC
  21. class GRPCPingHandlerTests: GRPCTestCase {
  22. var pingHandler: PingHandler!
  23. func testClosingStreamWithoutPermitCalls() {
  24. // Do not allow pings without calls
  25. self.setupPingHandler(interval: .seconds(1), timeout: .seconds(1))
  26. // New stream created
  27. var response: PingHandler.Action = self.pingHandler.streamCreated()
  28. XCTAssertEqual(response, .schedulePing(delay: .seconds(1), timeout: .seconds(1)))
  29. // Stream closed
  30. response = self.pingHandler.streamClosed()
  31. XCTAssertEqual(response, .none)
  32. }
  33. func testClosingStreamWithPermitCalls() {
  34. // Allow pings without calls (since `minimumReceivedPingIntervalWithoutData` and `maximumPingStrikes` are not set, ping strikes should not have any effect)
  35. self.setupPingHandler(interval: .seconds(1), timeout: .seconds(1), permitWithoutCalls: true)
  36. // New stream created
  37. var response: PingHandler.Action = self.pingHandler.streamCreated()
  38. XCTAssertEqual(response, .schedulePing(delay: .seconds(1), timeout: .seconds(1)))
  39. // Stream closed
  40. response = self.pingHandler.streamClosed()
  41. XCTAssertEqual(response, .none)
  42. }
  43. func testIntervalWithCallInFlight() {
  44. // Do not allow pings without calls
  45. self.setupPingHandler(interval: .seconds(1), timeout: .seconds(1))
  46. // New stream created
  47. var response: PingHandler.Action = self.pingHandler.streamCreated()
  48. XCTAssertEqual(response, .schedulePing(delay: .seconds(1), timeout: .seconds(1)))
  49. // Move time to 1 second in the future
  50. self.pingHandler._testingOnlyNow = .now() + .seconds(1)
  51. // Send ping, which is valid
  52. response = self.pingHandler.pingFired()
  53. XCTAssertEqual(
  54. response,
  55. .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: false))
  56. )
  57. // Received valid pong, scheduled timeout should be cancelled
  58. response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: true)
  59. XCTAssertEqual(response, .cancelScheduledTimeout)
  60. // Stream closed
  61. response = self.pingHandler.streamClosed()
  62. XCTAssertEqual(response, .none)
  63. }
  64. func testIntervalWithoutCallsInFlight() {
  65. // Do not allow pings without calls
  66. self.setupPingHandler(interval: .seconds(1), timeout: .seconds(1))
  67. // Send ping, which is invalid
  68. let response: PingHandler.Action = self.pingHandler.pingFired()
  69. XCTAssertEqual(response, .none)
  70. }
  71. func testIntervalWithCallNoLongerInFlight() {
  72. // Do not allow pings without calls
  73. self.setupPingHandler(interval: .seconds(1), timeout: .seconds(1))
  74. // New stream created
  75. var response: PingHandler.Action = self.pingHandler.streamCreated()
  76. XCTAssertEqual(response, .schedulePing(delay: .seconds(1), timeout: .seconds(1)))
  77. // Stream closed
  78. response = self.pingHandler.streamClosed()
  79. XCTAssertEqual(response, .none)
  80. // Move time to 1 second in the future
  81. self.pingHandler._testingOnlyNow = .now() + .seconds(1)
  82. // Send ping, which is invalid
  83. response = self.pingHandler.pingFired()
  84. XCTAssertEqual(response, .none)
  85. }
  86. func testIntervalWithoutCallsInFlightButPermitted() {
  87. // Allow pings without calls (since `minimumReceivedPingIntervalWithoutData` and `maximumPingStrikes` are not set, ping strikes should not have any effect)
  88. self.setupPingHandler(interval: .seconds(1), timeout: .seconds(1), permitWithoutCalls: true)
  89. // Send ping, which is valid
  90. var response: PingHandler.Action = self.pingHandler.pingFired()
  91. XCTAssertEqual(
  92. response,
  93. .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: false))
  94. )
  95. // Received valid pong, scheduled timeout should be cancelled
  96. response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: true)
  97. XCTAssertEqual(response, .cancelScheduledTimeout)
  98. }
  99. func testIntervalWithCallNoLongerInFlightButPermitted() {
  100. // Allow pings without calls (since `minimumReceivedPingIntervalWithoutData` and `maximumPingStrikes` are not set, ping strikes should not have any effect)
  101. self.setupPingHandler(interval: .seconds(1), timeout: .seconds(1), permitWithoutCalls: true)
  102. // New stream created
  103. var response: PingHandler.Action = self.pingHandler.streamCreated()
  104. XCTAssertEqual(response, .schedulePing(delay: .seconds(1), timeout: .seconds(1)))
  105. // Stream closed
  106. response = self.pingHandler.streamClosed()
  107. XCTAssertEqual(response, .none)
  108. // Move time to 1 second in the future
  109. self.pingHandler._testingOnlyNow = .now() + .seconds(1)
  110. // Send ping, which is valid
  111. response = self.pingHandler.pingFired()
  112. XCTAssertEqual(
  113. response,
  114. .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: false))
  115. )
  116. // Received valid pong, scheduled timeout should be cancelled
  117. response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: true)
  118. XCTAssertEqual(response, .cancelScheduledTimeout)
  119. }
  120. func testIntervalTooEarlyWithCallInFlight() {
  121. // Do not allow pings without calls
  122. self.setupPingHandler(interval: .seconds(2), timeout: .seconds(1))
  123. // New stream created
  124. var response: PingHandler.Action = self.pingHandler.streamCreated()
  125. XCTAssertEqual(response, .schedulePing(delay: .seconds(2), timeout: .seconds(1)))
  126. // Send first ping
  127. response = self.pingHandler.pingFired()
  128. XCTAssertEqual(
  129. response,
  130. .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: false))
  131. )
  132. // Move time to 1 second in the future
  133. self.pingHandler._testingOnlyNow = .now() + .seconds(1)
  134. // Send another ping, which is valid since client do not check ping strikes
  135. response = self.pingHandler.pingFired()
  136. XCTAssertEqual(
  137. response,
  138. .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: false))
  139. )
  140. // Stream closed
  141. response = self.pingHandler.streamClosed()
  142. XCTAssertEqual(response, .none)
  143. }
  144. func testIntervalTooEarlyWithoutCallsInFlight() {
  145. // Allow pings without calls with a maximum pings of 2
  146. self.setupPingHandler(
  147. interval: .seconds(2),
  148. timeout: .seconds(1),
  149. permitWithoutCalls: true,
  150. maximumPingsWithoutData: 2,
  151. minimumSentPingIntervalWithoutData: .seconds(5)
  152. )
  153. // Send first ping
  154. var response: PingHandler.Action = self.pingHandler.pingFired()
  155. XCTAssertEqual(
  156. response,
  157. .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: false))
  158. )
  159. // Move time to 1 second in the future
  160. self.pingHandler._testingOnlyNow = .now() + .seconds(1)
  161. // Send another ping, but since `now` is less than the ping interval, response should be no action
  162. response = self.pingHandler.pingFired()
  163. XCTAssertEqual(response, .none)
  164. // Move time to 5 seconds in the future
  165. self.pingHandler._testingOnlyNow = .now() + .seconds(5)
  166. // Send another ping, which is valid since we waited `minimumSentPingIntervalWithoutData`
  167. response = self.pingHandler.pingFired()
  168. XCTAssertEqual(
  169. response,
  170. .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: false))
  171. )
  172. // Move time to 10 seconds in the future
  173. self.pingHandler._testingOnlyNow = .now() + .seconds(10)
  174. // Send another ping, which is valid since we waited `minimumSentPingIntervalWithoutData`
  175. response = self.pingHandler.pingFired()
  176. XCTAssertEqual(
  177. response,
  178. .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: false))
  179. )
  180. // Send another ping, but we've exceeded `maximumPingsWithoutData` so response should be no action
  181. response = self.pingHandler.pingFired()
  182. XCTAssertEqual(response, .none)
  183. // New stream created
  184. response = self.pingHandler.streamCreated()
  185. XCTAssertEqual(response, .schedulePing(delay: .seconds(2), timeout: .seconds(1)))
  186. // Send another ping, now that there is call, ping is valid
  187. response = self.pingHandler.pingFired()
  188. XCTAssertEqual(
  189. response,
  190. .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: false))
  191. )
  192. // Stream closed
  193. response = self.pingHandler.streamClosed()
  194. XCTAssertEqual(response, .none)
  195. }
  196. func testPingStrikesOnClientShouldHaveNoEffect() {
  197. // Allow pings without calls (since `minimumReceivedPingIntervalWithoutData` and `maximumPingStrikes` are not set, ping strikes should not have any effect)
  198. self.setupPingHandler(interval: .seconds(2), timeout: .seconds(1), permitWithoutCalls: true)
  199. // Received first ping, response should be a pong
  200. var response: PingHandler.Action = self.pingHandler.read(
  201. pingData: HTTP2PingData(withInteger: 1),
  202. ack: false
  203. )
  204. XCTAssertEqual(response, .ack)
  205. // Received another ping, response should be a pong (ping strikes not in effect)
  206. response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false)
  207. XCTAssertEqual(response, .ack)
  208. // Received another ping, response should be a pong (ping strikes not in effect)
  209. response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false)
  210. XCTAssertEqual(response, .ack)
  211. }
  212. func testPingWithoutDataResultsInPongForClient() {
  213. // Don't allow _sending_ pings when no calls are active (receiving pings should be tolerated).
  214. self.setupPingHandler(permitWithoutCalls: false)
  215. let action = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false)
  216. XCTAssertEqual(action, .ack)
  217. }
  218. func testPingWithoutDataResultsInPongForServer() {
  219. // Don't allow _sending_ pings when no calls are active (receiving pings should be tolerated).
  220. // Set 'minimumReceivedPingIntervalWithoutData' and 'maximumPingStrikes' so that we enable
  221. // support for ping strikes.
  222. self.setupPingHandler(
  223. permitWithoutCalls: false,
  224. minimumReceivedPingIntervalWithoutData: .seconds(5),
  225. maximumPingStrikes: 1
  226. )
  227. let action = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false)
  228. XCTAssertEqual(action, .ack)
  229. }
  230. func testPingStrikesOnServer() {
  231. // Set a maximum ping strikes of 1 without a minimum of 1 second between pings
  232. self.setupPingHandler(
  233. interval: .seconds(2),
  234. timeout: .seconds(1),
  235. permitWithoutCalls: true,
  236. minimumReceivedPingIntervalWithoutData: .seconds(1),
  237. maximumPingStrikes: 1
  238. )
  239. // Received first ping, response should be a pong
  240. var response: PingHandler.Action = self.pingHandler.read(
  241. pingData: HTTP2PingData(withInteger: 1),
  242. ack: false
  243. )
  244. XCTAssertEqual(response, .ack)
  245. // Received another ping, which is invalid (ping strike), response should be no action
  246. response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false)
  247. XCTAssertEqual(response, .none)
  248. // Move time to 2 seconds in the future
  249. self.pingHandler._testingOnlyNow = .now() + .seconds(2)
  250. // Received another ping, which is valid now, response should be a pong
  251. response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false)
  252. XCTAssertEqual(response, .ack)
  253. // Received another ping, which is invalid (ping strike), response should be no action
  254. response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false)
  255. XCTAssertEqual(response, .none)
  256. // Received another ping, which is invalid (ping strike), since number of ping strikes is over the limit, response should be go away
  257. response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false)
  258. XCTAssertEqual(
  259. response,
  260. .reply(
  261. HTTP2Frame.FramePayload.goAway(
  262. lastStreamID: .rootStream,
  263. errorCode: .enhanceYourCalm,
  264. opaqueData: nil
  265. )
  266. )
  267. )
  268. }
  269. func testPongWithGoAwayPingData() {
  270. self.setupPingHandler()
  271. let response = self.pingHandler.read(pingData: self.pingHandler.pingDataGoAway, ack: true)
  272. XCTAssertEqual(response, .ratchetDownLastSeenStreamID)
  273. }
  274. private func setupPingHandler(
  275. pingCode: UInt64 = 1,
  276. interval: TimeAmount = .seconds(15),
  277. timeout: TimeAmount = .seconds(5),
  278. permitWithoutCalls: Bool = false,
  279. maximumPingsWithoutData: UInt = 2,
  280. minimumSentPingIntervalWithoutData: TimeAmount = .seconds(5),
  281. minimumReceivedPingIntervalWithoutData: TimeAmount? = nil,
  282. maximumPingStrikes: UInt? = nil
  283. ) {
  284. self.pingHandler = PingHandler(
  285. pingCode: pingCode,
  286. interval: interval,
  287. timeout: timeout,
  288. permitWithoutCalls: permitWithoutCalls,
  289. maximumPingsWithoutData: maximumPingsWithoutData,
  290. minimumSentPingIntervalWithoutData: minimumSentPingIntervalWithoutData,
  291. minimumReceivedPingIntervalWithoutData: minimumReceivedPingIntervalWithoutData,
  292. maximumPingStrikes: maximumPingStrikes
  293. )
  294. }
  295. }
  296. #if compiler(>=6.0)
  297. extension PingHandler.Action: @retroactive Equatable {}
  298. #else
  299. extension PingHandler.Action: Equatable {}
  300. #endif
  301. extension PingHandler.Action {
  302. public static func == (lhs: PingHandler.Action, rhs: PingHandler.Action) -> Bool {
  303. switch (lhs, rhs) {
  304. case (.none, .none):
  305. return true
  306. case (.ack, .ack):
  307. return true
  308. case (let .schedulePing(lhsDelay, lhsTimeout), let .schedulePing(rhsDelay, rhsTimeout)):
  309. return lhsDelay == rhsDelay && lhsTimeout == rhsTimeout
  310. case (.cancelScheduledTimeout, .cancelScheduledTimeout):
  311. return true
  312. case (.ratchetDownLastSeenStreamID, .ratchetDownLastSeenStreamID):
  313. return true
  314. case let (.reply(lhsPayload), .reply(rhsPayload)):
  315. switch (lhsPayload, rhsPayload) {
  316. case (let .ping(lhsData, ack: lhsAck), let .ping(rhsData, ack: rhsAck)):
  317. return lhsData == rhsData && lhsAck == rhsAck
  318. case (let .goAway(_, lhsErrorCode, _), let .goAway(_, rhsErrorCode, _)):
  319. return lhsErrorCode == rhsErrorCode
  320. default:
  321. return false
  322. }
  323. default:
  324. return false
  325. }
  326. }
  327. }
  328. extension GRPCPingHandlerTests {
  329. func testSingleAckIsEmittedOnPing() throws {
  330. let client = EmbeddedChannel()
  331. _ = try client.configureHTTP2Pipeline(mode: .client) { _ in
  332. fatalError("Unexpected inbound stream")
  333. }.wait()
  334. let server = EmbeddedChannel()
  335. let serverMux = try server.configureHTTP2Pipeline(mode: .server) { _ in
  336. fatalError("Unexpected inbound stream")
  337. }.wait()
  338. let idleHandler = GRPCIdleHandler(
  339. idleTimeout: .minutes(5),
  340. keepalive: .init(),
  341. logger: self.serverLogger
  342. )
  343. try server.pipeline.syncOperations.addHandler(idleHandler, position: .before(serverMux))
  344. try server.connect(to: .init(unixDomainSocketPath: "/ignored")).wait()
  345. try client.connect(to: .init(unixDomainSocketPath: "/ignored")).wait()
  346. func interact(client: EmbeddedChannel, server: EmbeddedChannel) throws {
  347. var didRead = true
  348. while didRead {
  349. didRead = false
  350. if let data = try client.readOutbound(as: ByteBuffer.self) {
  351. didRead = true
  352. try server.writeInbound(data)
  353. }
  354. if let data = try server.readOutbound(as: ByteBuffer.self) {
  355. didRead = true
  356. try client.writeInbound(data)
  357. }
  358. }
  359. }
  360. try interact(client: client, server: server)
  361. // Settings.
  362. let f1 = try XCTUnwrap(client.readInbound(as: HTTP2Frame.self))
  363. f1.payload.assertSettings(ack: false)
  364. // Settings ack.
  365. let f2 = try XCTUnwrap(client.readInbound(as: HTTP2Frame.self))
  366. f2.payload.assertSettings(ack: true)
  367. // Send a ping.
  368. let ping = HTTP2Frame(streamID: .rootStream, payload: .ping(.init(withInteger: 42), ack: false))
  369. try client.writeOutbound(ping)
  370. try interact(client: client, server: server)
  371. // Ping ack.
  372. let f3 = try XCTUnwrap(client.readInbound(as: HTTP2Frame.self))
  373. f3.payload.assertPing(ack: true)
  374. XCTAssertNil(try client.readInbound(as: HTTP2Frame.self))
  375. }
  376. }
  377. extension HTTP2Frame.FramePayload {
  378. func assertSettings(ack: Bool, file: StaticString = #file, line: UInt = #line) {
  379. switch self {
  380. case let .settings(settings):
  381. switch settings {
  382. case .ack:
  383. XCTAssertTrue(ack, file: file, line: line)
  384. case .settings:
  385. XCTAssertFalse(ack, file: file, line: line)
  386. }
  387. default:
  388. XCTFail("Expected .settings got \(self)", file: file, line: line)
  389. }
  390. }
  391. func assertPing(ack: Bool, file: StaticString = #file, line: UInt = #line) {
  392. switch self {
  393. case let .ping(_, ack: pingAck):
  394. XCTAssertEqual(pingAck, ack, file: file, line: line)
  395. default:
  396. XCTFail("Expected .ping got \(self)", file: file, line: line)
  397. }
  398. }
  399. }