-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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 deadlock in discard stats #1070
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅ A review job has been created and sent to the PullRequest network.
@ashish-goswami you can click here to see the review status or cancel the code review job.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add in the commit details, why there was a deadlock? It would be much clearer than just the code changes.
Reviewed with ❤️ by PullRequest
// Here insert discardStats more than the cap(vlog.lfDiscardStats.flushChan), that will ensure | ||
// if we have tried to write at least one discard stats entry while writes were blocked. | ||
encodedDS := db.vlog.encodedDiscardStats() | ||
for i := 0; i < cap(db.vlog.lfDiscardStats.flushChan)+2; i++ { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why 2
here?
value.go
Outdated
encodedDS := vlog.encodedDiscardStats() | ||
vlog.lfDiscardStats.flushChan <- encodedDS | ||
} | ||
vlog.lfDiscardStats.Unlock() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a good idea to to defer vlog.lfDiscardStats.Unlock()
right after you lock. That way, if anything happens within the critical section of your code (unhandled errors), then you will at least unlock and prevent a deadlock.
db.go
Outdated
return errors.Wrap(err, "failed to flush discard stats") | ||
} | ||
// Before closing writes, stop flushing discardStats. | ||
db.vlog.closeFlushDiscardStats() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this no longer return an error?
value.go
Outdated
} else if err != nil { | ||
return errors.Wrapf(err, "failed to push discard stats to write channel") | ||
|
||
LOOP: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great idea to provide labels 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
valueLog is a very large file, and could use some refactoring. I saw that there is a lot of logic that has to do with lfDiscardStats. This should definitely be in its own file and interface.
Reviewed with ❤️ by PullRequest
value.go
Outdated
}} | ||
req, err := vlog.db.sendToWriteCh(entries) | ||
if err == ErrBlockedWrites { | ||
// We'll block write while closing db. When L0 compaction in close may push discard |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
grammar nit: writes
instead of write
.
I think this line needs some clarification: "When L0 compaction in close may push discard"
value.go
Outdated
// This will be called while storing stats in BadgerDB | ||
// caller should acquire lock before encoding the stats. | ||
// encodedDiscardStats returns []byte representation of lfDiscardStats. This will be called while | ||
// storing stats in BadgerDB caller should acquire lock before encoding the stats. | ||
func (vlog *valueLog) encodedDiscardStats() []byte { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above, this should be part of the lfDiscardStats
interface.
value.go
Outdated
|
||
if len(vlog.lfDiscardStats.m) == 0 { | ||
return nil | ||
process := func(ds []byte) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
encodedDiscardedStats
or encodedDS
would be better than ds
.
Code is read much more often than it is written, it would be much more intuitive to be more descriptive.
value.go
Outdated
select { | ||
case <-vlog.lfDiscardStats.closer.HasBeenClosed(): | ||
vlog.lfDiscardStats.Lock() | ||
vlog.lfDiscardStats.flushChanClosed = true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
to prevent scattering Lock
and Unlock
everywhere, it would be better to have lfDiscardedStats have a CloseFlushChan
function.
func (ds *DiscardStats) CloseFlushChan() {
ds.Lock()
defer ds.Unlock()
close(ds.flushChan)
ds.flushChanClosed = true
}
value.go
Outdated
} | ||
} | ||
|
||
func (vlog *valueLog) closeFlushDiscardStats() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is just using lfDiscardStat APIs, and doesn't have any valueLog internal logic. Push this into the lfDiscardStat interface.
value.go
Outdated
vlog.lfDiscardStats.Unlock() | ||
// flushDiscardStats also acquires lock. So, we need to unlock here. | ||
return vlog.flushDiscardStats() | ||
encodedDS := vlog.encodedDiscardStats() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since you only use this once, we can just incorporate it with the line below:
vlog.lfDiscardStats.flushChan <- vlog.encodedDiscardStats():
} | ||
// This is important. Set updateSinceFlush to discardStatsFlushThresold so | ||
db.vlog.lfDiscardStats.m = map[uint32]int64{0: 0} | ||
// This is important. Set updateSinceFlush to discardStatsFlushThreshold so |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we also add a test that replicates the deadlock, and make sure it does not deadlock with this change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewable status: 0 of 5 files reviewed, 11 unresolved discussions (waiting on @ashish-goswami, @balajijinnah, @jarifibrahim, @manishrjain, and @pullrequest[bot])
db.go, line 384 at r1 (raw file):
Previously, pullrequest[bot] wrote…
Does this no longer return an error?
No. This doesn't.
value.go, line 1697 at r1 (raw file):
Previously, pullrequest[bot] wrote…
since you only use this once, we can just incorporate it with the line below:
vlog.lfDiscardStats.flushChan <- vlog.encodedDiscardStats():
Done.
value.go, line 1711 at r1 (raw file):
Previously, pullrequest[bot] wrote…
encodedDiscardedStats
orencodedDS
would be better thands
.
Code is read much more often than it is written, it would be much more intuitive to be more descriptive.
Done.
value.go, line 1721 at r1 (raw file):
Previously, pullrequest[bot] wrote…
grammar nit:
writes
instead ofwrite
.
I think this line needs some clarification: "When L0 compaction in close may push discard"
Done.
value.go, line 1762 at r1 (raw file):
Previously, pullrequest[bot] wrote…
It's a good idea to to
defer vlog.lfDiscardStats.Unlock()
right after you lock. That way, if anything happens within the critical section of your code (unhandled errors), then you will at least unlock and prevent a deadlock.
Since this lock will be used after we signal to closer, we need to unlock
it to avoid deadlock.
value_test.go, line 1111 at r1 (raw file):
Previously, pullrequest[bot] wrote…
Why
2
here?
This ensures that we are sending more that cap(db.vlog.lfDiscardStats.flushChan)
to the chan.
LGTM. But, if we have flusher go routine in the db level. It fits well with the existing mechanism(all go routine are spined in the db.go). Also, It'll easy to merge this change to the WAL and vlog separation. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was a tough change. Thanks for fixing this @ashish-goswami . I've added some minor comments.
Reviewed 3 of 5 files at r1.
Dismissed @pullrequest[bot] from a discussion.
Reviewable status: 3 of 5 files reviewed, 13 unresolved discussions (waiting on @ashish-goswami, @balajijinnah, @jarifibrahim, @manishrjain, and @pullrequest[bot])
value.go, line 1721 at r1 (raw file):
Previously, ashish-goswami (Ashish Goswami) wrote…
Done.
This is not done. The comment "When L0 compaction in close may ..." doesn't make sense.
I think you mean "The L0 compaction in DB.Close() function might push discard stats..."
value.go, line 1735 at r1 (raw file):
Previously, pullrequest[bot] wrote…
to prevent scattering
Lock
andUnlock
everywhere, it would be better to have lfDiscardedStats have aCloseFlushChan
function.func (ds *DiscardStats) CloseFlushChan() { ds.Lock() defer ds.Unlock() close(ds.flushChan) ds.flushChanClosed = true }
This makes sense to me. We should have a separate closeFlushChan
function. It would make code much cleaner as well.
value.go, line 1755 at r1 (raw file):
Previously, pullrequest[bot] wrote…
this is just using lfDiscardStat APIs, and doesn't have any valueLog internal logic. Push this into the lfDiscardStat interface.
@ashish-goswami We should do this. The encodedDiscardStats
can also be added to lfDiscardStats interface.
value.go, line 1684 at r2 (raw file):
defer vlog.lfDiscardStats.Unlock() // Compaction is closed after closing of flushing of discard stats.
// updateDiscardStats can be called by compaction after the `flushChan` is closed. This can happen when the DB is being closed.
value.go, line 1759 at r2 (raw file):
if len(vlog.lfDiscardStats.m) > 0 { encodedDS := vlog.encodedDiscardStats() vlog.lfDiscardStats.flushChan <- encodedDS
This can also be written as
vlog.lfDiscardStats.flushChan <- vlog.encodedDiscardStats()
value_test.go, line 1127 at r1 (raw file):
Previously, pullrequest[bot] wrote…
Could we also add a test that replicates the deadlock, and make sure it does not deadlock with this change?
I am working
value_test.go, line 1109 at r2 (raw file):
db.vlog.lfDiscardStats.m = map[uint32]int64{0: 0} db.blockWrite() // Here insert discardStats more than the cap(vlog.lfDiscardStats.flushChan), that will ensure
Push discard stats more than the capacity of flushChan. This ensures at least one flush operation completes successfully after the writes were blocked.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewable status: 3 of 5 files reviewed, 13 unresolved discussions (waiting on @ashish-goswami, @balajijinnah, @manishrjain, and @pullrequest[bot])
value_test.go, line 1127 at r1 (raw file):
Previously, jarifibrahim (Ibrahim Jarif) wrote…
I am working
I am trying to write a test for this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewable status: 2 of 5 files reviewed, 13 unresolved discussions (waiting on @ashish-goswami, @balajijinnah, @jarifibrahim, @manishrjain, and @pullrequest[bot])
value.go, line 1721 at r1 (raw file):
Previously, jarifibrahim (Ibrahim Jarif) wrote…
This is not done. The comment "When L0 compaction in close may ..." doesn't make sense.
I think you mean "The L0 compaction in DB.Close() function might push discard stats..."
updated the comment.
value.go, line 1735 at r1 (raw file):
Previously, jarifibrahim (Ibrahim Jarif) wrote…
This makes sense to me. We should have a separate
closeFlushChan
function. It would make code much cleaner as well.
Not sure, if we should create new function for this.
value.go, line 1755 at r1 (raw file):
Previously, jarifibrahim (Ibrahim Jarif) wrote…
@ashish-goswami We should do this. The
encodedDiscardStats
can also be added to lfDiscardStats interface.
Thinking of raising anther PR for this.
value.go, line 1684 at r2 (raw file):
Previously, jarifibrahim (Ibrahim Jarif) wrote…
// updateDiscardStats can be called by compaction after the `flushChan` is closed. This can happen when the DB is being closed.
updated the comment.
value.go, line 1759 at r2 (raw file):
Previously, jarifibrahim (Ibrahim Jarif) wrote…
This can also be written as
vlog.lfDiscardStats.flushChan <- vlog.encodedDiscardStats()
Done.
value_test.go, line 1109 at r2 (raw file):
Previously, jarifibrahim (Ibrahim Jarif) wrote…
Push discard stats more than the capacity of flushChan. This ensures at least one flush operation completes successfully after the writes were blocked.
Done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dismissed @pullrequest[bot] from a discussion.
Reviewable status: 2 of 5 files reviewed, 9 unresolved discussions (waiting on @ashish-goswami, @balajijinnah, @jarifibrahim, @manishrjain, and @pullrequest[bot])
value.go, line 1755 at r1 (raw file):
Previously, ashish-goswami (Ashish Goswami) wrote…
Thinking of raising anther PR for this.
I feel it should be part of this PR itself. Why commit some code which we know needs refactoring?
value.go, line 1769 at r1 (raw file):
Previously, pullrequest[bot] wrote…
Same as above, this should be part of the
lfDiscardStats
interface.
I also think this should be part of the lfDiscardStats
interface.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changes look good to me but the PR needs a proper description.
Reviewed 3 of 3 files at r4.
Dismissed @pullrequest[bot] from 4 discussions.
Reviewable status: all files reviewed, 9 unresolved discussions (waiting on @ashish-goswami, @balajijinnah, @manishrjain, and @pullrequest[bot])
value.go, line 1704 at r4 (raw file):
if vlog.lfDiscardStats.updatesSinceFlush > discardStatsFlushThreshold { encodedDS, _ = json.Marshal(vlog.lfDiscardStats.m) vlog.lfDiscardStats.updatesSinceFlush = 0
If the above json.Marshall(...)
call fails, we'll still set updatesSinceFlush
to 0
which is incorrect. We should skip setting updateSinceFlush
to 0
if we weren't able to marshal the stats.
value_test.go, line 486 at r4 (raw file):
// db.vlog.lfDiscardStats.updatesSinceFlush is already > discardStatsFlushThreshold, // send empty map to flushChan, so that latest discardStats map can be persited.
typo: persisted
value_test.go, line 488 at r4 (raw file):
// send empty map to flushChan, so that latest discardStats map can be persited. db.vlog.lfDiscardStats.flushChan <- map[uint32]int64{} time.Sleep(1 * time.Second) // Wait to map to be persisted.
typo: wait for ...
value_test.go, line 1003 at r4 (raw file):
} // // Regression test for https://github.com/dgraph-io/dgraph/issues/3669
This change is not necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dismissed @jarifibrahim and @pullrequest[bot] from 5 discussions.
Reviewable status: 3 of 5 files reviewed, 5 unresolved discussions (waiting on @balajijinnah, @jarifibrahim, @manishrjain, and @pullrequest[bot])
value.go, line 1704 at r4 (raw file):
Previously, jarifibrahim (Ibrahim Jarif) wrote…
If the above
json.Marshall(...)
call fails, we'll still setupdatesSinceFlush
to0
which is incorrect. We should skip settingupdateSinceFlush
to0
if we weren't able to marshal the stats.
Valid point. But I was thinking of why json.Marshal() will return error if our map is valid. Could not think of anything. Anyways now checking for it.
value_test.go, line 486 at r4 (raw file):
Previously, jarifibrahim (Ibrahim Jarif) wrote…
typo: persisted
Fixed.
value_test.go, line 488 at r4 (raw file):
Previously, jarifibrahim (Ibrahim Jarif) wrote…
typo: wait for ...
Done.
value_test.go, line 1003 at r4 (raw file):
Previously, jarifibrahim (Ibrahim Jarif) wrote…
This change is not necessary.
Fixed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor comment. get approval from @jarifibrahim
Reviewable status: 3 of 5 files reviewed, 6 unresolved discussions (waiting on @ashish-goswami, @jarifibrahim, @manishrjain, and @pullrequest[bot])
value.go, line 1704 at r4 (raw file):
if vlog.lfDiscardStats.updatesSinceFlush > discardStatsFlushThreshold { encodedDS, _ = json.Marshal(vlog.lfDiscardStats.m) vlog.lfDiscardStats.updatesSinceFlush = 0
Update the updatesSinceFlush after pushing to the sencCh successfully.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR needs a description.
Reviewable status: 3 of 5 files reviewed, 2 unresolved discussions (waiting on @ashish-goswami, @jarifibrahim, @manishrjain, and @pullrequest[bot])
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed 2 of 5 files at r1, 1 of 3 files at r4, 2 of 2 files at r5.
Reviewable status: all files reviewed, 7 unresolved discussions (waiting on @ashish-goswami and @pullrequest[bot])
value.go, line 1708 at r3 (raw file):
} func (vlog *valueLog) startFlushDiscardStats() {
Keep the name the same.
value.go, line 1695 at r5 (raw file):
mergeStats := func(stats map[uint32]int64) (encodedDS []byte) { vlog.lfDiscardStats.Lock()
Make this a RWLock. And this should acquire write lock. And readers should acquire read locks.
value.go, line 1704 at r5 (raw file):
if vlog.lfDiscardStats.updatesSinceFlush > discardStatsFlushThreshold { var err error encodedDS, err = json.Marshal(vlog.lfDiscardStats.m)
Avoid this named return.
value.go, line 1714 at r5 (raw file):
process := func(stats map[uint32]int64) error { encodedDS := mergeStats(stats)
This should return error.
value.go, line 1736 at r5 (raw file):
for { select { case <-vlog.lfDiscardStats.closer.HasBeenClosed():
closer := vlog.lf.closer
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewable status: 3 of 5 files reviewed, 7 unresolved discussions (waiting on @ashish-goswami, @manishrjain, and @pullrequest[bot])
value.go, line 1695 at r5 (raw file):
Previously, manishrjain (Manish R Jain) wrote…
Make this a RWLock. And this should acquire write lock. And readers should acquire read locks.
Done.
value.go, line 1704 at r5 (raw file):
Previously, manishrjain (Manish R Jain) wrote…
Avoid this named return.
Done.
value.go, line 1714 at r5 (raw file):
Previously, manishrjain (Manish R Jain) wrote…
This should return error.
Done.
value.go, line 1736 at r5 (raw file):
Previously, manishrjain (Manish R Jain) wrote…
closer := vlog.lf.closer
Done.
@jarifibrahim Is the description of the deadlock in #1032 correct or was there a different issue at hand? This change is pretty complex so it's difficult to determine when this was introduced and what versions may be affected |
Hey @connorgorman the description of issue #1032 was correct and this PR has fixed it. When we perform compaction, we flush discard stats back into badger. These discard stats follow the same path as normal keys (memtable -> L0 -> L1 -> ... ). In the reported issue, compaction was triggered because L0 and memtable were full. When the compaction completed, it tried to push the discard stats back into the DB but since memtable was full, we could not push the entry into badger. This was causing the deadlock. It should have been fixed by this PR. See c1cf0d7 for some more explanation. If you're looking for when this issue was introduced, it was introduced in fa0679c . |
Fixes #1032 Currently discardStats flow is as follows: * Discard Stats are generated during compaction. At the end, compaction routine updates these stats in vlog(vlog maintains all discard stats). If number of updates exceeds a threshold, a new request is generated and sent to write channel. Routine waits for request to complete(request.Wait()). * Requests are consumed from write channel and written to vlog first and then to memtable. * If memtable is full, it is flushed to flush channel. *From flush channel, memtables are written to L0 only if there are less than or equal to NumLevelZeroTablesStall tables already. Events which can lead to deadlock: Compaction is running on L0 which has NumLevelZeroTablesStall tables currently and tries to flush discard stats to write channel. After pushing stats to write channel, it waits for write request to complete, which cannot be completed due to cyclic dependency. Fix: This PR introduces a flush channel(buffered) for discardStats. Compaction routine, will push generated discard stats to flush channel, if channel is full it just returns. This decouples compaction and writes. We have a separate routine for consuming stats from flush chan. (cherry picked from commit c1cf0d7)
Fixes #1032
This change is