-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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(s) while unloading/loading tracks #2305
Fix race condition(s) while unloading/loading tracks #2305
Conversation
Pending read requests are discarded when unloading a track. But their response might arrive when the new track has already been loaded. This would reset the readable frame index range to empty and the new track plays with silence. Also, the FIFO between the worker and the reader needs to be drained entirely when unloading a track. No new read requests must be accepted until the new track has been loaded.
Naming is still a big mess. But I have extracted the state of the reader into its own The main cause for the bug was the invalid ordering of updates by the worker thread, i.e. first inserting the track loaded message, followed by invalid, discarded chunks from pending read requests for the previous track. |
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.
Thank you for wrapping your brain around it. I have left some comments.
// Don't accept any new read requests until the current | ||
// track has been unloaded and the new track has been | ||
// loaded! | ||
m_state = State::TrackLoading; |
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.
Should we set this bit before touching the worker?
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.
Not needed. The worker thread is not intercepting the reader. The reader polls for updates. But if it helps to understand what is happening I will reorder the instructions.
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 is even desired to inform the worker asap. A new comment explains why.
src/engine/cachingreader.cpp
Outdated
// All chunks have been freed before loading the next track! | ||
DEBUG_ASSERT(!m_mruCachingReaderChunk); | ||
DEBUG_ASSERT(!m_lruCachingReaderChunk); | ||
// Discard all pending read requests for the previous track |
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.
// Discard all results from pending read requests
m_readableFrameIndexRange = intersect( | ||
m_readableFrameIndexRange, | ||
update.readableFrameIndexRange()); | ||
} | ||
} else { |
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.
Are we sure that we hit always the else branch? We need to receive status update without a chunk.
I think it would be better to have that decoupled
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.
Yes. This is caused by the coupled design. But I won't change that. The debug assertions clearly indicate that there are two kinds of updates:
- Read result with a chunk
- State change track loaded/unloaded without a chunk
update.init(TRACK_NOT_LOADED); | ||
// Discard all pending read requests | ||
CachingReaderChunkReadRequest request; | ||
while (m_pChunkReadRequestFIFO->read(&request, 1) == 1) { |
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.
is the fifo multi consumer aware? I think we need to move this into the run() method and the if (m_newTrackAvailable) { branch.
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.
No and it doesn't have to be. loadTrack() is private and only invoked from run() within the same thread.
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.
Ah yes, I misread and took it for newTrack()
I have only added or modified comments, no code changes needed IMHO. |
LGTM, thanks. |
I think this PR rectifies a point release NOW. |
I don't think so. It has probably introduced after 2.2.2, I need to check. |
I agree, let's first finish the 2 outstanding PRs. |
I got a debug asserting in |
OMG. I didn't notice the enforced direct connections between worker and reader that both live on separate threads. These are wrong!! Or am I missing something??? Moreover, the reader should be responsible for emitting signals, but not the worker. Otherwise, the state of the reader and the emitted signals are inconsistent due to race conditions. I should quit and stop working on this evil and cursed code, it doesn't get any better. |
} | ||
|
||
void CachingReader::process() { | ||
ReaderStatusUpdate update; | ||
while (m_readerStatusFIFO.read(&update, 1) == 1) { | ||
while (m_stateFIFO.read(&update, 1) == 1) { | ||
DEBUG_ASSERT(m_state != State::Idle); |
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 one always fails after ejecting a track and loading a new one.
This is normal for every update.status == TRACK_LOADED message.
So it can be removed.
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.
- After TRACK_LOADED the state becomes State::TrackLoaded
- After TRACK_UNLOADED the state becomes State::Idle and no new updates are expected.
While the reader is idle no update messages are expected!
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 debug assertion is there for a reason. Idle means don't bother me with update messages or something is seriously wrong.
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 might be true after your recent changes in the other PR, but this assumption is here wrong.
Try it out. I am still wrapping my head around you new code but I want first understand the original issue.
Was this your assertion as well?
I'm not able to reproduce the bug. Awaiting confirmation that this PR fixes the bug!
https://bugs.launchpad.net/mixxx/+bug/1845695
Not a single, but multiple potential race conditions and wrong ordering of responses while unloading and loading tracks!
Pending read requests are discarded when unloading a track. But their response arrived when the new track had already been loaded. This would reset the readable frame index range to empty and the new track plays with silence.
Also, the FIFO between the worker and the reader needs to be drained entirely when unloading a track. No new read requests must be accepted until the new track has been loaded.
I've added many debug assertions to detect any inconsistencies and wrong assumptions.