blob: afa274c74d54be8d02f365e75c40d17ddab5cc49 [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/developer/forensics/crash_reports/product_quotas.h"
#include <lib/async/cpp/task.h>
#include <lib/zx/time.h>
#include <optional>
#include <random>
#include <string>
#include <utility>
#include "rapidjson/prettywriter.h"
#include "rapidjson/rapidjson.h"
#include "rapidjson/stringbuffer.h"
#include "src/developer/forensics/crash_reports/product.h"
#include "src/developer/forensics/utils/time.h"
#include "src/developer/forensics/utils/utc_clock_ready_watcher.h"
#include "src/lib/files/file.h"
#include "src/lib/files/path.h"
#include "src/lib/fxl/strings/string_printf.h"
#include "src/lib/timekeeper/clock.h"
#include "third_party/rapidjson/include/rapidjson/error/en.h"
namespace forensics {
namespace crash_reports {
namespace {
constexpr char kNextResetKey[] = "next_reset_time_utc_nanos";
constexpr char kQuotasKey[] = "quotas";
constexpr zx::duration kResetPeriod = zx::hour(24);
std::string Key(const Product& product) {
if (product.version.HasValue()) {
return fxl::StringPrintf("%s-%s", product.name.c_str(), product.version.Value().c_str());
} else {
return product.name;
}
}
// Since UTC epoch is 00:00:00 (midnight) on 1 January 1970, using truncating division on |time|
// will give us the number of days x between UTC epoch and the previous midnight. Multiply x by the
// number of nanoseconds in a day since timekeeper::time_utc is in nanoseconds.
timekeeper::time_utc StartOfDay(const timekeeper::time_utc time) {
return timekeeper::time_utc((time.get() / zx::hour(24).get()) * zx::hour(24).get());
}
} // namespace
ProductQuotas::ProductQuotas(async_dispatcher_t* dispatcher, timekeeper::Clock* clock,
const std::optional<uint64_t> quota, std::string quota_filepath,
UtcClockReadyWatcherBase* utc_clock_ready_watcher,
const zx::duration reset_time_offset)
: dispatcher_(dispatcher),
clock_(clock),
quota_(quota),
quota_filepath_(std::move(quota_filepath)),
utc_clock_ready_watcher_(utc_clock_ready_watcher),
reset_time_offset_(reset_time_offset) {
if (!quota_.has_value()) {
files::DeletePath(quota_filepath_, /*recursive=*/true);
return;
}
RestoreFromJson();
// Assume a 24 hour reset period until UTC clock starts.
reset_task_.PostDelayed(dispatcher_, kResetPeriod);
// This lambda could execute immediately if the UTC clock is already ready. Registering this
// callback with UtcClockReadyWatcherBase must be the last thing done in the constructor because
// OnClockStart requires initialization performed earlier in the constructor.
auto self = ptr_factory_.GetWeakPtr();
utc_clock_ready_watcher->OnClockReady([self] {
if (self) {
self->OnClockStart();
}
});
}
bool ProductQuotas::HasQuotaRemaining(const Product& product) {
// If no quota has been set, return true by default.
if (!quota_.has_value()) {
return true;
}
const auto key = Key(product);
if (remaining_quotas_.find(key) == remaining_quotas_.end()) {
remaining_quotas_[key] = quota_.value();
UpdateJson(key, quota_.value());
}
return remaining_quotas_[key] != 0u;
}
void ProductQuotas::DecrementRemainingQuota(const Product& product) {
// If no quota has been set, there's nothing to decrement.
if (!quota_.has_value()) {
return;
}
const auto key = Key(product);
FX_CHECK(remaining_quotas_.find(key) != remaining_quotas_.end());
FX_CHECK(remaining_quotas_[key] > 0);
--(remaining_quotas_[key]);
UpdateJson(key, remaining_quotas_[key]);
}
zx::duration ProductQuotas::RandomResetOffset() {
std::uniform_int_distribution<zx_duration_t> dist(zx::hour(-1).get(), zx::hour(1).get());
uint32_t seed = 0;
zx_cprng_draw(&seed, sizeof(seed));
std::default_random_engine rng(seed);
return zx::duration(dist(rng));
}
timekeeper::time_utc ProductQuotas::ActualResetTime() const {
return *next_reset_utc_time_ + reset_time_offset_;
}
void ProductQuotas::Reset() {
// If no quota has been set, resetting is a no-op.
if (!quota_.has_value()) {
return;
}
FX_LOGS(INFO) << "Resetting quota for all products";
remaining_quotas_.clear();
quota_json_.SetObject();
files::DeletePath(quota_filepath_, /*recursive=*/true);
if (utc_clock_ready_watcher_->IsUtcClockReady()) {
const timekeeper::time_utc current_time = CurrentUtcTimeRaw(clock_);
// Resets may not execute exactly at UTC midnight because the system's UTC clock drifts and is
// subject to correction. The start of the next UTC day needs to be calculated from the
// previously saved value in case Reset executes before midnight of the current day and the
// "next" midnight is in a short period of time. For example, if quotas were to be reset at
// 00:00 of February 2nd and Reset ran at 23:59 of February 1st, the next midnight would be
// 00:00 February 2nd.
next_reset_utc_time_ = StartOfDay(*next_reset_utc_time_ + zx::hour(24));
const zx::duration time_until_next_reset = ActualResetTime() - current_time;
UpdateJson(*next_reset_utc_time_);
reset_task_.PostDelayed(dispatcher_, time_until_next_reset);
} else {
reset_task_.PostDelayed(dispatcher_, kResetPeriod);
}
}
void ProductQuotas::OnClockStart() {
reset_task_.Cancel();
const timekeeper::time_utc current_time = CurrentUtcTimeRaw(clock_);
if (!next_reset_utc_time_.has_value()) {
// A next reset time wasn't persisted in the JSON file. Set it to next midnight.
next_reset_utc_time_ = StartOfDay(current_time + zx::hour(24));
UpdateJson(*next_reset_utc_time_);
}
const timekeeper::time_utc actual_reset_utc_time = ActualResetTime();
// Delay performing the reset.
if (current_time < actual_reset_utc_time) {
const zx::duration time_until_next_reset = actual_reset_utc_time - current_time;
reset_task_.PostDelayed(dispatcher_, time_until_next_reset);
return;
}
// A reset needs to occur now.
//
// Update |next_reset_utc_time_| so Reset() calculates the next midnight correctly.
//
// It should be midnight of the current day if we're past |next_reset_utc_time_| (a previous
// midnight), otherwise it should be midnight of the next day because we're after
// |actual_reset_time| and before |next_reset_utc_time_| (the next midnight).
next_reset_utc_time_ = current_time >= next_reset_utc_time_
? StartOfDay(current_time)
: StartOfDay(current_time + zx::hour(24));
Reset();
}
// Product "quotas" keys will be determined using the "Key" function in this file. JSON format will
// be:
// {
// "next_reset_time_utc_nanos": *utc-time in nanoseconds*,
// "quotas": {
// "foo-version": *remaining quota*,
// "bar": *remaining quota*,
// }
// }
void ProductQuotas::UpdateJson(const std::string& key, uint64_t remaining_quota) {
auto& allocator = quota_json_.GetAllocator();
if (!quota_json_.HasMember(kQuotasKey)) {
quota_json_.AddMember(kQuotasKey, rapidjson::Value(rapidjson::kObjectType), allocator);
}
const auto& json_quotas = quota_json_[kQuotasKey].GetObject();
if (!json_quotas.HasMember(key)) {
json_quotas.AddMember(rapidjson::Value(key, allocator), rapidjson::Value(0u), allocator);
}
json_quotas[key] = remaining_quota;
WriteJson();
}
void ProductQuotas::UpdateJson(timekeeper::time_utc next_reset_utc_time) {
auto& allocator = quota_json_.GetAllocator();
if (!quota_json_.HasMember(kNextResetKey)) {
quota_json_.AddMember(rapidjson::Value(kNextResetKey, allocator), rapidjson::Value(0),
allocator);
}
quota_json_[kNextResetKey] = next_reset_utc_time.get();
WriteJson();
}
void ProductQuotas::WriteJson() {
rapidjson::StringBuffer buffer;
rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(buffer);
quota_json_.Accept(writer);
if (!files::WriteFile(quota_filepath_, buffer.GetString(), buffer.GetLength())) {
FX_LOGS(ERROR) << "Failed to write remaining quota contents to " << quota_filepath_;
}
}
void ProductQuotas::RestoreFromJson() {
quota_json_.SetObject();
// If the file doesn't exit, return.
if (!files::IsFile(quota_filepath_)) {
return;
}
// Check-fail if the file can't be read.
std::string json;
FX_CHECK(files::ReadFileToString(quota_filepath_, &json));
if (const rapidjson::ParseResult ok = quota_json_.Parse(json); !ok) {
FX_LOGS(ERROR) << "Error parsing product quotas as JSON at offset " << ok.Offset() << " "
<< GetParseError_En(ok.Code());
files::DeletePath(quota_filepath_, /*recursive=*/true);
return;
}
if (quota_json_.HasMember(kNextResetKey) && quota_json_[kNextResetKey].IsInt64()) {
next_reset_utc_time_ = timekeeper::time_utc(quota_json_[kNextResetKey].GetInt64());
}
// Each product in the json is represented by an object containing string-int pairs
// that are the remaining quota for each product.
if (quota_json_.HasMember(kQuotasKey) && quota_json_[kQuotasKey].IsObject()) {
const auto& json_quotas = quota_json_[kQuotasKey].GetObject();
for (const auto& member : json_quotas) {
if (member.name.IsString() && member.value.IsInt64()) {
const std::string product_key = member.name.GetString();
const uint64_t remaining_quota = member.value.GetUint64();
remaining_quotas_[product_key] = remaining_quota;
}
}
}
}
} // namespace crash_reports
} // namespace forensics