Feat: Automatic Stale connection cleanup#48
Conversation
| private var resumed: Bool = false | ||
|
|
||
| deinit { | ||
| timer.setEventHandler {} |
There was a problem hiding this comment.
Won't accessing this lazy property mean that the DispatchSourceTimer is created during deinit, even if connectionTimeout is nil?
|
|
||
| private var active = true | ||
|
|
||
| private let timer: DispatchSourceTimer = DispatchSource.makeTimerSource() |
There was a problem hiding this comment.
Doesn't this mean every connection creates a DispatchSource, even if we don't use it? Can we only create one (in init()) if we need one?
| self.service = service | ||
| if let connectionTimeout = service?.connectionTimeout { | ||
| timer = DispatchSource.makeTimerSource() | ||
| self.timerStart(connectionTimeout: connectionTimeout) |
There was a problem hiding this comment.
No need for self. on function calls.
|
@bridger Would you be able to have a look as this PR to ensure it fixes your issue and see if you have any comments? Thanks |
| self.service = service | ||
| if let connectionTimeout = service?.connectionTimeout { | ||
| lastFrameReceivedAt = Date() | ||
| timer = DispatchSource.makeTimerSource() |
There was a problem hiding this comment.
It might be neater if timerStart() created the timer too? So this would be
self.timer = timerStart(connectionTimeout: connectionTimeout)There was a problem hiding this comment.
timerStart requires self and so needs all stored properties to be initialized before being called. This means we can't have it return the timer. We could have the timer as an optional var defaulted to nil but since it is not changed I thought this was better.
|
|
||
| static var allTests: [(String, (ConnectionCleanupTests) -> () throws -> Void)] { | ||
| return [ | ||
| ("testSingleConnectionTimeOut", testSingleConnectionTimeOut), |
There was a problem hiding this comment.
Can you make the order of the elements in the array match the order of the functions in the file please.
| usleep(1500) | ||
| XCTAssertEqual(service.connections.count, 1, "Failed to create connection to service") | ||
| sleep(3) | ||
| XCTAssertEqual(service.connections.count, 1, "Stale connection was unexpectantly cleaned up") |
| sleep(1) | ||
| self.sendFrame(final: true, withOpcode: self.opcodePing, withPayload: NSData(), on: socket) | ||
| sleep(1) | ||
| XCTAssertEqual(service.connections.count, 1, "Stale connection was unexpectantly cleaned up") |
| WebSocket.register(service: service, onPath: onPath ?? servicePath) | ||
| } | ||
|
|
||
| func register(onPath: String? = nil, closeReason: WebSocketCloseReasonCode, testServerRequest: Bool = false, pingMessage: String? = nil, connectionTimeout: Int? = nil) -> TestWebSocketService { |
There was a problem hiding this comment.
Do we need this extra function? Could we make the register() function above return a service instead?
| /// The time in seconds after which, if a connection is has sent no messages, the WebSocket server will send a ping to the connection. If a pong is not received in response, the connection will be closed. If connectionTimeout is nil, no connection cleanup will take place. | ||
| var connectionTimeout: Int? { get } | ||
| } | ||
| extension WebSocketService { |
There was a problem hiding this comment.
Newline above this please.
There was a problem hiding this comment.
Does this need to be marked public or is it implicitly public?
There was a problem hiding this comment.
It is implicitly public but i can declare it public if that makes the code clearer
| var connectionTimeout: Int? { get } | ||
| } | ||
| extension WebSocketService { | ||
| var connectionTimeout: Int? { |
There was a problem hiding this comment.
Does this need to be marked public or is it implicitly public?
There was a problem hiding this comment.
protocols cannot declare field access levels so that is set when something conforms to the protocol. The comment describing this is on the protocol connectionTimeout which is what gets displayed in Jazzy. I will add a comment describing the default implementation though.
README.md
Outdated
|
|
||
| #### Detect and close broken connections | ||
|
|
||
| WebSocketService protocol has an optional Int property called `connectionTimeout`. This is the time in seconds that a connection must be unresponsive to be automatically closed by the server. If the WebSocket server has not received any messages in the first half of the timeout time it will ping the connection. If a pong is not received in the remaining half if the timeout, the connection will be closed with a 1006 (connection closed abnormally) status code. The `connectionTimeout` defaults to `nil`, meaning no connection cleanup will take place. |
There was a problem hiding this comment.
if the timeout -> of the timeout
| /// the client sent the message to this `WebSocketService` | ||
| func received(message: String, from: WebSocketConnection) | ||
|
|
||
| /// The time in seconds that a connection must be unresponsive to be automatically closed by the server. If the WebSocket server has not received any messages in the first half of the timeout time it will ping the connection. If a pong is not received in the remaining half if the timeout, the connection will be closed with a 1006 (connection closed abnormally) status code. The `connectionTimeout` defaults to `nil`, meaning no connection cleanup will take place. |
There was a problem hiding this comment.
if the timeout -> of the timeout
Motivation: On Kitura-WebSocket Kitura/Kitura-WebSocket#48 introduces an automatic connection cleanup feature. Using this, `WebSocketService`s can configure an optional timeout value. If a connection remains idle for half of the configured timeout value, the server sends a heartbeat ping to the client. In the a subsquent time period again equal to half of the configured timeout value, if a pong is received on the connection, the latter is kept alive. If not, the connection is closed. In `Kitura-WebSocket-NIO`, we implement this using the `IdleStateHandler` which is configured with half of the configured timeout value. When we receive the first `IdleStateEvent`, we send a heartbeat ping to the client. If a pong is received before the next `IdleStateEvent`, the connection is kept alive. If the next `IdleStateEvent` is received before the pong, the connection is closed. This commit also includes a new client framework for testing. It is difficult to write the connection cleanup tests using the existing adhoc WebSocket client framework implemented in `KituraTest.swift` and `KituraTest+Frames.swift`. The new client framework is called `WebSocketClient`. It uses a callback model and is largely work-in-progress. We must move all of the existing tests to the new framework in due time.
Motivation: On Kitura-WebSocket Kitura/Kitura-WebSocket#48 introduces an automatic connection cleanup feature. Using this, `WebSocketService`s can configure an optional timeout value. If a connection remains idle for half of the configured timeout value, the server sends a heartbeat ping to the client. In a following time period equal to half of the configured timeout value, if a pong is received on the connection, the latter is kept alive. If not, the connection is closed. In `Kitura-WebSocket-NIO`, we implement this using the `IdleStateHandler` which is configured with half of the configured timeout value. When we receive the first `IdleStateEvent`, we send a heartbeat ping to the client. If a pong is received before the next `IdleStateEvent`, the connection is kept alive. If the next `IdleStateEvent` is received before the pong, the connection is closed. This commit also includes a new client framework for testing. It is difficult to write the connection cleanup tests using the existing adhoc WebSocket client framework implemented in `KituraTest.swift` and `KituraTest+Frames.swift`. The new client framework is called `WebSocketClient`. It uses a callback model and is largely work-in-progress. We must move all of the existing tests to the new framework in due time.
Motivation: On Kitura-WebSocket Kitura/Kitura-WebSocket#48 introduces an automatic connection cleanup feature. Using this, `WebSocketService`s can configure an optional timeout value. If a connection remains idle for half of the configured timeout value, the server sends a heartbeat ping to the client. In a following time period equal to half of the configured timeout value, if a pong is received on the connection, the latter is kept alive. If not, the connection is closed. In `Kitura-WebSocket-NIO`, we implement this using the `IdleStateHandler` which is configured with half of the configured timeout value. When we receive the first `IdleStateEvent`, we send a heartbeat ping to the client. If a pong is received before the next `IdleStateEvent`, the connection is kept alive. If the next `IdleStateEvent` is received before the pong, the connection is closed. This commit also includes a new client framework for testing. It is difficult to write the connection cleanup tests using the existing adhoc WebSocket client framework implemented in `KituraTest.swift` and `KituraTest+Frames.swift`. The new client framework is called `WebSocketClient`. It uses a callback model and is largely work-in-progress. We must move all of the existing tests to the new framework in due time.
Motivation: On Kitura-WebSocket Kitura/Kitura-WebSocket#48 introduces an automatic connection cleanup feature. Using this, `WebSocketService`s can configure an optional timeout value. If a connection remains idle for half of the configured timeout value, the server sends a heartbeat ping to the client. In a following time period equal to half of the configured timeout value, if a pong is received on the connection, the latter is kept alive. If not, the connection is closed. In `Kitura-WebSocket-NIO`, we implement this using the `IdleStateHandler` which is configured with half of the configured timeout value. When we receive the first `IdleStateEvent`, we send a heartbeat ping to the client. If a pong is received before the next `IdleStateEvent`, the connection is kept alive. If the next `IdleStateEvent` is received before the pong, the connection is closed. This commit also includes a new client framework for testing. It is difficult to write the connection cleanup tests using the existing adhoc WebSocket client framework implemented in `KituraTest.swift` and `KituraTest+Frames.swift`. The new client framework is called `WebSocketClient`. It uses a callback model and is largely work-in-progress. We must move all of the existing tests to the new framework in due time.
Motivation: On Kitura-WebSocket Kitura/Kitura-WebSocket#48 introduces an automatic connection cleanup feature. Using this, `WebSocketService`s can configure an optional timeout value. If a connection remains idle for half of the configured timeout value, the server sends a heartbeat ping to the client. In a following time period equal to half of the configured timeout value, if a pong is received on the connection, the latter is kept alive. If not, the connection is closed. In `Kitura-WebSocket-NIO`, we implement this using the `IdleStateHandler` which is configured with half of the configured timeout value. When we receive the first `IdleStateEvent`, we send a heartbeat ping to the client. If a pong is received before the next `IdleStateEvent`, the connection is kept alive. If the next `IdleStateEvent` is received before the pong, the connection is closed. This commit also includes a new client framework for testing. It is difficult to write the connection cleanup tests using the existing adhoc WebSocket client framework implemented in `KituraTest.swift` and `KituraTest+Frames.swift`. The new client framework is called `WebSocketClient`. It uses a callback model and is largely work-in-progress. We must move all of the existing tests to the new framework in due time.
* Add automatic stale connection cleanup Motivation: On Kitura-WebSocket Kitura/Kitura-WebSocket#48 introduces an automatic connection cleanup feature. Using this, `WebSocketService`s can configure an optional timeout value. If a connection remains idle for half of the configured timeout value, the server sends a heartbeat ping to the client. In a following time period equal to half of the configured timeout value, if a pong is received on the connection, the latter is kept alive. If not, the connection is closed. In `Kitura-WebSocket-NIO`, we implement this using the `IdleStateHandler` which is configured with half of the configured timeout value. When we receive the first `IdleStateEvent`, we send a heartbeat ping to the client. If a pong is received before the next `IdleStateEvent`, the connection is kept alive. If the next `IdleStateEvent` is received before the pong, the connection is closed. This commit also includes a new client framework for testing. It is difficult to write the connection cleanup tests using the existing adhoc WebSocket client framework implemented in `KituraTest.swift` and `KituraTest+Frames.swift`. The new client framework is called `WebSocketClient`. It uses a callback model and is largely work-in-progress. We must move all of the existing tests to the new framework in due time. * Log error and close channel on unknown error * Propagate event if not IdleStateEvent
This PR adds a connectionTimeout tune-able to Websocket service. This represents the time in seconds after which, if a connection is has sent no messages, the WebSocket server will send a ping to the connection. If a pong is not received in response, the connection will be closed. If connectionTimeout is nil, no connection cleanup will take place.
This is requested in issue #24 for cleaning up stale connections.
This is implemented through a lastFrameReceivedAt timestamp and a Timer which checks every connectionTimeout seconds to see if the connection has had any activity. If their has been no activity it sends a ping to the client and if by the next interval their is no response it closes the connection.
This has been tested by running a chat server, crashing a client connection and seeing it get cleaned up by the server.