/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */


#include "pagespeed/kernel/thread/scheduler.h"

#include "pagespeed/kernel/base/abstract_mutex.h"
#include "pagespeed/kernel/base/basictypes.h"
#include "pagespeed/kernel/base/function.h"
#include "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/util/platform.h"
#include "pagespeed/kernel/base/thread_system.h"
#include "pagespeed/kernel/base/timer.h"
#include "pagespeed/kernel/thread/worker_test_base.h"

namespace net_instaweb {

// Many tests cribbed from mock_timer_test, only without the mockery.  This
// actually restricts the timing dependencies we can detect, though not in a
// terrible way.
class SchedulerTest : public WorkerTestBase {
 protected:
  SchedulerTest()
      : thread_system_(Platform::CreateThreadSystem()),
        timer_(thread_system_->NewTimer()),
        scheduler_(thread_system_.get(), timer_.get()) { }

  int Compare(const Scheduler::Alarm* a, const Scheduler::Alarm* b) const {
    Scheduler::CompareAlarms comparator;
    return comparator(a, b);
  }

  void LockAndProcessAlarms() {
    ScopedMutex lock(scheduler_.mutex());
    scheduler_.ProcessAlarmsOrWaitUs(0);  // Don't block!
  }

  void QuiesceAlarms(int64 timeout_us) {
    ScopedMutex lock(scheduler_.mutex());
    int64 now_us = timer_->NowUs();
    int64 end_us = now_us + timeout_us;
    while (now_us < end_us && !scheduler_.NoPendingAlarms()) {
      scheduler_.ProcessAlarmsOrWaitUs(end_us - now_us);
      now_us = timer_->NowUs();
    }
  }

  scoped_ptr<ThreadSystem> thread_system_;
  scoped_ptr<Timer> timer_;
  Scheduler scheduler_;

 private:
  DISALLOW_COPY_AND_ASSIGN(SchedulerTest);
};

namespace {

const int64 kDsUs = Timer::kSecondUs / 10;
const int64 kYearUs = Timer::kYearMs * Timer::kMsUs;

TEST_F(SchedulerTest, AlarmsGetRun) {
  int64 start_us = timer_->NowUs();
  int counter = 0;
  // Note that we give this test extra time (50ms) to start up so that
  // we don't attempt to compare already-run (and thus deleted) alarms
  // when running under valgrind.
  Scheduler::Alarm* alarm1 =
      scheduler_.AddAlarmAtUs(start_us + 52 * Timer::kMsUs,
                              new CountFunction(&counter));
  Scheduler::Alarm* alarm2 =
      scheduler_.AddAlarmAtUs(start_us + 54 * Timer::kMsUs,
                              new CountFunction(&counter));
  Scheduler::Alarm* alarm3 =
      scheduler_.AddAlarmAtUs(start_us + 53 * Timer::kMsUs,
                              new CountFunction(&counter));
  if (counter == 0) {
    // In rare cases under Valgrind, we run over the 50ms limit and the
    // callbacks get run and freed.  We skip these checks in that case.
    // Ordinarily these can be observed to run (change the above test to true ||
    // and run under valgrind to observe this).
    EXPECT_FALSE(Compare(alarm1, alarm1));
    EXPECT_FALSE(Compare(alarm2, alarm2));
    EXPECT_FALSE(Compare(alarm3, alarm3));
    EXPECT_TRUE(Compare(alarm1, alarm2));
    EXPECT_TRUE(Compare(alarm1, alarm3));
    EXPECT_FALSE(Compare(alarm2, alarm1));
    EXPECT_FALSE(Compare(alarm2, alarm3));
    EXPECT_FALSE(Compare(alarm3, alarm1));
    EXPECT_TRUE(Compare(alarm3, alarm2));
  }
  {
    ScopedMutex lock(scheduler_.mutex());
    scheduler_.BlockingTimedWaitMs(55);  // Never signaled, should time out.
  }
  int64 end_us = timer_->NowUs();
  EXPECT_EQ(3, counter);
  EXPECT_LT(start_us + 55 * Timer::kMsUs, end_us);
  // Note: we assume this will terminate within 1 min., and will have hung
  // noticeably if it didn't.
  EXPECT_GT(start_us + Timer::kMinuteUs, end_us);
}

TEST_F(SchedulerTest, MidpointBlock) {
  int64 start_us = timer_->NowUs();
  int counter = 0;
  scheduler_.AddAlarmAtUs(start_us + 2 * Timer::kMsUs,
                          new CountFunction(&counter));
  scheduler_.AddAlarmAtUs(start_us + 6 * Timer::kMsUs,
                          new CountFunction(&counter));
  scheduler_.AddAlarmAtUs(start_us + 3 * Timer::kMsUs,
                          new CountFunction(&counter));
  {
    ScopedMutex lock(scheduler_.mutex());
    scheduler_.BlockingTimedWaitMs(4);  // Never signaled, should time out.
  }
  int64 mid_us = timer_->NowUs();
  EXPECT_LT(start_us + 4 * Timer::kMsUs, mid_us);
  EXPECT_LE(2, counter);
  QuiesceAlarms(Timer::kMinuteUs);
  int64 end_us = timer_->NowUs();
  EXPECT_EQ(3, counter);
  EXPECT_LT(start_us + 6 * Timer::kMsUs, end_us);
  // Note: we assume this will terminate within 1 min., and will have hung
  // noticeably if it didn't.
  EXPECT_GT(start_us + Timer::kMinuteUs, end_us);
}

TEST_F(SchedulerTest, AlarmInPastRuns) {
  int64 start_us = timer_->NowUs();
  int counter = 0;
  scheduler_.AddAlarmAtUs(start_us - 2 * Timer::kMsUs,
                          new CountFunction(&counter));
  Scheduler::Alarm* alarm2 =
      scheduler_.AddAlarmAtUs(start_us + Timer::kMinuteUs,
                              new CountFunction(&counter));
  LockAndProcessAlarms();  // Don't block!
  EXPECT_EQ(1, counter);
  {
    ScopedMutex lock(scheduler_.mutex());
    scheduler_.CancelAlarm(alarm2);
  }
  int64 end_us = timer_->NowUs();
  EXPECT_LT(start_us, end_us);
  EXPECT_GT(start_us + Timer::kMinuteUs, end_us);
}

TEST_F(SchedulerTest, MidpointCancellation) {
  int64 start_us = timer_->NowUs();
  int counter = 0;
  scheduler_.AddAlarmAtUs(start_us + 3 * Timer::kMsUs,
                          new CountFunction(&counter));
  scheduler_.AddAlarmAtUs(start_us + 2 * Timer::kMsUs,
                          new CountFunction(&counter));
  Scheduler::Alarm* alarm3 =
      scheduler_.AddAlarmAtUs(start_us + Timer::kMinuteUs,
                              new CountFunction(&counter));
  {
    ScopedMutex lock(scheduler_.mutex());
    scheduler_.BlockingTimedWaitMs(4);  // Never signaled, should time out.
  }
  int64 mid_us = timer_->NowUs();
  EXPECT_LT(start_us + 4 * Timer::kMsUs, mid_us);
  EXPECT_EQ(2, counter);
  // No longer safe to cancel first two alarms.
  {
    ScopedMutex lock(scheduler_.mutex());
    scheduler_.CancelAlarm(alarm3);
  }
  QuiesceAlarms(Timer::kMinuteUs);
  int64 end_us = timer_->NowUs();
  EXPECT_EQ(-98, counter);
  EXPECT_LT(start_us + 3 * Timer::kMsUs, end_us);
  // Note: we assume this will terminate within 1 min., and will have hung
  // noticeably if it didn't.
  EXPECT_GT(start_us + Timer::kMinuteUs, end_us);
}

TEST_F(SchedulerTest, SimultaneousAlarms) {
  int64 start_us = timer_->NowUs();
  int counter = 0;
  scheduler_.AddAlarmAtUs(start_us + 2 * Timer::kMsUs,
                          new CountFunction(&counter));
  scheduler_.AddAlarmAtUs(start_us + 2 * Timer::kMsUs,
                          new CountFunction(&counter));
  scheduler_.AddAlarmAtUs(start_us + 2 * Timer::kMsUs,
                          new CountFunction(&counter));
  QuiesceAlarms(Timer::kMinuteUs);
  int64 end_us = timer_->NowUs();
  EXPECT_EQ(3, counter);
  EXPECT_LT(start_us + 2 * Timer::kMsUs, end_us);
  // Note: we assume this will terminate within 1 min., and will have hung
  // noticeably if it didn't.
  EXPECT_GT(start_us + Timer::kMinuteUs, end_us);
}

TEST_F(SchedulerTest, TimedWaitExpire) {
  int64 start_us = timer_->NowUs();
  int counter = 0;
  {
    ScopedMutex lock(scheduler_.mutex());
    scheduler_.TimedWaitMs(2, new CountFunction(&counter));
    scheduler_.TimedWaitMs(4, new CountFunction(&counter));
    scheduler_.TimedWaitMs(3, new CountFunction(&counter));
    scheduler_.BlockingTimedWaitMs(5);
  }
  int64 end_us = timer_->NowUs();
  EXPECT_EQ(3, counter);
  EXPECT_LT(start_us + 5 * Timer::kMsUs, end_us);
  // Note: we assume this will terminate within 1 min., and will have hung
  // noticeably if it didn't.
  EXPECT_GT(start_us + Timer::kMinuteUs, end_us);
}

TEST_F(SchedulerTest, TimedWaitSignal) {
  int64 start_us = timer_->NowUs();
  int counter = 0;
  {
    ScopedMutex lock(scheduler_.mutex());
    scheduler_.TimedWaitMs(2, new CountFunction(&counter));
    scheduler_.TimedWaitMs(4, new CountFunction(&counter));
    scheduler_.TimedWaitMs(3, new CountFunction(&counter));
    scheduler_.Signal();
  }
  int64 end_us = timer_->NowUs();
  EXPECT_EQ(3, counter);
  // Note: we assume this will terminate within 1 min., and will have hung
  // noticeably if it didn't.
  EXPECT_GT(start_us + Timer::kMinuteUs, end_us);
}

TEST_F(SchedulerTest, TimedWaitMidpointSignal) {
  int64 start_us = timer_->NowUs();
  int counter = 0;
  {
    ScopedMutex lock(scheduler_.mutex());
    scheduler_.TimedWaitMs(3, new CountFunction(&counter));
    scheduler_.TimedWaitMs(2, new CountFunction(&counter));
    scheduler_.TimedWaitMs(Timer::kYearMs, new CountFunction(&counter));
    scheduler_.BlockingTimedWaitMs(4);  // Will time out
    EXPECT_EQ(2, counter);
    scheduler_.Signal();
  }
  int64 end_us = timer_->NowUs();
  EXPECT_EQ(3, counter);
  // Note: we assume this will terminate within 1 min., and will have hung
  // noticeably if it didn't.
  EXPECT_GT(start_us + Timer::kMinuteUs, end_us);
}

// Function that retries a TimedWait when invoked until 10ms have passed.
class RetryWaitFunction : public Function {
 public:
  RetryWaitFunction(Timer* timer, int64 start_ms, Scheduler* scheduler,
                    int* counter)
      : timer_(timer), start_ms_(start_ms),
        scheduler_(scheduler), counter_(counter) {
  }

  virtual ~RetryWaitFunction() {}

  virtual void Run() EXCLUSIVE_LOCKS_REQUIRED(scheduler_->mutex()) {
    ++*counter_;
    if ((timer_->NowMs() - start_ms_) < 10) {
      // Note that we want the retry delay here to place us later than
      // the original timeout the first invocation had, as that will
      // place us later inside the wait queue ordering. In the past,
      // that would cause Signal() to instantly detect us in the queue
      // and run us w/o returning control.
      scheduler_->TimedWaitMs(
          10, new RetryWaitFunction(timer_, start_ms_, scheduler_, counter_));
    }
  }

 private:
  Timer* timer_;
  int64 start_ms_;
  Scheduler* scheduler_;
  int* counter_;
  DISALLOW_COPY_AND_ASSIGN(RetryWaitFunction);
};

TEST_F(SchedulerTest, TimedWaitFromSignalWakeup) {
  int counter = 0;
  int64 start_ms = timer_->NowMs();
  {
    ScopedMutex lock(scheduler_.mutex());
    scheduler_.TimedWaitMs(
        5, new RetryWaitFunction(timer_.get(), start_ms, &scheduler_,
                                 &counter));
    scheduler_.Signal();
  }
  QuiesceAlarms(20 * Timer::kMsUs);
  EXPECT_GE(2, counter);
}

}  // namespace

}  // namespace net_instaweb
