ServerConnectionManagementHandler+StateMachineTests.swift 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. /*
  2. * Copyright 2024, 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 NIOHTTP2
  18. import XCTest
  19. @testable import GRPCHTTP2Core
  20. final class ServerConnectionManagementHandlerStateMachineTests: XCTestCase {
  21. private func makeStateMachine(
  22. allowKeepaliveWithoutCalls: Bool = false,
  23. minPingReceiveIntervalWithoutCalls: TimeAmount = .minutes(5),
  24. goAwayPingData: HTTP2PingData = HTTP2PingData(withInteger: 42)
  25. ) -> ServerConnectionManagementHandler.StateMachine {
  26. return .init(
  27. allowKeepaliveWithoutCalls: allowKeepaliveWithoutCalls,
  28. minPingReceiveIntervalWithoutCalls: minPingReceiveIntervalWithoutCalls,
  29. goAwayPingData: goAwayPingData
  30. )
  31. }
  32. func testCloseAllStreamsWhenActive() {
  33. var state = self.makeStateMachine()
  34. state.streamOpened(1)
  35. XCTAssertEqual(state.streamClosed(1), .startIdleTimer)
  36. }
  37. func testCloseSomeStreamsWhenActive() {
  38. var state = self.makeStateMachine()
  39. state.streamOpened(1)
  40. state.streamOpened(2)
  41. XCTAssertEqual(state.streamClosed(2), .none)
  42. }
  43. func testOpenAndCloseStreamWhenClosed() {
  44. var state = self.makeStateMachine()
  45. state.markClosed()
  46. state.streamOpened(1)
  47. XCTAssertEqual(state.streamClosed(1), .none)
  48. }
  49. func testGracefulShutdownWhenNoOpenStreams() {
  50. let pingData = HTTP2PingData(withInteger: 42)
  51. var state = self.makeStateMachine(goAwayPingData: pingData)
  52. XCTAssertEqual(state.startGracefulShutdown(), .sendGoAwayAndPing(pingData))
  53. }
  54. func testGracefulShutdownWhenClosing() {
  55. let pingData = HTTP2PingData(withInteger: 42)
  56. var state = self.makeStateMachine(goAwayPingData: pingData)
  57. XCTAssertEqual(state.startGracefulShutdown(), .sendGoAwayAndPing(pingData))
  58. XCTAssertEqual(state.startGracefulShutdown(), .none)
  59. }
  60. func testGracefulShutdownWhenClosed() {
  61. let pingData = HTTP2PingData(withInteger: 42)
  62. var state = self.makeStateMachine(goAwayPingData: pingData)
  63. state.markClosed()
  64. XCTAssertEqual(state.startGracefulShutdown(), .none)
  65. }
  66. func testReceiveAckForGoAwayPingWhenStreamsOpenedBeforeShutdownOnly() {
  67. let pingData = HTTP2PingData(withInteger: 42)
  68. var state = self.makeStateMachine(goAwayPingData: pingData)
  69. state.streamOpened(1)
  70. XCTAssertEqual(state.startGracefulShutdown(), .sendGoAwayAndPing(pingData))
  71. XCTAssertEqual(
  72. state.receivedPingAck(data: pingData),
  73. .sendGoAway(lastStreamID: 1, close: false)
  74. )
  75. }
  76. func testReceiveAckForGoAwayPingWhenStreamsOpenedBeforeAck() {
  77. let pingData = HTTP2PingData(withInteger: 42)
  78. var state = self.makeStateMachine(goAwayPingData: pingData)
  79. XCTAssertEqual(state.startGracefulShutdown(), .sendGoAwayAndPing(pingData))
  80. state.streamOpened(1)
  81. XCTAssertEqual(
  82. state.receivedPingAck(data: pingData),
  83. .sendGoAway(lastStreamID: 1, close: false)
  84. )
  85. }
  86. func testReceiveAckForGoAwayPingWhenNoOpenStreams() {
  87. let pingData = HTTP2PingData(withInteger: 42)
  88. var state = self.makeStateMachine(goAwayPingData: pingData)
  89. XCTAssertEqual(state.startGracefulShutdown(), .sendGoAwayAndPing(pingData))
  90. XCTAssertEqual(
  91. state.receivedPingAck(data: pingData),
  92. .sendGoAway(lastStreamID: .rootStream, close: true)
  93. )
  94. }
  95. func testReceiveAckNotForGoAwayPing() {
  96. let pingData = HTTP2PingData(withInteger: 42)
  97. var state = self.makeStateMachine(goAwayPingData: pingData)
  98. XCTAssertEqual(state.startGracefulShutdown(), .sendGoAwayAndPing(pingData))
  99. let otherPingData = HTTP2PingData(withInteger: 0)
  100. XCTAssertEqual(state.receivedPingAck(data: otherPingData), .none)
  101. }
  102. func testReceivePingAckWhenActive() {
  103. var state = self.makeStateMachine()
  104. XCTAssertEqual(state.receivedPingAck(data: HTTP2PingData()), .none)
  105. }
  106. func testReceivePingAckWhenClosed() {
  107. var state = self.makeStateMachine()
  108. state.markClosed()
  109. XCTAssertEqual(state.receivedPingAck(data: HTTP2PingData()), .none)
  110. }
  111. func testGracefulShutdownFlow() {
  112. var state = self.makeStateMachine()
  113. // Open a few streams.
  114. state.streamOpened(1)
  115. state.streamOpened(2)
  116. switch state.startGracefulShutdown() {
  117. case .sendGoAwayAndPing(let pingData):
  118. // Open another stream and then receive the ping ack.
  119. state.streamOpened(3)
  120. XCTAssertEqual(
  121. state.receivedPingAck(data: pingData),
  122. .sendGoAway(lastStreamID: 3, close: false)
  123. )
  124. case .none:
  125. XCTFail("Expected '.sendGoAwayAndPing'")
  126. }
  127. // Both GOAWAY frames have been sent. Start closing streams.
  128. XCTAssertEqual(state.streamClosed(1), .none)
  129. XCTAssertEqual(state.streamClosed(2), .none)
  130. XCTAssertEqual(state.streamClosed(3), .close)
  131. }
  132. func testGracefulShutdownWhenNoOpenStreamsBeforeSecondGoAway() {
  133. var state = self.makeStateMachine()
  134. // Open a stream.
  135. state.streamOpened(1)
  136. switch state.startGracefulShutdown() {
  137. case .sendGoAwayAndPing(let pingData):
  138. // Close the stream. This shouldn't lead to a close.
  139. XCTAssertEqual(state.streamClosed(1), .none)
  140. // Only on receiving the ack do we send a GOAWAY and close.
  141. XCTAssertEqual(
  142. state.receivedPingAck(data: pingData),
  143. .sendGoAway(lastStreamID: 1, close: true)
  144. )
  145. case .none:
  146. XCTFail("Expected '.sendGoAwayAndPing'")
  147. }
  148. }
  149. func testPingStrikeUsingMinReceiveInterval(
  150. state: inout ServerConnectionManagementHandler.StateMachine,
  151. interval: TimeAmount,
  152. expectedID id: HTTP2StreamID
  153. ) {
  154. var time = NIODeadline.now()
  155. let data = HTTP2PingData()
  156. // The first ping is never a strike.
  157. XCTAssertEqual(state.receivedPing(atTime: time, data: data), .sendAck)
  158. // Advance time by just less than the interval and get two strikes.
  159. time = time + interval - .nanoseconds(1)
  160. XCTAssertEqual(state.receivedPing(atTime: time, data: data), .sendAck)
  161. XCTAssertEqual(state.receivedPing(atTime: time, data: data), .sendAck)
  162. // Advance time so that we're at one interval since the last valid ping. This isn't a
  163. // strike (but doesn't reset strikes) and updates the last valid ping time.
  164. time = time + .nanoseconds(1)
  165. XCTAssertEqual(state.receivedPing(atTime: time, data: data), .sendAck)
  166. // Now get a third and final strike.
  167. XCTAssertEqual(state.receivedPing(atTime: time, data: data), .enhanceYourCalmThenClose(id))
  168. }
  169. func testPingStrikesWhenKeepaliveIsNotPermittedWithoutCalls() {
  170. let initialState = self.makeStateMachine(
  171. allowKeepaliveWithoutCalls: false,
  172. minPingReceiveIntervalWithoutCalls: .minutes(5)
  173. )
  174. var state = initialState
  175. state.streamOpened(1)
  176. self.testPingStrikeUsingMinReceiveInterval(state: &state, interval: .minutes(5), expectedID: 1)
  177. state = initialState
  178. self.testPingStrikeUsingMinReceiveInterval(state: &state, interval: .hours(2), expectedID: 0)
  179. }
  180. func testPingStrikesWhenKeepaliveIsPermittedWithoutCalls() {
  181. var state = self.makeStateMachine(
  182. allowKeepaliveWithoutCalls: true,
  183. minPingReceiveIntervalWithoutCalls: .minutes(5)
  184. )
  185. self.testPingStrikeUsingMinReceiveInterval(state: &state, interval: .minutes(5), expectedID: 0)
  186. }
  187. func testResetPingStrikeState() {
  188. var state = self.makeStateMachine(
  189. allowKeepaliveWithoutCalls: true,
  190. minPingReceiveIntervalWithoutCalls: .minutes(5)
  191. )
  192. var time = NIODeadline.now()
  193. let data = HTTP2PingData()
  194. // The first ping is never a strike.
  195. XCTAssertEqual(state.receivedPing(atTime: time, data: data), .sendAck)
  196. // Advance time by less than the interval and get two strikes.
  197. time = time + .minutes(1)
  198. XCTAssertEqual(state.receivedPing(atTime: time, data: data), .sendAck)
  199. XCTAssertEqual(state.receivedPing(atTime: time, data: data), .sendAck)
  200. // Reset the ping strike state and test ping strikes as normal.
  201. state.resetKeepaliveState()
  202. self.testPingStrikeUsingMinReceiveInterval(state: &state, interval: .minutes(5), expectedID: 0)
  203. }
  204. }