/*
 * 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/cache/purge_context.h"

#include "base/logging.h"
#include "pagespeed/kernel/base/abstract_mutex.h"
#include "pagespeed/kernel/base/atomic_bool.h"
#include "pagespeed/kernel/base/basictypes.h"
#include "pagespeed/kernel/base/file_system.h"
#include "pagespeed/kernel/base/function.h"
#include "pagespeed/kernel/base/message_handler.h"
#include "pagespeed/kernel/base/null_message_handler.h"
#include "pagespeed/kernel/base/named_lock_manager.h"
#include "pagespeed/kernel/base/statistics.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/base/thread_annotations.h"
#include "pagespeed/kernel/base/thread_system.h"
#include "pagespeed/kernel/base/time_util.h"
#include "pagespeed/kernel/cache/lru_cache_base.h"
#include "pagespeed/kernel/thread/scheduler.h"
#include "pagespeed/kernel/util/copy_on_write.h"

namespace net_instaweb {

namespace {

const int64 kStealLockAfterMs = 2 * Timer::kSecondMs;
const int64 kTimeoutMs = 3 * Timer::kSecondMs;
const int kMaxContentionRetries = 2;

}  // namespace

const char PurgeContext::kCancellations[]        = "purge_cancellations";
const char PurgeContext::kContentions[]          = "purge_contentions";
const char PurgeContext::kFileParseFailures[]    = "purge_file_parse_failures";
const char PurgeContext::kFileStats[]            = "purge_file_stats";
const char PurgeContext::kFileWriteFailures[]    = "purge_file_write_failures";
const char PurgeContext::kFileWrites[]           = "purge_file_writes";
const char PurgeContext::kPurgeIndex[]           = "purge_index";

// TODO(jmarantz): make it possible to avoid showing this implementation detail
// in the statistics page.
const char PurgeContext::kPurgePollTimestampMs[] = "_purge_poll_timestamp_ms";

namespace {

// Wrapper class around an UpDownCounter which makes it guaranteed to be
// locally consistent, even if the backing UpDownCounter is non-functional
// due to a shared-memory failure or statistics being turned off.
class BackupUpDownCounter : public UpDownCounter {
 public:
  // Takes ownership of mutex.
  BackupUpDownCounter(UpDownCounter* counter, AbstractMutex* mutex)
      : counter_(counter),
        mutex_(mutex),
        local_counter_(0) {
  }

  virtual int64 Get() const {
    // Don't take the lock for local_counter unless we have actually set some
    // value in the past, yet we can't retrieve it from the real UpDownCounter.
    bool was_set = was_set_.value();
    int64 val = counter_->Get();
    if (was_set && (val == 0)) {
      ScopedMutex lock(mutex_.get());
      val = local_counter_;
    }
    return val;
  }

  virtual StringPiece GetName() const { return counter_->GetName(); }

  virtual void Set(int64 value) {
    counter_->Set(value);
    {
      ScopedMutex lock(mutex_.get());
      local_counter_ = value;
      was_set_.set_value(true);
    }
  }

  virtual int64 AddHelper(int64 delta) {
    LOG(DFATAL) << "AddHelper is not used in PurgeContext";
    return 0;
  }

 private:
  UpDownCounter* counter_;
  scoped_ptr<AbstractMutex> mutex_;
  int64 local_counter_ GUARDED_BY(mutex_);
  AtomicBool was_set_;
};

}  // namespace

PurgeContext::PurgeContext(StringPiece filename,
                           FileSystem* file_system,
                           Timer* timer,
                           int max_bytes_in_cache,
                           ThreadSystem* thread_system,
                           NamedLockManager* lock_manager,
                           Scheduler* scheduler,
                           Statistics* statistics,
                           MessageHandler* handler)
    : filename_(filename.data(), filename.size()),
      interprocess_lock_(lock_manager->CreateNamedLock(LockName())),
      file_system_(file_system),
      timer_(timer),
      statistics_(statistics),
      mutex_(thread_system->NewMutex()),
      pending_purges_(max_bytes_in_cache),
      local_purge_index_(0),
      num_consecutive_failures_(0),
      waiting_for_interprocess_lock_(false),
      reading_(false),
      enable_purge_(true),
      max_bytes_in_cache_(max_bytes_in_cache),
      request_batching_delay_ms_(0),
      cancellations_(statistics->GetVariable(kCancellations)),
      contentions_(statistics->GetVariable(kContentions)),
      file_parse_failures_(statistics->GetVariable(kFileParseFailures)),
      file_stats_(statistics->GetVariable(kFileStats)),
      file_write_failures_(statistics->GetVariable(kFileWriteFailures)),
      file_writes_(statistics->GetVariable(kFileWrites)),
      purge_index_(statistics->GetVariable(kPurgeIndex)),
      purge_poll_timestamp_ms_(new BackupUpDownCounter(
          statistics->GetUpDownCounter(kPurgePollTimestampMs),
          thread_system->NewMutex())),
      scheduler_(scheduler),
      message_handler_(handler) {
  purge_set_.MakeWriteable()->set_max_size(max_bytes_in_cache_);
}

PurgeContext::~PurgeContext() {
}

void PurgeContext::InitStats(Statistics* statistics) {
  statistics->AddVariable(kCancellations);
  statistics->AddVariable(kContentions);
  statistics->AddVariable(kFileParseFailures);
  statistics->AddVariable(kFileStats);
  statistics->AddVariable(kFileWrites);
  statistics->AddVariable(kFileWriteFailures);
  statistics->AddVariable(kPurgeIndex);
  statistics->AddUpDownCounter(kPurgePollTimestampMs);
}

bool PurgeContext::ParseAndValidateTimestamp(
    StringPiece time_string, int64 now_ms, int64* timestamp_ms) {
  if (!StringToInt64(time_string, timestamp_ms)) {
    message_handler_->Info(filename_.c_str(), 1,
                           "Invalidation timestamp (%s) not parsed as int64",
                           time_string.as_string().c_str());
    return false;
  } else if ((*timestamp_ms != PurgeSet::kInitialTimestampMs) &&
             ((*timestamp_ms < 0) ||
              (*timestamp_ms > now_ms + PurgeSet::kClockSkewAllowanceMs))) {
    GoogleString converted_time_string;
    ConvertTimeToString(*timestamp_ms, &converted_time_string);
    message_handler_->Info(filename_.c_str(), 1,
                           "Invalidation timestamp (%s) in the future: %s",
                           time_string.as_string().c_str(),
                           converted_time_string.c_str());
    return false;
  }
  return true;
}

// Parses the cache purge file.
void PurgeContext::ReadPurgeFile(PurgeSet* purges_from_file) {
  GoogleString buffer;

  file_stats_->Add(1);
  NullMessageHandler null_handler;

  // Prior to mod_pagespeed 1.8, the cache.flush file's contents were not
  // significant, and generally empty, and only the timestamp of the file
  // itself was important, meaning "wipe everything out of the cache predating
  // that timestamp."
  if (!enable_purge_) {
    int64 timestamp_sec;
    if (file_system_->Mtime(filename_, &timestamp_sec, &null_handler)) {
      int64 timestamp_ms = timestamp_sec * Timer::kSecondMs;
      purges_from_file->UpdateGlobalInvalidationTimestampMs(timestamp_ms);
    }
    return;
  }

  if (!file_system_->ReadFile(filename_.c_str(), &buffer, &null_handler)) {
    // If the file simply doesn't exist, that's a 'successful' read.  It's
    // fine for there to be no cache file and no invalidation data, and thus
    // we swallow file-not-found messages with NullMessageHandler.
    return;
  }
  StringPieceVector lines;
  SplitStringPieceToVector(buffer, "\n", &lines, true);
  int64 timestamp_ms = 0;
  int64 now_ms = timer_->NowMs();

  // The first line should contain the global invalidation timestamp,
  // though we'll just silently leave the invalidation timestamp unchanged
  // if the file was empty.
  if (lines.empty() || !ParseAndValidateTimestamp(lines[0], now_ms,
                                                  &timestamp_ms)) {
    file_parse_failures_->Add(1);
    return;
  }
  purges_from_file->UpdateGlobalInvalidationTimestampMs(timestamp_ms);

  for (int i = 1, n = lines.size(); i < n; ++i) {
    // Each line is in the form "TIMESTAMP_MS URL".  The url
    // has no character restrictions in this file format, and terminates at
    // newline.  The timestamp is a 64-bit decimal number measured in
    // milliseconds since 1970.
    StringPiece line = lines[i];
    if (!line.empty()) {  // skip empty lines but include them in line-count.
      stringpiece_ssize_type pos = line.find(' ');
      if ((pos == StringPiece::npos) ||
          !ParseAndValidateTimestamp(line.substr(0, pos), now_ms,
                                     &timestamp_ms)) {
        file_parse_failures_->Add(1);
      } else {
        StringPiece url = line.substr(pos + 1);
        purges_from_file->Put(url.as_string(), timestamp_ms);
      }
    }
  }
}

// While still holding the interprocess lock, verify that the bytes in
// the file are the ones we wrote.  If another process stole the lock
// and overwrote our bytes, we'll simply schedule another try, which will
// merge in whatever results they had written.
bool PurgeContext::Verify(const GoogleString& expected_purge_file_contents) {
  GoogleString verify;
  return (file_system_->ReadFile(filename_.c_str(), &verify,
                                 message_handler_) &&
          (verify == expected_purge_file_contents));
}

void PurgeContext::UpdateCachePurgeFile() {
  // Use a global lock to perform an atomic read/modify/write.
  //
  // Note that the lock is advisory, and if it gets stolen, then we'll
  // have a contended update to the file and potentially lose some
  // purge records, at least temporarily.
  DCHECK(interprocess_lock_->Held());
  DCHECK(waiting_for_interprocess_lock_);
  PurgeSet purges_from_file(max_bytes_in_cache_);
  PurgeSet return_purges(max_bytes_in_cache_);
  PurgeCallbackVector callbacks;
  bool lock_and_update = false;
  bool success = true;
  int failures = 0;

  // Initiate a read/modify/write/verify sequence while holding
  // interprocess_lock_.  Note that during 'modify' we need to
  // also grab mutex_, so we'll need to collect the serizlized
  // buffer and callback-list at the same time for atomicity.
  GoogleString buffer, verify;
  ReadPurgeFile(&purges_from_file);                                   // read
  ModifyPurgeSet(&purges_from_file, &buffer, &callbacks,
                 &return_purges, &failures);                          // modify
  if (!WritePurgeFile(buffer) ||                                      // write
      !Verify(buffer)) {                                              // verify
    contentions_->Add(1);
    success = false;
    HandleWriteFailure(failures, &callbacks, &return_purges, &lock_and_update);
  }

  interprocess_lock_->Unlock();

  if (!callbacks.empty()) {
    if (success) {
      // Induce a file-read the next time PollFileSystem() is called.
      // Note that there is a small chance we might read the same
      // version of the file twice if we get a PollFileSystem()
      // request with 5 seconds since the last check, after writing
      // the file and before this line.  However that redundant read
      // is not harmful.
      purge_index_->Add(1);
    }
    for (int i = 0, n = callbacks.size(); i < n; ++i) {
      callbacks[i]->Run(success, "");
    }
  } else if (lock_and_update) {
    GrabLockAndUpdate();
  }
}

void PurgeContext::HandleWriteFailure(int failures,
                                      PurgeCallbackVector* callbacks,
                                      PurgeSet* return_purges,
                                      bool* lock_and_update) {
  ScopedMutex lock(mutex_.get());

  num_consecutive_failures_ += failures + 1;
  if (num_consecutive_failures_ <= kMaxContentionRetries) {
    // Since we relinquished the lock prior to verifying, another
    // thread may have added more purge requests prior to re-acquiring
    // the lock.  In either case we need to restore the callbacks so
    // we can try again and notify interested parties.
    if (waiting_for_interprocess_lock_) {
      DCHECK(!pending_callbacks_.empty());
      pending_callbacks_.insert(pending_callbacks_.end(), callbacks->begin(),
                                callbacks->end());
      pending_purges_.Merge(*return_purges);
      callbacks->clear();
    } else {
      DCHECK(pending_callbacks_.empty());
      DCHECK(!callbacks->empty());
      waiting_for_interprocess_lock_ = true;
      callbacks->swap(pending_callbacks_);
      pending_purges_.Swap(return_purges);
      *lock_and_update = true;
    }
  } else {
    // Either the file-write failed or the read-verify mismatched.
    //
    // This might be due to a permissions problem, in which case
    // we might never succeed, or it might be a transient issue
    // due to a broken lock contention.  In either case, we will
    // give up and report failure.
    //
    // Note: the file-system itself already reported Info messages on
    // whatever the error was so we don't need another error here, but we'll
    // return a 404 or whatever to the PURGE request.
    //
    // TODO(jmarantz): capture the file-system logs and return those
    // to PURGE clients as the response body.
    file_write_failures_->Add(callbacks->size());
    num_consecutive_failures_ = 0;
  }
}

void PurgeContext::ModifyPurgeSet(PurgeSet* purges_from_file,
                                  GoogleString* buffer,
                                  PurgeCallbackVector* return_callbacks,
                                  PurgeSet* return_purges,
                                  int* failures) {
  // Note that while were are reading the file, another Purge might
  // arrive in pending_purges_, protected by mutex_.  We avoid holding
  // the that mutex when reading/writing the file as that might create
  // contention, with IsValid requests in other threads.  But we must
  // hold the mutex during merges.
  ScopedMutex lock(mutex_.get());

  // TODO(jmarantz): as we do not currently merge in purge_set_ as
  // consider the Source Of Truth to be the contents of the file plus
  // the pending purges.  However I would like to add some resilience
  // to file corruption, which might be partially addressed by putting
  // in 'purges_from_file->Merge(purge_set_);' here.  However we
  // should have testcases for how the recovery would work and be
  // transmitted back to the file system and to all servers in
  // a bounded amount of time.

  purges_from_file->Merge(pending_purges_);
  return_purges->Swap(&pending_purges_);
  pending_purges_.Clear();
  waiting_for_interprocess_lock_ = false;
  // Now if another Purge arrives it will have to wait for the next lock
  // again.

  // Collect the write-buffer from the aggregated PurgeSet while we
  // have mutex_ held.
  StrAppend(buffer, Integer64ToString(
      purges_from_file->global_invalidation_timestamp_ms()),
            "\n");
  for (PurgeSet::Iterator p = purges_from_file->Begin(),
           e = purges_from_file->End();
       p != e; ++p) {
    StrAppend(buffer, Integer64ToString(p.Value()), " ", p.Key(), "\n");
  }

  // We must also collect up and return callbacks for the requests we have
  // now merged, since once the lock is released more Purge requests may
  // come in and we don't want to call their callbacks prematurely.
  //
  // Note we can't call these callbacks until we've written the serialized
  // form to disk and verified that the data indeed hit the disk.
  return_callbacks->swap(pending_callbacks_);

  // Same with the number of failures.  They need to be removed from
  // our mutex-protected world now and re-accumulated under mutex if
  // another failure ensues.
  *failures = num_consecutive_failures_;
  num_consecutive_failures_ = 0;
}

bool PurgeContext::WritePurgeFile(const GoogleString& buffer) {
  // Atomicly write file so we don't have to acquire the lock to read it.
  file_writes_->Add(1);
  return file_system_->WriteFileAtomic(filename_, buffer, message_handler_);
}

// There is a non-zero chance that this thread will be unable to
// acquire the named-lock in the given time-limit.  We'll just bump up
// the cancellation count.
void PurgeContext::CancelCachePurgeFile() {
  PurgeCallbackVector callbacks;
  {
    ScopedMutex lock(mutex_.get());
    callbacks.swap(pending_callbacks_);

    // We are giving up on these pending invalidations rather than having
    // them take effect at some random time in the future.
    pending_purges_.Clear();

    waiting_for_interprocess_lock_ = false;
  }

  // All the purges in the queue failed :(.  Maybe the clients
  // will retry.
  cancellations_->Add(callbacks.size());
  for (int i = 0, n = callbacks.size(); i < n; ++i) {
    callbacks[i]->Run(false, "timeout");
  }
}

void PurgeContext::WaitForTimerAndGrabLock() {
  if (request_batching_delay_ms_ == 0) {
    GrabLockAndUpdate();
  } else {
    int64 alarm_time_us =
        timer_->NowUs() + request_batching_delay_ms_ * Timer::kMsUs;
    scheduler_->AddAlarmAtUs(alarm_time_us,
                             MakeFunction(this,
                                          &PurgeContext::GrabLockAndUpdate,
                                          &PurgeContext::CancelCachePurgeFile));
  }
}

void PurgeContext::GrabLockAndUpdate() {
  interprocess_lock_->LockTimedWaitStealOld(
      kTimeoutMs, kStealLockAfterMs,
      MakeFunction(this,
                   &PurgeContext::UpdateCachePurgeFile,
                   &PurgeContext::CancelCachePurgeFile));
}

void PurgeContext::SetCachePurgeGlobalTimestampMs(
    int64 timestamp_ms, PurgeCallback* callback) {
  bool grab_lock = false;
  {
    ScopedMutex lock(mutex_.get());
    pending_purges_.UpdateGlobalInvalidationTimestampMs(timestamp_ms);
    if (!waiting_for_interprocess_lock_) {
      waiting_for_interprocess_lock_ = true;
      grab_lock = true;
    }
    pending_callbacks_.push_back(callback);
  }
  if (grab_lock) {
    WaitForTimerAndGrabLock();
  }
}

void PurgeContext::AddPurgeUrl(StringPiece url, int64 timestamp_ms,
                               PurgeCallback* callback) {
  if (!enable_purge_) {
    callback->Run(false, "EnableCachePurge is off");
  } else {
    bool grab_lock = false;
    {
      ScopedMutex lock(mutex_.get());
      pending_purges_.Put(url.as_string(), timestamp_ms);
      if (!waiting_for_interprocess_lock_) {
        waiting_for_interprocess_lock_ = true;
        grab_lock = true;
      }
      pending_callbacks_.push_back(callback);
    }
    if (grab_lock) {
      WaitForTimerAndGrabLock();
    }
  }
}

void PurgeContext::PollFileSystem() {
  int64 now_ms = timer_->NowMs();
  int64 delta_ms = now_ms - purge_poll_timestamp_ms_->Get();
  int64 global_purge_index = purge_index_->Get();
  mutex_->Lock();
  bool needs_update = (local_purge_index_ < global_purge_index);
  if (!reading_ &&
      (needs_update || (delta_ms >= kCheckCacheIntervalMs))) {
    if (needs_update) {
      local_purge_index_ = global_purge_index;
    }
    reading_ = true;
    mutex_->Unlock();
    purge_poll_timestamp_ms_->Set(now_ms);
    ReadFileAndCallCallbackIfChanged(needs_update);
    mutex_->Lock();
    reading_ = false;
  }
  mutex_->Unlock();
}

void PurgeContext::ReadFileAndCallCallbackIfChanged(bool needs_update) {
  CopyOnWrite<PurgeSet> purges_from_file;
  PurgeSet* mutable_purges_from_file = purges_from_file.MakeWriteable();
  mutable_purges_from_file->set_max_size(max_bytes_in_cache_);
  bool call_callback = false;

  // Note that we don't hold the global lock while reading the file.
  // But under mutex we have set reading_ so another thread doesn't
  // try a concurrent read.
  DCHECK(reading_);
  ReadPurgeFile(mutable_purges_from_file);

  {
    ScopedMutex lock(mutex_.get());
    if (!purge_set_->Equals(*purges_from_file)) {
      if (!needs_update) {
        // This update was induced by a timeout in this process, rather
        // than by a signal from another process or an UpdateCachePurgeFile
        // in this process.  This signals to other child processes that their
        // local_purge_index_ state is now stale and they must re-read.  We
        // only do this if the purge-set from disk actually changed.
        purge_index_->Add(1);
      }
      purge_set_ = purges_from_file;
      if (update_callback_ != NULL) {
        // We don't want to call the update callback while holding the
        // lock.  Also note that even though we will release the lock
        // before calling the callback, purge_set_ will not be mutated
        // until our caller sets reading_ to false.
        call_callback = true;
      }
    }
  }
  if (call_callback) {
    update_callback_->Run(purges_from_file);
  }

  // Note that we don't merge in pending_purges_ in this flow.  Only
  // when we grab the lock and write the file do we merge in the
  // pending_purges_.
}

void PurgeContext::SetUpdateCallback(PurgeSetCallback* cb) {
  update_callback_.reset(cb);
}

}  // namespace net_instaweb
