gecko-dev/netwerk/ipc/ChannelEventQueue.cpp
Shih-Chiang Chien e6a79fe88f Bug 1426751 - Prevent FlushQueue to be invoked on multiple threads simultaneously. r=mayhemer
This issue is triggered by off-main-thread ODA listener that return error cause from OnDataAvailable callback.
A CancelEvent will be prepend to event queue and trigger race condition between CompleteResume and EndForceEnqueueing.

The `mFlushing` is checked and set in separate critical sections, therefore two threads that executing MayFlushQueue
might both pass the `mFlushing` check and trying to call FlushQueue simultaneously.

The solution is to check and set `mFlushing` in single critical section, so we can guarantee that only one FlushQueue
can be executed at anytime.

In addition, resumption is postponed until no AutoEventEnqueuer is activated. Therefore, CompleteResume will only be
triggered while all the suspension requests and auto enqueue requests are finished.

MozReview-Commit-ID: HpxzgUqYm8C

--HG--
extra : rebase_source : 9be0d37bb4475a0817dafb6270585263160a9da1
2018-01-04 18:53:02 +08:00

186 lines
4.6 KiB
C++

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
* vim: set sw=2 ts=8 et tw=80 :
*/
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "ChannelEventQueue.h"
#include "mozilla/Assertions.h"
#include "mozilla/Unused.h"
#include "nsISupports.h"
#include "nsThreadUtils.h"
namespace mozilla {
namespace net {
ChannelEvent*
ChannelEventQueue::TakeEvent()
{
mMutex.AssertCurrentThreadOwns();
MOZ_ASSERT(mFlushing);
if (mSuspended || mEventQueue.IsEmpty()) {
return nullptr;
}
UniquePtr<ChannelEvent> event(Move(mEventQueue[0]));
mEventQueue.RemoveElementAt(0);
return event.release();
}
void
ChannelEventQueue::FlushQueue()
{
// Events flushed could include destruction of channel (and our own
// destructor) unless we make sure its refcount doesn't drop to 0 while this
// method is running.
nsCOMPtr<nsISupports> kungFuDeathGrip(mOwner);
mozilla::Unused << kungFuDeathGrip; // Not used in this function
#ifdef DEBUG
{
MutexAutoLock lock(mMutex);
MOZ_ASSERT(mFlushing);
}
#endif // DEBUG
bool needResumeOnOtherThread = false;
while (true) {
UniquePtr<ChannelEvent> event;
{
MutexAutoLock lock(mMutex);
event.reset(TakeEvent());
if (!event) {
MOZ_ASSERT(mFlushing);
mFlushing = false;
MOZ_ASSERT(mEventQueue.IsEmpty() || (mSuspended || !!mForcedCount));
break;
}
}
nsCOMPtr<nsIEventTarget> target = event->GetEventTarget();
MOZ_ASSERT(target);
bool isCurrentThread = false;
nsresult rv = target->IsOnCurrentThread(&isCurrentThread);
if (NS_WARN_IF(NS_FAILED(rv))) {
// Simply run this event on current thread if we are not sure about it
// in release channel, or assert in Aurora/Nightly channel.
MOZ_DIAGNOSTIC_ASSERT(false);
isCurrentThread = true;
}
if (!isCurrentThread) {
// Next event needs to run on another thread. Put it back to
// the front of the queue can try resume on that thread.
Suspend();
PrependEvent(event);
needResumeOnOtherThread = true;
{
MutexAutoLock lock(mMutex);
MOZ_ASSERT(mFlushing);
mFlushing = false;
MOZ_ASSERT(!mEventQueue.IsEmpty());
}
break;
}
event->Run();
} // end of while(true)
// The flush procedure is aborted because next event cannot be run on current
// thread. We need to resume the event processing right after flush procedure
// is finished.
// Note: we cannot call Resume() while "mFlushing == true" because
// CompleteResume will not trigger FlushQueue while there is an ongoing flush.
if (needResumeOnOtherThread) {
Resume();
}
}
void
ChannelEventQueue::Suspend()
{
MutexAutoLock lock(mMutex);
SuspendInternal();
}
void
ChannelEventQueue::SuspendInternal()
{
mMutex.AssertCurrentThreadOwns();
mSuspended = true;
mSuspendCount++;
}
void ChannelEventQueue::Resume()
{
MutexAutoLock lock(mMutex);
ResumeInternal();
}
void
ChannelEventQueue::ResumeInternal()
{
mMutex.AssertCurrentThreadOwns();
// Resuming w/o suspend: error in debug mode, ignore in build
MOZ_ASSERT(mSuspendCount > 0);
if (mSuspendCount <= 0) {
return;
}
if (!--mSuspendCount) {
if (mEventQueue.IsEmpty() || !!mForcedCount) {
// Nothing in queue to flush or waiting for AutoEventEnqueuer to
// finish the force enqueue period, simply clear the flag.
mSuspended = false;
return;
}
// Hold a strong reference of mOwner to avoid the channel release
// before CompleteResume was executed.
class CompleteResumeRunnable : public CancelableRunnable
{
public:
explicit CompleteResumeRunnable(ChannelEventQueue* aQueue, nsISupports* aOwner)
: CancelableRunnable("CompleteResumeRunnable")
, mQueue(aQueue)
, mOwner(aOwner)
{
}
NS_IMETHOD Run() override
{
mQueue->CompleteResume();
return NS_OK;
}
private:
virtual ~CompleteResumeRunnable() {}
RefPtr<ChannelEventQueue> mQueue;
nsCOMPtr<nsISupports> mOwner;
};
// Worker thread requires a CancelableRunnable.
RefPtr<Runnable> event = new CompleteResumeRunnable(this, mOwner);
nsCOMPtr<nsIEventTarget> target;
target = mEventQueue[0]->GetEventTarget();
MOZ_ASSERT(target);
Unused << NS_WARN_IF(NS_FAILED(target->Dispatch(event.forget(),
NS_DISPATCH_NORMAL)));
}
}
} // namespace net
} // namespace mozilla