2
0

GRPCPingHandlerTests.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  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. extension PingHandler.Action: Equatable {
  297. public static func == (lhs: PingHandler.Action, rhs: PingHandler.Action) -> Bool {
  298. switch (lhs, rhs) {
  299. case (.none, .none):
  300. return true
  301. case (.ack, .ack):
  302. return true
  303. case (let .schedulePing(lhsDelay, lhsTimeout), let .schedulePing(rhsDelay, rhsTimeout)):
  304. return lhsDelay == rhsDelay && lhsTimeout == rhsTimeout
  305. case (.cancelScheduledTimeout, .cancelScheduledTimeout):
  306. return true
  307. case (.ratchetDownLastSeenStreamID, .ratchetDownLastSeenStreamID):
  308. return true
  309. case let (.reply(lhsPayload), .reply(rhsPayload)):
  310. switch (lhsPayload, rhsPayload) {
  311. case (let .ping(lhsData, ack: lhsAck), let .ping(rhsData, ack: rhsAck)):
  312. return lhsData == rhsData && lhsAck == rhsAck
  313. case (let .goAway(_, lhsErrorCode, _), let .goAway(_, rhsErrorCode, _)):
  314. return lhsErrorCode == rhsErrorCode
  315. default:
  316. return false
  317. }
  318. default:
  319. return false
  320. }
  321. }
  322. }
  323. extension GRPCPingHandlerTests {
  324. func testSingleAckIsEmittedOnPing() throws {
  325. let client = EmbeddedChannel()
  326. _ = try client.configureHTTP2Pipeline(mode: .client) { _ in
  327. fatalError("Unexpected inbound stream")
  328. }.wait()
  329. let server = EmbeddedChannel()
  330. let serverMux = try server.configureHTTP2Pipeline(mode: .server) { _ in
  331. fatalError("Unexpected inbound stream")
  332. }.wait()
  333. let idleHandler = GRPCIdleHandler(
  334. idleTimeout: .minutes(5),
  335. keepalive: .init(),
  336. logger: self.serverLogger
  337. )
  338. try server.pipeline.syncOperations.addHandler(idleHandler, position: .before(serverMux))
  339. try server.connect(to: .init(unixDomainSocketPath: "/ignored")).wait()
  340. try client.connect(to: .init(unixDomainSocketPath: "/ignored")).wait()
  341. func interact(client: EmbeddedChannel, server: EmbeddedChannel) throws {
  342. var didRead = true
  343. while didRead {
  344. didRead = false
  345. if let data = try client.readOutbound(as: ByteBuffer.self) {
  346. didRead = true
  347. try server.writeInbound(data)
  348. }
  349. if let data = try server.readOutbound(as: ByteBuffer.self) {
  350. didRead = true
  351. try client.writeInbound(data)
  352. }
  353. }
  354. }
  355. try interact(client: client, server: server)
  356. // Settings.
  357. let f1 = try XCTUnwrap(client.readInbound(as: HTTP2Frame.self))
  358. f1.payload.assertSettings(ack: false)
  359. // Settings ack.
  360. let f2 = try XCTUnwrap(client.readInbound(as: HTTP2Frame.self))
  361. f2.payload.assertSettings(ack: true)
  362. // Send a ping.
  363. let ping = HTTP2Frame(streamID: .rootStream, payload: .ping(.init(withInteger: 42), ack: false))
  364. try client.writeOutbound(ping)
  365. try interact(client: client, server: server)
  366. // Ping ack.
  367. let f3 = try XCTUnwrap(client.readInbound(as: HTTP2Frame.self))
  368. f3.payload.assertPing(ack: true)
  369. XCTAssertNil(try client.readInbound(as: HTTP2Frame.self))
  370. }
  371. }
  372. extension HTTP2Frame.FramePayload {
  373. func assertSettings(ack: Bool, file: StaticString = #file, line: UInt = #line) {
  374. switch self {
  375. case let .settings(settings):
  376. switch settings {
  377. case .ack:
  378. XCTAssertTrue(ack, file: file, line: line)
  379. case .settings:
  380. XCTAssertFalse(ack, file: file, line: line)
  381. }
  382. default:
  383. XCTFail("Expected .settings got \(self)", file: file, line: line)
  384. }
  385. }
  386. func assertPing(ack: Bool, file: StaticString = #file, line: UInt = #line) {
  387. switch self {
  388. case let .ping(_, ack: pingAck):
  389. XCTAssertEqual(pingAck, ack, file: file, line: line)
  390. default:
  391. XCTFail("Expected .ping got \(self)", file: file, line: line)
  392. }
  393. }
  394. }