Skip to content

Feat: Automatic Stale connection cleanup#48

Merged
ianpartridge merged 17 commits intomasterfrom
staleCleanup
Jul 31, 2018
Merged

Feat: Automatic Stale connection cleanup#48
ianpartridge merged 17 commits intomasterfrom
staleCleanup

Conversation

@Andrew-Lees11
Copy link
Copy Markdown
Contributor

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.

private var resumed: Bool = false

deinit {
timer.setEventHandler {}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for self. on function calls.

@Andrew-Lees11
Copy link
Copy Markdown
Contributor Author

@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()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be neater if timerStart() created the timer too? So this would be

self.timer = timerStart(connectionTimeout: connectionTimeout)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpectedly

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")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpectedly

WebSocket.register(service: service, onPath: onPath ?? servicePath)
}

func register(onPath: String? = nil, closeReason: WebSocketCloseReasonCode, testServerRequest: Bool = false, pingMessage: String? = nil, connectionTimeout: Int? = nil) -> TestWebSocketService {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Newline above this please.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be marked public or is it implicitly public?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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? {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment please.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be marked public or is it implicitly public?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the timeout -> of the timeout

Copy link
Copy Markdown
Contributor

@ianpartridge ianpartridge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

pushkarnk pushed a commit to Kitura/Kitura-WebSocket-NIO that referenced this pull request Jul 17, 2019
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.
pushkarnk pushed a commit to Kitura/Kitura-WebSocket-NIO that referenced this pull request Jul 17, 2019
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.
pushkarnk pushed a commit to Kitura/Kitura-WebSocket-NIO that referenced this pull request Jul 22, 2019
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.
pushkarnk pushed a commit to Kitura/Kitura-WebSocket-NIO that referenced this pull request Jul 29, 2019
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.
pushkarnk pushed a commit to Kitura/Kitura-WebSocket-NIO that referenced this pull request Jul 30, 2019
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.
pushkarnk pushed a commit to Kitura/Kitura-WebSocket-NIO that referenced this pull request Jul 30, 2019
* 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants