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

4.x: Fix Delay hanging due to wrong disposable-chain management #560

Merged
merged 3 commits into from
Jun 1, 2018

Conversation

akarnokd
Copy link
Collaborator

This PR fixes a bug in Delay that can lead to an unwanted cancellation of the drain loop. In addition, the IStopwatch.Elapsed should be consistently read while holding the same lock.

The bug is that the _cancelable can be reset by the IDisposable of the very first task starting the drain loop while that drain loop already set the IDisposable for the next round. The first assignment then cancels the second run's cancellation token which then throws an ObjectDisposedException and the loop quits for good. Any queued items and terminal signals will then never be emitted.

The solution is to conditionally set that first IDisposable if the target is still null, otherwise the subsequent runs have set a newer one and nothing has to be done. The disposability is still intact in both cases.

To be able to do this, the original subscription containers had to be replaced by fields so that the new atomic Disposable.TryX methods can be employed.

In the pre-patch version, running the DelayTest.Delay_DateTimeOffset_DefaultScheduler with a loop a million times and with no practical time shift (FromMilliseconds(0)) triggered the bug within the first few hundred rounds. With the patch, the same test succeeded all million rounds. I did not change the test because it would take a minute to complete and also code coverage tends to choke on such high-volume tests in my experience.

Fixes: #382

return this;
}

protected override void Dispose(bool disposing)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Move those two disposables and Dispose into the base class?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The class is ugly, didn't want to accidentally break it.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Should be good for now, might be an optimization PR for later.

// ScheduleDrain might have already set a newer disposable
// using TrySetSerial would cancel it, stopping the emission
// and hang the consumer
Disposable.TrySetSingle(ref _cancelable, parent._scheduler.Schedule(parent._dueTime, Start));
Copy link
Collaborator

Choose a reason for hiding this comment

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

_cancelable was as SerialDisposable before, even if this works, consider TrySetSerial throughout to reveal intention more consistently.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That was the bug: _cancelable was SerialDisposable and SerialDisposable always forcefully overwrites and disposes the previous IDisposable it contains.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah, alright.

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.

ObservableTimeTest.Delay_DateTimeOffset_DefaultScheduler stuck in CI
2 participants