// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef ASH_UTILITY_PERSISTENT_PROTO_H_
#define ASH_UTILITY_PERSISTENT_PROTO_H_
#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include "ash/ash_export.h"
#include "base/callback_list.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/important_file_writer.h"
#include "base/functional/bind.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/sequence_checker.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/threading/scoped_blocking_call.h"
#include "base/time/time.h"
namespace ash {
namespace internal {
// Data types ------------------------------------------------------------------
// The result of reading a backing file from disk. These values persist to logs.
// Entries should not be renumbered and numeric values should never be reused.
enum class ReadStatus {
kOk = 0,
kMissing = 1,
kReadError = 2,
kParseError = 3,
// kNoop is currently unused, but was previously used when no read was
// required.
kNoop = 4,
kMaxValue = kNoop,
};
// The result of writing a backing file to disk. These values persist to logs.
// Entries should not be renumbered and numeric values should never be reused.
enum class WriteStatus {
kOk = 0,
kWriteError = 1,
kSerializationError = 2,
kMaxValue = kSerializationError,
};
// Helpers ---------------------------------------------------------------------
template <class T>
std::pair<ReadStatus, std::unique_ptr<T>> Read(const base::FilePath& filepath) {
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::MAY_BLOCK);
if (!base::PathExists(filepath))
return {ReadStatus::kMissing, nullptr};
std::string proto_str;
if (!base::ReadFileToString(filepath, &proto_str))
return {ReadStatus::kReadError, nullptr};
auto proto = std::make_unique<T>();
if (!proto->ParseFromString(proto_str))
return {ReadStatus::kParseError, nullptr};
return {ReadStatus::kOk, std::move(proto)};
}
// Writes `proto_str` to the file specified by `filepath`.
WriteStatus ASH_EXPORT Write(const base::FilePath& filepath,
std::string_view proto_str);
} // namespace internal
// PersistentProto wraps a proto class and persists it to disk. Usage summary:
// 1. Init is asynchronous. Using the object before initialization is complete
// will result in a crash.
// 2. pproto->Method() will call Method on the underlying proto.
// 3. Call `QueueWrite()` to write to disk.
//
// Reading. The backing file is loaded asynchronously during initialization.
// Until initialization completed, `has_value()` will be false and `get()` will
// return `nullptr`. Register an init callback to be notified of completion.
//
// Writing. Writes must be triggered manually. Two methods are available:
// 1. `QueueWrite()` delays writing to disk for `write_delay` time, in order to
// batch successive writes.
// 2. `StartWrite()` writes to disk as soon as the task scheduler allows.
// Registered write callbacks are executed whenever a write operation finishes.
template <class T>
class ASH_EXPORT PersistentProto {
public:
using InitCallback = base::OnceClosure;
using WriteCallback = base::RepeatingCallback<void(/*success=*/bool)>;
PersistentProto(
const base::FilePath& path,
const base::TimeDelta write_delay,
base::TaskPriority task_priority = base::TaskPriority::BEST_EFFORT)
: path_(path),
write_delay_(write_delay),
on_init_callbacks_(
std::make_unique<base::OnceCallbackList<InitCallback::RunType>>()),
on_write_callbacks_(
std::make_unique<
base::RepeatingCallbackList<WriteCallback::RunType>>()),
task_runner_(base::ThreadPool::CreateSequencedTaskRunner(
{task_priority, base::MayBlock(),
base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})) {}
~PersistentProto() = default;
PersistentProto(const PersistentProto&) = delete;
PersistentProto& operator=(const PersistentProto&) = delete;
PersistentProto(PersistentProto&& other) {
path_ = other.path_;
write_delay_ = other.write_delay_;
initialized_ = other.initialized_;
write_is_queued_ = false;
purge_after_reading_ = other.purge_after_reading_;
on_init_callbacks_ = std::move(other.on_init_callbacks_);
on_write_callbacks_ = std::move(other.on_write_callbacks_);
task_runner_ = std::move(other.task_runner_);
proto_ = std::move(other.proto_);
}
void Init() {
task_runner_->PostTaskAndReplyWithResult(
FROM_HERE, base::BindOnce(&internal::Read<T>, path_),
base::BindOnce(&PersistentProto<T>::OnReadComplete,
weak_factory_.GetWeakPtr()));
}
[[nodiscard]] base::CallbackListSubscription RegisterOnInit(
InitCallback on_init) {
return on_init_callbacks_->Add(std::move(on_init));
}
// NOTE: The caller must ensure `on_init` to be valid when init completes.
void RegisterOnInitUnsafe(InitCallback on_init) {
on_init_callbacks_->AddUnsafe(std::move(on_init));
}
// NOTE: The caller must ensure `on_write` to be valid during the life cycle
// of `PersistentProto`.
void RegisterOnWriteUnsafe(WriteCallback on_write) {
on_write_callbacks_->AddUnsafe(std::move(on_write));
}
T* get() { return proto_.get(); }
T* operator->() {
CHECK(proto_);
return proto_.get();
}
const T* operator->() const {
CHECK(proto_);
return proto_.get();
}
T operator*() {
CHECK(proto_);
return *proto_;
}
bool initialized() const { return initialized_; }
constexpr bool has_value() const { return proto_.get() != nullptr; }
constexpr explicit operator bool() const { return has_value(); }
// Write the backing proto to disk after `save_delay_ms_` has elapsed.
void QueueWrite() {
DCHECK(proto_);
if (!proto_)
return;
// If a save is already queued, do nothing.
if (write_is_queued_)
return;
write_is_queued_ = true;
base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&PersistentProto<T>::OnQueueWrite,
weak_factory_.GetWeakPtr()),
write_delay_);
}
// Write the backing proto to disk 'now'.
void StartWrite() {
DCHECK(proto_);
if (!proto_)
return;
// Serialize the proto outside of the posted task, because otherwise we need
// to pass a proto pointer into the task. This causes a rare race condition
// during destruction where the proto can be destroyed before serialization,
// causing a crash.
std::string proto_str;
if (!proto_->SerializeToString(&proto_str))
OnWriteComplete(internal::WriteStatus::kSerializationError);
// The SequentialTaskRunner ensures the writes won't trip over each other,
// so we can schedule without checking whether another write is currently
// active.
task_runner_->PostTaskAndReplyWithResult(
FROM_HERE, base::BindOnce(&internal::Write, path_, proto_str),
base::BindOnce(&PersistentProto<T>::OnWriteComplete,
weak_factory_.GetWeakPtr()));
}
// Safely clear this proto from memory and disk. This is preferred to clearing
// the proto, because it ensures the proto is purged even if called before the
// backing file is read from disk. In this case, the file is overwritten after
// it has been read. In either case, the file is written as soon as possible,
// skipping the `save_delay_ms_` wait time.
void Purge() {
if (proto_) {
proto_.reset();
proto_ = std::make_unique<T>();
StartWrite();
} else {
purge_after_reading_ = true;
}
}
private:
void OnReadComplete(
std::pair<internal::ReadStatus, std::unique_ptr<T>> result) {
const internal::ReadStatus status = result.first;
base::UmaHistogramEnumeration("Apps.AppList.PersistentProto.ReadStatus",
status);
if (status == internal::ReadStatus::kOk) {
proto_ = std::move(result.second);
} else {
proto_ = std::make_unique<T>();
QueueWrite();
}
if (purge_after_reading_) {
proto_.reset();
proto_ = std::make_unique<T>();
StartWrite();
purge_after_reading_ = false;
}
initialized_ = true;
on_init_callbacks_->Notify();
}
void OnWriteComplete(const internal::WriteStatus status) {
base::UmaHistogramEnumeration("Apps.AppList.PersistentProto.WriteStatus",
status);
on_write_callbacks_->Notify(/*success=*/status ==
internal::WriteStatus::kOk);
}
void OnQueueWrite() {
// Reset the queued flag before posting the task. Last-moment updates to
// `proto_` will post another task to write the proto, avoiding race
// conditions.
write_is_queued_ = false;
StartWrite();
}
// Path on disk to read from and write to.
base::FilePath path_;
// How long to delay writing to disk for on a call to QueueWrite.
base::TimeDelta write_delay_;
// Whether the proto has finished reading from disk. `proto_` will be empty
// before `initialized_` is true.
bool initialized_ = false;
// Whether or not a write is currently scheduled.
bool write_is_queued_ = false;
// Whether we should immediately clear the proto after reading it.
bool purge_after_reading_ = false;
// Run when `proto_` finishes initialization.
std::unique_ptr<base::OnceCallbackList<InitCallback::RunType>>
on_init_callbacks_;
// Run when the cache finishes writing to disk.
std::unique_ptr<base::RepeatingCallbackList<WriteCallback::RunType>>
on_write_callbacks_;
// The proto itself.
std::unique_ptr<T> proto_;
scoped_refptr<base::SequencedTaskRunner> task_runner_;
base::WeakPtrFactory<PersistentProto> weak_factory_{this};
};
} // namespace ash
#endif // ASH_UTILITY_PERSISTENT_PROTO_H_