cachelib/cachebench/consistency/ValueHistory.cpp (227 lines of code) (raw):
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* Licensed 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 "cachelib/cachebench/consistency/ValueHistory.h"
#include <folly/logging/xlog.h>
#include <glog/logging.h>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <ctime>
namespace facebook {
namespace cachelib {
namespace cachebench {
std::string EventInfo::formatTime() const {
// Out format is fixed size. It needs 16 chars with terminating null. For
// safety (if someone changes the strftime format) I made it double of it.
char buf[32];
size_t bufSize{sizeof buf};
auto tt = std::chrono::system_clock::to_time_t(tp);
struct tm tm_time;
::localtime_r(&tt, &tm_time);
::strftime(buf, bufSize, "%H:%M:%S", &tm_time);
auto len = std::strlen(buf);
auto micros = std::chrono::duration_cast<std::chrono::microseconds>(
tp.time_since_epoch());
std::snprintf(buf + len,
bufSize - len,
".%06d",
static_cast<int>(micros.count() % 1'000'000));
return buf;
}
bool ValueHistory::isWriteEnd(Event e) {
switch (e) {
case Event::kEndMiss:
case Event::kEndSet:
case Event::kEndDelete:
return true;
default:
return false;
}
}
const char* ValueHistory::toString(Event e) {
switch (e) {
case Event::kBeginGet:
case Event::kEndGet:
return "GET";
case Event::kBeginMiss:
case Event::kEndMiss:
return "MISS";
case Event::kBeginSet:
case Event::kEndSet:
return "SET";
case Event::kBeginDelete:
case Event::kEndDelete:
return "DELETE";
}
return nullptr;
}
bool ValueHistory::isEnd(Event e) {
switch (e) {
case Event::kEndGet:
case Event::kEndMiss:
case Event::kEndSet:
case Event::kEndDelete:
return true;
default:
return false;
}
}
bool ValueHistory::hasValue(Event e) {
switch (e) {
case Event::kBeginSet:
case Event::kEndGet:
case Event::kEndSet:
return true;
default:
return false;
}
}
uint32_t ValueHistory::beginGet(EventInfo ei) {
std::lock_guard<std::mutex> lock{mutex_};
cleanupIfFull();
LogRecord rec;
rec.event = Event::kBeginGet;
rec.setInfo(ei);
// If for a record at index i: log_[i].other == i, then record doesn't have
// link to other (yet).
rec.other = log_.last();
return log_.write(rec);
}
uint32_t ValueHistory::beginSet(EventInfo ei, uint64_t value) {
std::lock_guard<std::mutex> lock{mutex_};
cleanupIfFull();
LogRecord rec;
rec.event = Event::kBeginSet;
rec.setInfo(ei);
rec.other = log_.last();
rec.value = value;
openSetCount_++;
return log_.write(rec);
}
uint32_t ValueHistory::beginDelete(EventInfo ei) {
std::lock_guard<std::mutex> lock{mutex_};
cleanupIfFull();
LogRecord rec;
rec.event = Event::kBeginDelete;
rec.setInfo(ei);
rec.other = log_.last();
return log_.write(rec);
}
bool ValueHistory::endGet(EventInfo ei,
uint32_t beginIdx,
uint64_t value,
bool found,
EventStream* eventStream) {
std::lock_guard<std::mutex> lock{mutex_};
cleanupIfFull();
auto begin = log_.getAt(beginIdx);
XDCHECK_EQ(toInt(begin.event), toInt(Event::kBeginGet));
LogRecord rec;
rec.event = Event::kEndGet;
rec.setInfo(ei);
rec.other = beginIdx;
if (found) {
rec.value = value;
}
// Need to store end, because when we cleanup, we can't remove set with
// overlapping gets that don't have end yet.
const auto pos = log_.write(rec);
// Update begin's link
begin.other = pos;
log_.setAt(beginIdx, begin);
// TODO: If the log looks like ..., Gb[i], Ge[i] - remove these last two
// log entries (if @data is not null, in which case we transform to delete).
// Check consistency if we got data
if (found) {
return isConsistent(pos, eventStream);
} else {
// If we have a cache miss, it effectively means that we have a delete from
// hisotry point of view (eviction in cache) when we check for consistency.
// Convert to MISS.
rec.event = Event::kEndMiss;
log_.setAt(pos, rec);
begin.event = Event::kBeginMiss;
log_.setAt(beginIdx, begin);
return true;
}
}
void ValueHistory::endSet(EventInfo ei, uint32_t beginIdx) {
std::lock_guard<std::mutex> lock{mutex_};
cleanupIfFull();
auto begin = log_.getAt(beginIdx);
XDCHECK_EQ(toInt(begin.event), toInt(Event::kBeginSet));
LogRecord rec;
rec.event = Event::kEndSet;
rec.setInfo(ei);
rec.other = beginIdx;
rec.value = begin.value; // Store value in the end set too
const auto pos = log_.write(rec);
// Update begin's link
begin.other = pos;
log_.setAt(beginIdx, begin);
XDCHECK_GT(openSetCount_, 0u);
openSetCount_--;
}
void ValueHistory::endDelete(EventInfo ei, uint32_t beginIdx) {
std::lock_guard<std::mutex> lock{mutex_};
cleanupIfFull();
auto begin = log_.getAt(beginIdx);
XDCHECK_EQ(toInt(begin.event), toInt(Event::kBeginDelete));
LogRecord rec;
rec.event = Event::kEndDelete;
rec.setInfo(ei);
rec.other = beginIdx;
const auto pos = log_.write(rec);
// Update begin's link
begin.other = pos;
log_.setAt(beginIdx, begin);
}
bool ValueHistory::isConsistent(uint64_t endGetIdx, EventStream* es) const {
// Given the idea of what values are considered "possible", described in the
// header, detection is done with a simple algorithm
// ("b" for "begin", "e" for "end"):
// * Find first "S"et [Sb, Se] such that it is before "G"et [Gb, Ge] and
// doesn't intersect it (if any) - "last inorder set".
// * All sets, that overlap with [Sb, Ge] (note Sb and Ge) are candidates.
// * As we go, we should track sets that come in order. Latest of them
// overrides earlier. We track it with @setIdxMin. Only sets outside
// of the current get can override others.
//
// Delete from this perspective identical to set, but without value to check.
//
// We analyze history in reverse chronological order. First pass we find last
// inorder set. Second pass find all intersection with it and check if @value
// equals to one of them (with inorder overrides tracking).
const auto recEndGet = log_.getAt(endGetIdx);
XDCHECK_EQ(toInt(recEndGet.event), toInt(Event::kEndGet));
const auto beginGetIdx = recEndGet.other;
const auto beginSetIdx = findInOrderSetBegin(endGetIdx);
auto setIdxMin = log_.first();
uint32_t counter = 0;
for (uint64_t i = endGetIdx;
i > log_.first() && (i > beginSetIdx || counter < openSetCount_);) {
i--;
const auto rec = log_.getAt(i);
if (rec.event == Event::kBeginSet && rec.other == i) {
counter++;
}
// Check if this is a completed operation (doesn't matter if one of SETs,
// or GET) and skip if it is older than in order SET.
if (i != rec.other && i < setIdxMin) {
continue;
}
// Check only sets, everything else can be ignored
if (rec.event == Event::kBeginSet || rec.event == Event::kEndSet) {
// Set events contain a value. Check it.
if (rec.value == recEndGet.value) {
return true;
}
} else if (i == log_.first() &&
(rec.event == Event::kBeginGet || rec.event == Event::kEndGet)) {
return true;
}
if (isWriteEnd(rec.event) && i < beginGetIdx) {
// If SET begins outside of the GET, do not consider SETs before
// @setIdxMin - they are older in order SETs.
setIdxMin = std::max<uint32_t>(setIdxMin, rec.other);
}
}
if (es != nullptr) {
// Find a log record @kHistoryContext from @beginSetIdx, not beyond the
// first. Do not subtract from @beginSetIdx - it is unsigned.
const auto historyBeginIdx =
std::max<uint64_t>(beginSetIdx, log_.first() + kHistoryContext) -
kHistoryContext;
const auto numEvents = endGetIdx - historyBeginIdx;
for (uint32_t i = 0; i <= numEvents; i++) {
auto rec = log_.getAt(endGetIdx - i);
es->event(i,
endGetIdx - rec.other,
toString(rec.event),
isEnd(rec.event),
hasValue(rec.event),
rec.value,
rec.getInfo());
}
}
return false;
}
uint64_t ValueHistory::findInOrderSetBegin(uint64_t endGetIdx) const {
// At least get begin and end are in the log
XDCHECK_GE(log_.size(), 2u);
const auto beginGetIdx = log_.getAt(endGetIdx).other;
for (uint64_t ri = beginGetIdx; ri-- > log_.first();) {
const auto rec = log_.getAt(ri);
if (isWriteEnd(rec.event)) {
// Found the end of last inorder set. Return the beginning.
return rec.other;
}
}
// Last inorder set not found. It is normal - all sets in flight. But pay
// attention, it may be a bug too (in history trimming). All log has possible
// values.
return log_.first();
}
void ValueHistory::cleanupIfFull() {
// TODO: Check if cleanup can lead to false detections
if (log_.size() == kCapacity) {
(void)log_.read();
}
}
} // namespace cachebench
} // namespace cachelib
} // namespace facebook