| // Copyright 2020 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #ifndef COMPONENTS_SESSION_PROTO_DB_SESSION_PROTO_DB_H_ |
| #define COMPONENTS_SESSION_PROTO_DB_SESSION_PROTO_DB_H_ |
| |
| #include <queue> |
| #include <string> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/callback_helpers.h" |
| #include "base/containers/contains.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/task/thread_pool.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "components/commerce/core/proto/persisted_state_db_content.pb.h" |
| #include "components/keyed_service/core/keyed_service.h" |
| #include "components/leveldb_proto/public/proto_database.h" |
| #include "components/leveldb_proto/public/proto_database_provider.h" |
| #include "components/session_proto_db/session_proto_storage.h" |
| #include "content/public/browser/browser_context.h" |
| #include "third_party/abseil-cpp/absl/types/optional.h" |
| #include "third_party/leveldatabase/src/include/leveldb/options.h" |
| |
| namespace { |
| const char kOrphanedDataCountHistogramName[] = |
| "Tabs.PersistedTabData.Storage.LevelDB.OrphanedDataCount"; |
| } // namespace |
| |
| class SessionProtoDBTest; |
| |
| template <typename T> |
| class SessionProtoDBFactory; |
| |
| // General purpose per session (BrowserContext/BrowserState), per proto key -> |
| // proto database where the template is the proto which is being stored. A |
| // SessionProtoDB should be acquired using SessionProtoDBFactory. SessionProtoDB |
| // is a wrapper on top of leveldb_proto which: |
| // - Is specifically for databases which are per session |
| // (BrowserContext/BrowserState) |
| // and per proto (leveldb_proto is a proto database which may or may not be |
| // per BrowserContext/BrowserState). |
| // - Provides a simplified interface for the use cases that surround |
| // SessionProtoDB such as providing LoadContentWithPrefix instead of the |
| // more generic API in |
| // leveldb_proto which requires a filter to be passed in. |
| // - Is a KeyedService to support the per session (BrowserContext/BrowserState) |
| // nature of the database. |
| template <typename T> |
| class SessionProtoDB : public KeyedService, public SessionProtoStorage<T> { |
| public: |
| using KeyAndValue = std::pair<std::string, T>; |
| |
| // Callback which is used when content is acquired. |
| using LoadCallback = base::OnceCallback<void(bool, std::vector<KeyAndValue>)>; |
| |
| // Used for confirming an operation was completed successfully (e.g. |
| // insert, delete). This will be invoked on a different SequenceRunner |
| // to SessionProtoDB. |
| using OperationCallback = base::OnceCallback<void(bool)>; |
| |
| // Represents an entry in the database. |
| using ContentEntry = typename leveldb_proto::ProtoDatabase<T>::KeyEntryVector; |
| |
| SessionProtoDB(const SessionProtoDB&) = delete; |
| SessionProtoDB& operator=(const SessionProtoDB&) = delete; |
| ~SessionProtoDB() override; |
| |
| // SessionProtoStorage implementation: |
| void LoadOneEntry(const std::string& key, LoadCallback callback) override; |
| |
| void LoadAllEntries(LoadCallback callback) override; |
| |
| void LoadContentWithPrefix(const std::string& key_prefix, |
| LoadCallback callback) override; |
| |
| void PerformMaintenance(const std::vector<std::string>& keys_to_keep, |
| const std::string& key_substring_to_match, |
| OperationCallback callback) override; |
| |
| void InsertContent(const std::string& key, |
| const T& value, |
| OperationCallback callback) override; |
| |
| void DeleteOneEntry(const std::string& key, |
| OperationCallback callback) override; |
| |
| void DeleteContentWithPrefix(const std::string& key_prefix, |
| OperationCallback callback) override; |
| |
| void DeleteAllContent(OperationCallback callback) override; |
| |
| void Destroy() const override; |
| |
| private: |
| friend class ::SessionProtoDBTest; |
| template <typename U> |
| friend class ::SessionProtoDBFactory; |
| |
| // Initializes the database. |
| SessionProtoDB(content::BrowserContext* browser_context, |
| leveldb_proto::ProtoDatabaseProvider* proto_database_provider, |
| const base::FilePath& database_dir, |
| leveldb_proto::ProtoDbType proto_db_type); |
| |
| // Used for testing. |
| SessionProtoDB( |
| std::unique_ptr<leveldb_proto::ProtoDatabase<T>> storage_database, |
| scoped_refptr<base::SequencedTaskRunner> task_runner); |
| |
| // Passes back database status following database initialization. |
| void OnDatabaseInitialized(leveldb_proto::Enums::InitStatus status); |
| |
| // Callback when one entry is loaded. |
| void OnLoadOneEntry(LoadCallback callback, |
| bool success, |
| std::unique_ptr<T> entry); |
| |
| // Callback when content is loaded. |
| void OnLoadContent(LoadCallback callback, |
| bool success, |
| std::unique_ptr<std::vector<T>> content); |
| |
| // Callback when PerformMaintenance is complete. |
| void OnPerformMaintenance(OperationCallback callback, |
| bool success, |
| std::unique_ptr<std::vector<T>> entries_to_delete); |
| |
| // Callback when an operation (e.g. insert or delete) is called. |
| void OnOperationCommitted(OperationCallback callback, bool success); |
| |
| // Returns true if initialization status of database is not yet known. |
| bool InitStatusUnknown() const; |
| |
| // Returns true if the database failed to initialize. |
| bool FailedToInit() const; |
| |
| static bool DatabasePrefixFilter(const std::string& key_prefix, |
| const std::string& key) { |
| return base::StartsWith(key, key_prefix, base::CompareCase::SENSITIVE); |
| } |
| |
| // Browser context associated with SessionProtoDB (SessionProtoDB are per |
| // BrowserContext). |
| raw_ptr<content::BrowserContext> browser_context_; |
| |
| // Status of the database initialization. |
| absl::optional<leveldb_proto::Enums::InitStatus> database_status_; |
| |
| // The database for storing content storage information. |
| std::unique_ptr<leveldb_proto::ProtoDatabase<T>> storage_database_; |
| |
| // Store operations until the database is initialized at which point |
| // |deferred_operations_| is flushed and all operations are executed. |
| std::vector<base::OnceClosure> deferred_operations_; |
| |
| base::WeakPtrFactory<SessionProtoDB> weak_ptr_factory_{this}; |
| }; |
| |
| template <typename T> |
| SessionProtoDB<T>::~SessionProtoDB() = default; |
| |
| template <typename T> |
| void SessionProtoDB<T>::LoadOneEntry(const std::string& key, |
| LoadCallback callback) { |
| if (InitStatusUnknown()) { |
| deferred_operations_.push_back(base::BindOnce( |
| &SessionProtoDB::LoadOneEntry, weak_ptr_factory_.GetWeakPtr(), key, |
| std::move(callback))); |
| } else if (FailedToInit()) { |
| base::ThreadPool::PostTask( |
| FROM_HERE, |
| base::BindOnce(std::move(callback), false, std::vector<KeyAndValue>())); |
| } else { |
| storage_database_->GetEntry( |
| key, |
| base::BindOnce(&SessionProtoDB::OnLoadOneEntry, |
| weak_ptr_factory_.GetWeakPtr(), std::move(callback))); |
| } |
| } |
| |
| template <typename T> |
| void SessionProtoDB<T>::LoadAllEntries(LoadCallback callback) { |
| if (InitStatusUnknown()) { |
| deferred_operations_.push_back( |
| base::BindOnce(&SessionProtoDB::LoadAllEntries, |
| weak_ptr_factory_.GetWeakPtr(), std::move(callback))); |
| } else if (FailedToInit()) { |
| base::ThreadPool::PostTask( |
| FROM_HERE, |
| base::BindOnce(std::move(callback), false, std::vector<KeyAndValue>())); |
| } else { |
| storage_database_->LoadEntries( |
| base::BindOnce(&SessionProtoDB::OnLoadContent, |
| weak_ptr_factory_.GetWeakPtr(), std::move(callback))); |
| } |
| } |
| |
| template <typename T> |
| void SessionProtoDB<T>::LoadContentWithPrefix(const std::string& key_prefix, |
| LoadCallback callback) { |
| if (InitStatusUnknown()) { |
| deferred_operations_.push_back(base::BindOnce( |
| &SessionProtoDB::LoadContentWithPrefix, weak_ptr_factory_.GetWeakPtr(), |
| key_prefix, std::move(callback))); |
| } else if (FailedToInit()) { |
| base::ThreadPool::PostTask( |
| FROM_HERE, |
| base::BindOnce(std::move(callback), false, std::vector<KeyAndValue>())); |
| } else { |
| storage_database_->LoadEntriesWithFilter( |
| base::BindRepeating(&DatabasePrefixFilter, key_prefix), |
| {.fill_cache = false}, |
| /* target_prefix */ "", |
| base::BindOnce(&SessionProtoDB::OnLoadContent, |
| weak_ptr_factory_.GetWeakPtr(), std::move(callback))); |
| } |
| } |
| |
| template <typename T> |
| void SessionProtoDB<T>::PerformMaintenance( |
| const std::vector<std::string>& keys_to_keep, |
| const std::string& key_substring_to_match, |
| OperationCallback callback) { |
| if (InitStatusUnknown()) { |
| deferred_operations_.push_back(base::BindOnce( |
| &SessionProtoDB::PerformMaintenance, weak_ptr_factory_.GetWeakPtr(), |
| keys_to_keep, key_substring_to_match, std::move(callback))); |
| } else if (FailedToInit()) { |
| base::ThreadPool::PostTask(FROM_HERE, |
| base::BindOnce(std::move(callback), false)); |
| } else { |
| // The following could be achieved with UpdateEntriesWithRemoveFilter rather |
| // than LoadEntriesWithFilter followed by UpdateEntries, however, that would |
| // not allow metrics to be recorded regarding how much orphaned data was |
| // identified. |
| storage_database_->LoadEntriesWithFilter( |
| base::BindRepeating( |
| [](const std::vector<std::string>& keys_to_keep, |
| const std::string& key_substring_to_match, |
| const std::string& key) { |
| // Return all keys which where key_substring_to_match is a |
| // substring of said keys and hasn't been explicitly marked |
| // not to be removed in keys_to_keep. |
| return base::Contains(key, key_substring_to_match) && |
| !base::Contains(keys_to_keep, key); |
| }, |
| keys_to_keep, key_substring_to_match), |
| base::BindOnce(&SessionProtoDB::OnPerformMaintenance, |
| weak_ptr_factory_.GetWeakPtr(), std::move(callback))); |
| } |
| } |
| |
| // Inserts a value for a given key and passes the result (success/failure) to |
| // OperationCallback. |
| template <typename T> |
| void SessionProtoDB<T>::InsertContent(const std::string& key, |
| const T& value, |
| OperationCallback callback) { |
| if (InitStatusUnknown()) { |
| deferred_operations_.push_back(base::BindOnce( |
| &SessionProtoDB::InsertContent, weak_ptr_factory_.GetWeakPtr(), key, |
| std::move(value), std::move(callback))); |
| } else if (FailedToInit()) { |
| base::ThreadPool::PostTask(FROM_HERE, |
| base::BindOnce(std::move(callback), false)); |
| } else { |
| auto contents_to_save = std::make_unique<ContentEntry>(); |
| contents_to_save->emplace_back(key, value); |
| storage_database_->UpdateEntries( |
| std::move(contents_to_save), |
| std::make_unique<std::vector<std::string>>(), |
| base::BindOnce(&SessionProtoDB::OnOperationCommitted, |
| weak_ptr_factory_.GetWeakPtr(), std::move(callback))); |
| } |
| } |
| |
| template <typename T> |
| void SessionProtoDB<T>::DeleteOneEntry(const std::string& key, |
| OperationCallback callback) { |
| if (InitStatusUnknown()) { |
| deferred_operations_.push_back(base::BindOnce( |
| &SessionProtoDB::DeleteOneEntry, weak_ptr_factory_.GetWeakPtr(), key, |
| std::move(callback))); |
| } else if (FailedToInit()) { |
| base::ThreadPool::PostTask(FROM_HERE, |
| base::BindOnce(std::move(callback), false)); |
| } else { |
| auto keys = std::make_unique<std::vector<std::string>>(); |
| keys->push_back(key); |
| storage_database_->UpdateEntries( |
| std::make_unique<ContentEntry>(), std::move(keys), |
| base::BindOnce(&SessionProtoDB::OnOperationCommitted, |
| weak_ptr_factory_.GetWeakPtr(), std::move(callback))); |
| } |
| } |
| |
| // Deletes content in the database, matching all keys which have a prefix |
| // that matches the key. |
| template <typename T> |
| void SessionProtoDB<T>::DeleteContentWithPrefix(const std::string& key_prefix, |
| OperationCallback callback) { |
| if (InitStatusUnknown()) { |
| deferred_operations_.push_back(base::BindOnce( |
| &SessionProtoDB::DeleteContentWithPrefix, |
| weak_ptr_factory_.GetWeakPtr(), key_prefix, std::move(callback))); |
| } else if (FailedToInit()) { |
| base::ThreadPool::PostTask(FROM_HERE, |
| base::BindOnce(std::move(callback), false)); |
| } else { |
| storage_database_->UpdateEntriesWithRemoveFilter( |
| std::make_unique<ContentEntry>(), |
| std::move(base::BindRepeating(&DatabasePrefixFilter, key_prefix)), |
| base::BindOnce(&SessionProtoDB::OnOperationCommitted, |
| weak_ptr_factory_.GetWeakPtr(), std::move(callback))); |
| } |
| } |
| |
| // Delete all content in the database. |
| template <typename T> |
| void SessionProtoDB<T>::DeleteAllContent(OperationCallback callback) { |
| if (InitStatusUnknown()) { |
| deferred_operations_.push_back( |
| base::BindOnce(&SessionProtoDB::DeleteAllContent, |
| weak_ptr_factory_.GetWeakPtr(), std::move(callback))); |
| } else if (FailedToInit()) { |
| base::ThreadPool::PostTask(FROM_HERE, |
| base::BindOnce(std::move(callback), false)); |
| } else { |
| storage_database_->Destroy(std::move(callback)); |
| } |
| } |
| |
| template <typename T> |
| void SessionProtoDB<T>::Destroy() const { |
| // TODO(davidjm): Consider calling the factory's disassociate method here. |
| // This isn't strictly necessary since it will be called when |
| // the context is destroyed anyway. |
| } |
| |
| template <typename T> |
| SessionProtoDB<T>::SessionProtoDB( |
| content::BrowserContext* browser_context, |
| leveldb_proto::ProtoDatabaseProvider* proto_database_provider, |
| const base::FilePath& database_dir, |
| leveldb_proto::ProtoDbType proto_db_type) |
| : SessionProtoStorage<T>(), |
| browser_context_(browser_context), |
| database_status_(absl::nullopt), |
| storage_database_(proto_database_provider->GetDB<T>( |
| proto_db_type, |
| database_dir, |
| base::ThreadPool::CreateSequencedTaskRunner( |
| {base::MayBlock(), base::TaskPriority::USER_VISIBLE}))) { |
| static_assert(std::is_base_of<google::protobuf::MessageLite, T>::value, |
| "T must implement 'google::protobuf::MessageLite'"); |
| storage_database_->Init(base::BindOnce(&SessionProtoDB::OnDatabaseInitialized, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| // Used for tests. |
| template <typename T> |
| SessionProtoDB<T>::SessionProtoDB( |
| std::unique_ptr<leveldb_proto::ProtoDatabase<T>> storage_database, |
| scoped_refptr<base::SequencedTaskRunner> task_runner) |
| : SessionProtoStorage<T>(), |
| database_status_(absl::nullopt), |
| storage_database_(std::move(storage_database)) { |
| static_assert(std::is_base_of<google::protobuf::MessageLite, T>::value, |
| "T must implement 'google::protobuf::MessageLite'"); |
| storage_database_->Init(base::BindOnce(&SessionProtoDB::OnDatabaseInitialized, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| // Passes back database status following database initialization. |
| template <typename T> |
| void SessionProtoDB<T>::OnDatabaseInitialized( |
| leveldb_proto::Enums::InitStatus status) { |
| database_status_ = |
| absl::make_optional<leveldb_proto::Enums::InitStatus>(status); |
| for (auto& deferred_operation : deferred_operations_) { |
| std::move(deferred_operation).Run(); |
| } |
| deferred_operations_.clear(); |
| } |
| |
| // Callback when one entry is loaded. |
| template <typename T> |
| void SessionProtoDB<T>::OnLoadOneEntry(LoadCallback callback, |
| bool success, |
| std::unique_ptr<T> entry) { |
| std::vector<KeyAndValue> results; |
| if (success && entry) { |
| results.emplace_back(entry->key(), *entry); |
| } |
| std::move(callback).Run(success, std::move(results)); |
| } |
| |
| // Callback when content is loaded. |
| template <typename T> |
| void SessionProtoDB<T>::OnLoadContent(LoadCallback callback, |
| bool success, |
| std::unique_ptr<std::vector<T>> content) { |
| std::vector<KeyAndValue> results; |
| if (success) { |
| for (const auto& proto : *content) { |
| // TODO(crbug.com/1157881) relax requirement for proto to have a key field |
| // and return key value pairs OnLoadContent. |
| results.emplace_back(proto.key(), proto); |
| } |
| } |
| std::move(callback).Run(success, std::move(results)); |
| } |
| |
| template <typename T> |
| void SessionProtoDB<T>::OnPerformMaintenance( |
| OperationCallback callback, |
| bool success, |
| std::unique_ptr<std::vector<T>> entries_to_delete) { |
| auto keys_to_delete = std::make_unique<std::vector<std::string>>(); |
| if (success) { |
| for (const auto& proto : *entries_to_delete) { |
| keys_to_delete->emplace_back(proto.key()); |
| } |
| base::UmaHistogramCounts100(kOrphanedDataCountHistogramName, |
| keys_to_delete->size()); |
| } |
| auto save_no_entries = |
| std::make_unique<std::vector<std::pair<std::string, T>>>(); |
| storage_database_->UpdateEntries( |
| std::move(save_no_entries), std::move(keys_to_delete), |
| base::BindOnce(&SessionProtoDB::OnOperationCommitted, |
| weak_ptr_factory_.GetWeakPtr(), std::move(callback))); |
| } |
| |
| // Callback when an operation (e.g. insert or delete) is called. |
| template <typename T> |
| void SessionProtoDB<T>::OnOperationCommitted(OperationCallback callback, |
| bool success) { |
| std::move(callback).Run(success); |
| } |
| |
| // Returns true if initialization status of database is not yet known. |
| template <typename T> |
| bool SessionProtoDB<T>::InitStatusUnknown() const { |
| return database_status_ == absl::nullopt; |
| } |
| |
| // Returns true if the database failed to initialize. |
| template <typename T> |
| bool SessionProtoDB<T>::FailedToInit() const { |
| return database_status_.has_value() && |
| database_status_.value() != leveldb_proto::Enums::InitStatus::kOK; |
| } |
| |
| #endif // COMPONENTS_SESSION_PROTO_DB_SESSION_PROTO_DB_H_ |