| // 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_ |