Skip to content

Better handle writability changes in triggerWriteOperations #1624

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

Merged
merged 2 commits into from
Aug 26, 2020

Conversation

glbrntt
Copy link
Contributor

@glbrntt glbrntt commented Aug 25, 2020

Motivation:

If a write is buffered in the PendingWritesManager and the high
watermark is breached then a writability change will be fired as a
result. The corresponding event when the channel becomes writable again
comes when enough pending bytes have been written to the socket (i.e. as
a result of a flush()) and we fall below the low watermark.

However, if a promise associated with a write has a callback
registered on its future which writes enough data to breach the high
watermark (and also flushes) then we will re-entrantly enter
flushNow(). This is okay, flushNow() protects against re-entrancy
and any re-entrantly enqueud flushed writes will be picked up in the
write-spin-loop.

The issue here is that the writeSpinLoop does not monitor for changes
in writability. Instead if checks the writability before it begins and
after it completes, only signalling if there was a change. This is
problematic: the re-entrant write-and-flush could have caused the
channel to become unwritable yet no event will be fired to make it
writable again!

Modifications:

  • Check whether the channel became unwritable after each write
    operation, and emit a writability change if the channel became
    unwritable and writable again during the spin loop.

Result:

  • Better protection against re-entrant writability changes.

@glbrntt glbrntt requested a review from Lukasa August 25, 2020 16:25
@glbrntt glbrntt added the 🔨 semver/patch No public API change. label Aug 25, 2020
@@ -478,6 +479,9 @@ extension PendingWritesManager {
}

oneResult = try triggerOneWriteOperation(self.currentBestWriteMechanism)
// We may have become unwritable re-entrantly.
becameUnwritable = (becameUnwritable || !self.isWritable)
Copy link
Contributor

Choose a reason for hiding this comment

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

So the downside of this is that we are now repeatedly polling the atomic isWritable variable. This is a technically correct solution, so we may need to merge it if we can't find a better one, but I'd like to look at alternative options.

One thing I'm wondering about is whether we can defer writability changes that occur while we're already in triggerWriteOperations. Essentially, if we're in triggerWriteOperations, perhaps add should never issue a writability notification.

Another option that preserves these writability notifications (which, to be clear, I believe are useful) is to have a state variable updated by add if it does emit a false writability state update. That state variable can be checked by this code at the bottom of the loop. This is probably the easiest to implement and simplest to understand: maybe we should do that?

What do you think @glbrntt?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So the downside of this is that we are now repeatedly polling the atomic isWritable variable.

Agreed, it's far from ideal!

One thing I'm wondering about is whether we can defer writability changes that occur while we're already in triggerWriteOperations. Essentially, if we're in triggerWriteOperations, perhaps add should never issue a writability notification.

Another option that preserves these writability notifications (which, to be clear, I believe are useful) is to have a state variable updated by add if it does emit a false writability state update. That state variable can be checked by this code at the bottom of the loop. This is probably the easiest to implement and simplest to understand: maybe we should do that?

I think it's better to preserve the writability notifications, I'll give this second approach a shot.

@glbrntt glbrntt force-pushed the gb-writability-reentrancy branch from 6c217cd to d69a211 Compare August 26, 2020 09:54
@@ -451,12 +456,13 @@ internal enum WriteMechanism {
case nothingToBeWritten
}

internal protocol PendingWritesManager {
internal protocol PendingWritesManager: AnyObject {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had to add this to work around triggerWriteOperations being an extension (since it mutates self), making it a mutating func didn't do the trick either.

Motivation:

If a write is buffered in the `PendingWritesManager` and the high
watermark is breached then a writability change will be fired as a
result. The corresponding event when the channel becomes writable again
comes when enough pending bytes have been written to the socket (i.e. as
a result of a `flush()`) and we fall below the low watermark.

However, if a promise associated with a write has a callback
registered on its future which writes enough data to breach the high
watermark (and also flushes) then we will re-entrantly enter
`flushNow()`. This is okay, `flushNow()` protects against re-entrancy
and any re-entrantly enqueud flushed writes will be picked up in the
write-spin-loop.

The issue here is that the `writeSpinLoop` does not monitor for changes
in writability. Instead if checks the writability before it begins and
after it completes, only signalling if there was a change. This is
problematic: the re-entrant write-and-flush could have caused the
channel to become unwritable yet no event will be fired to make it
writable again!

Modifications:

- Store a local writability state in the pending writes manager and
  modify it when we emit writability changes

Result:

- Better protection against re-entrant writability changes.
@glbrntt glbrntt force-pushed the gb-writability-reentrancy branch from d69a211 to 8ff9ae4 Compare August 26, 2020 10:47
Copy link
Contributor

@Lukasa Lukasa left a comment

Choose a reason for hiding this comment

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

Great fix @glbrntt, I really like it.

@Lukasa Lukasa merged commit 5fc2434 into apple:master Aug 26, 2020
@glbrntt glbrntt deleted the gb-writability-reentrancy branch August 26, 2020 12:01
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