Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid precondition failure in write timeout #803

Merged
merged 3 commits into from
Jan 28, 2025

Conversation

rnro
Copy link
Collaborator

@rnro rnro commented Jan 28, 2025

Motivation:

In some cases we can crash because of a precondition failure when the write timeout fires and we aren't in the running state. This can happen for example if the connection is closed whilst the write timer is active.

Modifications:

  • Remove the precondition and instead take no action if the timeout fires outside of the running state. Instead we take a new Action, .noAction when the timer fires.
  • Clear write timeouts upon request completion. When a request completes we have no use for the idle write timer, we clear the read timer and we should clear the write one too.

Result:

Fewer crashes.

The supplied tests fails without these changes and passes with either of them.

@rnro rnro requested a review from fabianfett January 28, 2025 14:47
Motivation:

In some cases we can crash because of a precondition failure when the
write timeout fires and we aren't in the running state. This can happen
for example if the connection is closed whilst the write timer is
active.

Modifications:

Remove the precondition and instead take no action if the timeout fires
outside of the running state. Instead we take a new `Action`,
`.noAction` when the timer fires.

Result:

Fewer crashes.
@rnro rnro force-pushed the write_timeout_crash branch from 4d896b0 to 90e8b58 Compare January 28, 2025 14:57
@rnro rnro added the 🔨 semver/patch No public API change. label Jan 28, 2025
When a request completes we have no use for the idle write timer, we
clear the read timer and we should clear the write one too.
@@ -281,7 +281,7 @@ final class HTTP1ClientChannelHandler: ChannelDuplexHandler {
case .close:
context.close(promise: nil)

case .wait:
case .wait, .noAction:
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Do we need this new state or can we just re-use .wait?

Copy link
Member

Choose a reason for hiding this comment

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

reuse .wait. .wait is a do nothing. can be renamed in a follow up pr.

Copy link
Member

@fabianfett fabianfett left a comment

Choose a reason for hiding this comment

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

Nits.

@@ -281,7 +281,7 @@ final class HTTP1ClientChannelHandler: ChannelDuplexHandler {
case .close:
context.close(promise: nil)

case .wait:
case .wait, .noAction:
Copy link
Member

Choose a reason for hiding this comment

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

reuse .wait. .wait is a do nothing. can be renamed in a follow up pr.

@@ -359,7 +361,7 @@ struct HTTP1ConnectionStateMachine {

mutating func idleWriteTimeoutTriggered() -> Action {
guard case .inRequest(var requestStateMachine, let close) = self.state else {
preconditionFailure("Invalid state: \(self.state)")
return .noAction
Copy link
Member

Choose a reason for hiding this comment

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

Do you mind adding another test to HTTP1ConnectionStateMachineTests that just tests this edge?

@@ -83,6 +83,8 @@ struct HTTP1ConnectionStateMachine {
case fireChannelActive
case fireChannelInactive
case fireChannelError(Error, closeConnection: Bool)

case noAction
Copy link
Member

Choose a reason for hiding this comment

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

please remove.

@@ -840,6 +840,81 @@ class HTTP1ClientChannelHandlerTests: XCTestCase {
channel.writeAndFlush(request, promise: nil)
XCTAssertEqual(request.events.map(\.kind), [.willExecuteRequest, .requestHeadSent])
}

class SlowHandler: ChannelOutboundHandler {
Copy link
Member

Choose a reason for hiding this comment

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

this isn't used at all.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah, a relic from a previous test approach.

request.body = .init(contentLength: nil, stream: streamCallback)

let delegate = NullResponseDelegate()
var maybeRequestBag: RequestBag<NullResponseDelegate>?
Copy link
Member

Choose a reason for hiding this comment

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

Why not use a ResponseAccumulator here instead?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I hadn't spotted that, thanks.

@rnro rnro requested a review from fabianfett January 28, 2025 16:47
Copy link
Member

@fabianfett fabianfett left a comment

Choose a reason for hiding this comment

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

Thanks!

@rnro rnro merged commit f38c2fe into swift-server:main Jan 28, 2025
21 of 22 checks passed
@rnro rnro deleted the write_timeout_crash branch January 28, 2025 17:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🔨 semver/patch No public API change.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants