1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311
|
// 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_
|