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

Fix race condition in the merge iterator close method #9163

Merged
merged 1 commit into from
Nov 27, 2017

Conversation

jsternberg
Copy link
Contributor

@jsternberg jsternberg commented Nov 27, 2017

If the close happens when next is being called, it can result in a race
condition where the current iterator gets set to nil after the initial
check.

This also fixes the finalizer so it runs the close method in a goroutine
instead of running it by itself. This is because all finalizers run on
the same goroutine so a close that takes a long time can cause a backup
for all finalizers. This also removes the redundant call to
runtime.SetFinalizer from the finalizer itself because a finalizer,
when called, has already cleared itself.

  • Rebased/mergable
  • Tests pass
  • CHANGELOG.md updated

@ghost ghost assigned jsternberg Nov 27, 2017
@ghost ghost added the review label Nov 27, 2017
@jsternberg jsternberg force-pushed the js-merge-iterator-close-race-condition branch 4 times, most recently from 1f279f0 to bed37b5 Compare November 27, 2017 22:45
If the close happens when next is being called, it can result in a race
condition where the current iterator gets set to nil after the initial
check.

This also fixes the finalizer so it runs the close method in a goroutine
instead of running it by itself. This is because all finalizers run on
the same goroutine so a close that takes a long time can cause a backup
for all finalizers. This also removes the redundant call to
`runtime.SetFinalizer` from the finalizer itself because a finalizer,
when called, has already cleared itself.
@jsternberg jsternberg force-pushed the js-merge-iterator-close-race-condition branch from bed37b5 to a73c3a1 Compare November 27, 2017 22:55
@jsternberg
Copy link
Contributor Author

@rbetts this will need to be merged and backported to 1.4. Do you want me to backport this to 1.3 too?

Copy link
Contributor

@stuartcarnie stuartcarnie left a comment

Choose a reason for hiding this comment

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

LGTM but interested in your comments about the goroutine

runtime.SetFinalizer(itr, nil)
itr.logger.Error("{{.Name}}Iterator finalized by GC")
itr.Close()
go func() {
Copy link
Contributor

Choose a reason for hiding this comment

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

do we have any data that the itr.Close runs for a long time? From my observations

  • a finalizer iterator wraps a Merge iterator and maybe the Interrupt iterator
  • a Merge iterator wraps 1..N tsm1.<type>Iterator iterators
  • a tsm1.<type>Iterator wraps 1..N cursors
  • closing a cursor decrements any file refs before closing

I would not expect any of those to take any amount of time to clean up.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The main reason I added this is because the merge iterator now has a lock. The close method needs to wait for next to finish before taking the lock and I didn't want to stall the garbage collector. I know the GC isn't "stop the world" anymore and it appears that finalizers run in a different thread, but the SetFinalizer() documentation still recommends launching a goroutine if it can potentially take awhile.

https://godoc.org/runtime#SetFinalizer

Relevant sections:

SetFinalizer sets the finalizer associated with obj to the provided finalizer function. When the garbage collector finds an unreachable block with an associated finalizer, it clears the association and runs finalizer(obj) in a separate goroutine. This makes obj reachable again, but now without an associated finalizer. Assuming that SetFinalizer is not called again, the next time the garbage collector sees that obj is unreachable, it will free obj.

A single goroutine runs all finalizers for a program, sequentially. If a finalizer must run for a long time, it should do so by starting a new goroutine.

If the possibility that this delays other finalizers is fine, then I can remove the goroutine and that should simplify the test.

Copy link
Contributor

@stuartcarnie stuartcarnie Nov 27, 2017

Choose a reason for hiding this comment

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

Given there is only one of these created per CreateIterator call, it is fine. I wouldn't want to see 1,000's of these created just to call Close

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.

None yet

2 participants