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

stop the deadline timer in Stream.Read and Write #2519

Merged
merged 2 commits into from
May 5, 2020

Conversation

marten-seemann
Copy link
Member

When a deadline is set on a stream, the time.Time is saved in the deadline member variable.
When Read or Write are called, and deadline is set, we start a timer in order to return these calls on timer expiration. In the common case, deadlines don't expire, and Read and Write calls return before expiration of the timer. We need to make sure to stop this timer, otherwise it will leak.

This is the more probable reason for the performance issue reported in ipfs/kubo#7263, as every single Read and Write call that returns before the deadline leaks a new timer. This will accumulate a lot more timers than the timer leak that happened when closing a session, or when we received a duplicate Initial, which was fixed in #2516.

cc @Stebalien

@@ -132,6 +132,7 @@ func (s *receiveStream) readImpl(p []byte) (bool /*stream completed */, int, err
}
if deadlineTimer == nil {
deadlineTimer = utils.NewTimer()
defer deadlineTimer.Stop()

Choose a reason for hiding this comment

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

  • This will defer until the function returns. Are you sure this won't happen multiple times.
  • Alternatively, why is the deadline timer not defined outside the outer loop?
  • More generally, why are we looping in the first place instead of reading one frame and returning?

Copy link
Member Author

Choose a reason for hiding this comment

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

This will defer until the function returns. Are you sure this won't happen multiple times.

Yes, deadlineTimer is only set if deadlineTimer == nil, so it can only happen once.

Alternatively, why is the deadline timer not defined outside the outer loop?

If no deadline is set, there's no need to start a timer. Note that the user might set a deadline concurrently with a Read call.

More generally, why are we looping in the first place instead of reading one frame and returning?

That's due to the way frames are processed: every time a STREAM frame is received on that stream, the Read loop wakes up and tries to read data. STREAM frames might arrive out of order though, so we need to repeat this until a STREAM frame at the current read offset is received.

More generally speaking, I wish there was a way to concurrently use a timer. Then I could just set the timer in SetDeadline and use it in Read. This whole dance of saving the deadline as a timestamp and then creating and destroying the timer in Read is only necessary because Go messed up the timer API.

Choose a reason for hiding this comment

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

Ah, got it.

Choose a reason for hiding this comment

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

But are you sure we can't loop on line 107?

Copy link
Member Author

Choose a reason for hiding this comment

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

But are you sure we can't loop on line 107?

I don't understand the question. The loop is on line 107.

Choose a reason for hiding this comment

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

Basically, either:

  1. The loop on line 107 is useless.
  2. We can reach this code multiple times (creating multiple deadline timers).

My suggestion was moving the deadlineTimer variable declaration above the outer loop because it's currently inside the outer loop.

Copy link
Member Author

Choose a reason for hiding this comment

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

The use case is the following:

  1. The user calls Read. The call blocks, since there's no data to read and we're waiting for the STREAM frame at the right offset to arrive.
  2. The user then calls SetReadDeadline.
  3. After this, the STREAM frame is received (or the deadline expires).

My suggestion was moving the deadlineTimer variable declaration above the outer loop because it's currently inside the outer loop.

I guess you could declare it as a pointer to a timer (so we don't actually have to start a timer when no deadline is set), and then have a defer that stops the timer if it's != nil.

Choose a reason for hiding this comment

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

It sounds like you're talking about the inner loop. I'm talking about the outer loop.

I guess you could declare it as a pointer to a timer (so we don't actually have to start a timer when no deadline is set), and then have a defer that stops the timer if it's != nil.

That's what you're already doing, isn't it? I'm literally just suggesting that the declaration on line 115 be moved to line 106. If you do that, looping at line 107 won't re-declare a new deadlineTimer variable, causing you to allocate a new timer on line 134.

Copy link
Member Author

@marten-seemann marten-seemann May 4, 2020

Choose a reason for hiding this comment

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

That makes sense. And it's consistent with what we do in the sending half of the stream. Thank you!

@codecov-io
Copy link

codecov-io commented May 4, 2020

Codecov Report

Merging #2519 into master will increase coverage by 0.00%.
The diff coverage is 100.00%.

Impacted file tree graph

@@           Coverage Diff           @@
##           master    #2519   +/-   ##
=======================================
  Coverage   86.14%   86.14%           
=======================================
  Files         122      122           
  Lines        9536     9538    +2     
=======================================
+ Hits         8214     8216    +2     
  Misses        984      984           
  Partials      338      338           
Impacted Files Coverage Δ
receive_stream.go 96.43% <100.00%> (+0.02%) ⬆️
send_stream.go 92.99% <100.00%> (+0.03%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update c0b6d4e...996dd42. Read the comment docs.

Copy link

@Stebalien Stebalien left a comment

Choose a reason for hiding this comment

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

LGTM!

@marten-seemann marten-seemann merged commit 4d83009 into master May 5, 2020
@marten-seemann marten-seemann deleted the fix-stream-deadline-timer-leak branch May 5, 2020 11:10
@Stebalien Stebalien mentioned this pull request May 26, 2020
77 tasks
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.

4 participants