/*
 * 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/sharedmem/shared_mem_lock_manager.h"

#include <cstddef>

#include "base/logging.h"
#include "pagespeed/kernel/base/abstract_mutex.h"
#include "pagespeed/kernel/base/abstract_shared_mem.h"
#include "pagespeed/kernel/base/basictypes.h"
#include "pagespeed/kernel/base/hasher.h"
#include "pagespeed/kernel/base/message_handler.h"
#include "pagespeed/kernel/base/scoped_ptr.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/base/timer.h"
#include "pagespeed/kernel/thread/scheduler.h"
#include "pagespeed/kernel/thread/scheduler_based_abstract_lock.h"

namespace net_instaweb {

namespace SharedMemLockData {

// Memory structure:
//
// Bucket 0:
//  Slot 0
//     lock name hash (64-bit)
//     acquire timestamp (64-bit)
//  Slot 1
//  ...
//  Slot kSlotsPerBucket - 1
//  Mutex
//  (pad to 64-byte alignment)
// Bucket 1:
//  ..
// Bucket kBuckets - 1:
//  ..
//
// Each key is statically assigned to a bucket based on its hash.
// When we're trying to lock or unlock the given named lock, we lock
// the corresponding bucket.
//
// Whenever a lock is held, some slot in the corresponding bucket has its hash
// and the time of acquisition. When a slot is free (or unlocked), its timestamp
// is set to kNotAcquired.
//
// Very old locks can be stolen by new clients, in which case the timestamp gets
// updated. This serves multiple purposes:
// 1) It means only one extra process will grab it for each timeout period,
// as all others will see the new timestamp.
// 2) It makes it possible for the last grabber to be the one to unlock the
// lock, as we check the grabber's acquisition timestamp versus the lock's.
//
// A further issue is what happens when a bucket is overflowed. In that case,
// however, we simply state that lock acquisition failed. This is because the
// purpose of this service is to limit the load on the system, and the table
// getting filled suggests it's under heavy load as it is, in which case
// blocking further operations is desirable.
//
const size_t kBuckets = 512;   // needs to be <= 65536 as we use 2 bytes of
                               // hash to pick a bucket.
const size_t kSlotsPerBucket = 32;

struct Slot {
  uint64 hash;
  int64 acquired_at_ms;  // kNotAcquired if free.
};

const int64 kNotAcquired = 0;

struct Bucket {
  Slot slots[kSlotsPerBucket];
  char mutex_base[1];
};

inline size_t Align64(size_t in) {
  return (in + 63) & ~63;
}

inline size_t BucketSize(size_t lock_size) {
  return Align64(offsetof(Bucket, mutex_base) + lock_size);
}

inline size_t SegmentSize(size_t lock_size) {
  return kBuckets * BucketSize(lock_size);
}

}  // namespace SharedMemLockData

namespace Data = SharedMemLockData;

class SharedMemLock : public SchedulerBasedAbstractLock {
 public:
  virtual ~SharedMemLock() {
    Unlock();
  }

  virtual bool TryLock() {
    return TryLockImpl(false, 0);
  }

  virtual bool TryLockStealOld(int64 timeout_ms) {
    return TryLockImpl(true, timeout_ms);
  }

  virtual void Unlock() {
    if (acquisition_time_ == Data::kNotAcquired) {
      return;
    }

    // Protect the bucket.
    scoped_ptr<AbstractMutex> lock(AttachMutex());
    ScopedMutex hold_lock(lock.get());

    // Search for this lock.
    // note: we permit empty slots in the middle, and start search at different
    // positions depending on the hash to increase chance of quick hit.
    // TODO(morlovich): Consider remembering which bucket we locked to avoid
    // the search. (Could potentially be made lock-free, too).
    size_t base = hash_ % Data::kSlotsPerBucket;
    for (size_t offset = 0; offset < Data::kSlotsPerBucket; ++offset) {
      size_t s = (base + offset) % Data::kSlotsPerBucket;
      Data::Slot& slot = bucket_->slots[s];
      if (slot.hash == hash_ && slot.acquired_at_ms == acquisition_time_) {
        slot.acquired_at_ms = Data::kNotAcquired;
        break;
      }
    }

    acquisition_time_ = Data::kNotAcquired;
  }

  virtual GoogleString name() const {
    return name_;
  }

  virtual bool Held() {
    return (acquisition_time_ != Data::kNotAcquired);
  }

 protected:
  virtual Scheduler* scheduler() const {
    return manager_->scheduler_;
  }

 private:
  friend class SharedMemLockManager;

  // ctor should only be called by CreateNamedLock below.
  SharedMemLock(SharedMemLockManager* manager, const StringPiece& name)
      : manager_(manager),
        name_(name.data(), name.size()),
        acquisition_time_(Data::kNotAcquired) {
    size_t bucket_num;
    GetHashAndBucket(name_, &hash_, &bucket_num);
    bucket_ = manager_->Bucket(bucket_num);
  }

  // Compute hash and bucket used to store the lock for a given lock name.
  void GetHashAndBucket(const StringPiece& name, uint64* hash_out,
                        size_t* bucket_out) {
    GoogleString raw_hash = manager_->hasher_->RawHash(name);

    // We use 10 bytes from the hash in the computation below:
    // 2 choose the bucket, while 8 form the uint64 we use as the key.
    CHECK_GE(raw_hash.size(), 10u);

    // We use separate hash bits to determine the hash and the bucket.
    unsigned char bucket_low = static_cast<unsigned char>(raw_hash[8]);
    unsigned char bucket_high = static_cast<unsigned char>(raw_hash[9]);
    *bucket_out = (bucket_high * 256 + bucket_low) % Data::kBuckets;

    uint64 hash = 0;
    for (int c = 0; c < 8; ++c) {
      hash = (hash << 8) | static_cast<unsigned char>(raw_hash[c]);
    }
    *hash_out = hash;
  }

  AbstractMutex* AttachMutex() const {
    return manager_->seg_->AttachToSharedMutex(
        manager_->MutexOffset(bucket_));
  }

  bool TryLockImpl(bool steal, int64 steal_timeout_ms) {
    // Protect the bucket.
    scoped_ptr<AbstractMutex> lock(AttachMutex());
    ScopedMutex hold_lock(lock.get());

    int64 now_ms = manager_->scheduler_->timer()->NowMs();
    if (now_ms == Data::kNotAcquired) {
      ++now_ms;
    }

    // Search for existing lock or empty slot. We need to check everything
    // for existing lock, of course.
    size_t empty_slot = Data::kSlotsPerBucket;
    size_t base = hash_ % Data::kSlotsPerBucket;
    for (size_t offset = 0; offset < Data::kSlotsPerBucket; ++offset) {
      size_t s = (base + offset) % Data::kSlotsPerBucket;
      Data::Slot& slot = bucket_->slots[s];
      if (slot.hash == hash_) {
        if (slot.acquired_at_ms == Data::kNotAcquired ||
            (steal && ((now_ms - slot.acquired_at_ms) >= steal_timeout_ms))) {
          // Stealing lock, or re-using a free slot we ourselves unlocked.
          //
          // We know we don't have an actual locked entry with our key elsewhere
          // because:
          // 1) After our last unlock of it no one else has ever locked it (or
          // our key would have been overwritten), so if we ever performed an
          // another lock operation we would have done it with this slot in
          // present state.
          //
          // 2) We always chose the first candidate.
          DoLockSlot(s, now_ms);
          return true;
        } else {
          // Not permitted to steal or not stale enough to steal.
          return false;
        }
      } else if (slot.acquired_at_ms == Data::kNotAcquired) {
        if (empty_slot == Data::kSlotsPerBucket) {
          empty_slot = s;
        }
      }
    }

    if (empty_slot != Data::kSlotsPerBucket) {
      DoLockSlot(empty_slot, now_ms);
      return true;
    }

    manager_->handler_->Message(kInfo,
                                "Overflowed bucket trying to grab lock.");
    return false;
  }

  // Writes out our ID and current timestamp into the slot, and marks the
  // fact of our acquisition.
  void DoLockSlot(size_t s, int64 now_ms) {
    Data::Slot& slot = bucket_->slots[s];
    slot.hash = hash_;
    slot.acquired_at_ms = now_ms;
    acquisition_time_ = now_ms;
  }

  SharedMemLockManager* manager_;
  GoogleString name_;

  uint64 hash_;

  // Time at which we acquired the lock...
  int64 acquisition_time_;

  // base pointer for the bucket we are in.
  Data::Bucket* bucket_;

  DISALLOW_COPY_AND_ASSIGN(SharedMemLock);
};

SharedMemLockManager::SharedMemLockManager(
    AbstractSharedMem* shm, const GoogleString& path, Scheduler* scheduler,
    Hasher* hasher, MessageHandler* handler)
    : shm_runtime_(shm),
      path_(path),
      scheduler_(scheduler),
      hasher_(hasher),
      handler_(handler),
      lock_size_(shm->SharedMutexSize()) {
  CHECK_GE(hasher_->RawHashSizeInBytes(), 9) << "Need >= 9 byte hashes";
}

SharedMemLockManager::~SharedMemLockManager() {
}

bool SharedMemLockManager::Initialize() {
  seg_.reset(shm_runtime_->CreateSegment(path_, Data::SegmentSize(lock_size_),
                                         handler_));
  if (seg_.get() == NULL) {
    handler_->MessageS(kError, "Unable to create memory segment for locks.");
    return false;
  }

  // Create the mutexes for each bucket
  for (size_t bucket = 0; bucket < Data::kBuckets; ++bucket) {
    if (!seg_->InitializeSharedMutex(MutexOffset(Bucket(bucket)), handler_)) {
      handler_->MessageS(kError,
                         StrCat("Unable to create lock service mutex #",
                                Integer64ToString(bucket)));
      return false;
    }
  }
  return true;
}

bool SharedMemLockManager::Attach() {
  size_t size = Data::SegmentSize(shm_runtime_->SharedMutexSize());
  seg_.reset(shm_runtime_->AttachToSegment(path_, size, handler_));
  if (seg_.get() == NULL) {
    handler_->MessageS(kWarning,
                       "Unable to attach to lock service SHM segment");
    return false;
  }

  return true;
}

void SharedMemLockManager::GlobalCleanup(
  AbstractSharedMem* shm, const GoogleString& path, MessageHandler* handler) {
  shm->DestroySegment(path, handler);
}

SchedulerBasedAbstractLock* SharedMemLockManager::CreateNamedLock(
    const StringPiece& name) {
  return new SharedMemLock(this, name);
}

Data::Bucket* SharedMemLockManager::Bucket(size_t bucket) {
  return reinterpret_cast<Data::Bucket*>(
      const_cast<char*>(seg_->Base()) + bucket * Data::BucketSize(lock_size_));
}

size_t SharedMemLockManager::MutexOffset(SharedMemLockData::Bucket* bucket) {
  return &bucket->mutex_base[0] - seg_->Base();
}

}  // namespace net_instaweb
