blob: db3e30a5de733fc15635fd0bef16263cfd39f64c [file] [log] [blame]
// Copyright 2014 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.
#include "extensions/browser/value_store/leveldb_value_store.h"
#include <stdint.h>
#include <utility>
#include "base/files/file_util.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/macros.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/sys_string_conversions.h"
#include "base/thread_task_runner_handle.h"
#include "base/trace_event/memory_dump_manager.h"
#include "base/trace_event/process_memory_dump.h"
#include "content/public/browser/browser_thread.h"
#include "third_party/leveldatabase/env_chromium.h"
#include "third_party/leveldatabase/src/include/leveldb/iterator.h"
#include "third_party/leveldatabase/src/include/leveldb/write_batch.h"
using content::BrowserThread;
namespace {
const char kInvalidJson[] = "Invalid JSON";
const char kCannotSerialize[] = "Cannot serialize value to JSON";
const char kRestoredDuringOpen[] = "Database corruption repaired during open";
// UMA values used when recovering from a corrupted leveldb.
// Do not change/delete these values as you will break reporting for older
// copies of Chrome. Only add new values to the end.
enum LevelDBDatabaseCorruptionRecoveryValue {
LEVELDB_DB_RESTORE_DELETE_SUCCESS = 0,
LEVELDB_DB_RESTORE_DELETE_FAILURE,
LEVELDB_DB_RESTORE_REPAIR_SUCCESS,
LEVELDB_DB_RESTORE_MAX
};
// UMA values used when recovering from a corrupted leveldb.
// Do not change/delete these values as you will break reporting for older
// copies of Chrome. Only add new values to the end.
enum LevelDBValueCorruptionRecoveryValue {
LEVELDB_VALUE_RESTORE_DELETE_SUCCESS,
LEVELDB_VALUE_RESTORE_DELETE_FAILURE,
LEVELDB_VALUE_RESTORE_MAX
};
// Scoped leveldb snapshot which releases the snapshot on destruction.
class ScopedSnapshot {
public:
explicit ScopedSnapshot(leveldb::DB* db)
: db_(db), snapshot_(db->GetSnapshot()) {}
~ScopedSnapshot() {
db_->ReleaseSnapshot(snapshot_);
}
const leveldb::Snapshot* get() {
return snapshot_;
}
private:
leveldb::DB* db_;
const leveldb::Snapshot* snapshot_;
DISALLOW_COPY_AND_ASSIGN(ScopedSnapshot);
};
ValueStore::StatusCode LevelDbToValueStoreStatus(
const leveldb::Status& status) {
if (status.ok())
return ValueStore::OK;
if (status.IsCorruption())
return ValueStore::CORRUPTION;
return ValueStore::OTHER_ERROR;
}
} // namespace
LeveldbValueStore::LeveldbValueStore(const std::string& uma_client_name,
const base::FilePath& db_path)
: db_path_(db_path),
db_unrecoverable_(false),
open_histogram_(nullptr),
db_restore_histogram_(nullptr),
value_restore_histogram_(nullptr) {
DCHECK_CURRENTLY_ON(BrowserThread::FILE);
open_options_.max_open_files = 0; // Use minimum.
open_options_.create_if_missing = true;
open_options_.paranoid_checks = true;
open_options_.reuse_logs = leveldb_env::kDefaultLogReuseOptionValue;
read_options_.verify_checksums = true;
// Used in lieu of UMA_HISTOGRAM_ENUMERATION because the histogram name is
// not a constant.
open_histogram_ = base::LinearHistogram::FactoryGet(
"Extensions.Database.Open." + uma_client_name, 1,
leveldb_env::LEVELDB_STATUS_MAX, leveldb_env::LEVELDB_STATUS_MAX + 1,
base::Histogram::kUmaTargetedHistogramFlag);
db_restore_histogram_ = base::LinearHistogram::FactoryGet(
"Extensions.Database.Database.Restore." + uma_client_name, 1,
LEVELDB_DB_RESTORE_MAX, LEVELDB_DB_RESTORE_MAX + 1,
base::Histogram::kUmaTargetedHistogramFlag);
value_restore_histogram_ = base::LinearHistogram::FactoryGet(
"Extensions.Database.Value.Restore." + uma_client_name, 1,
LEVELDB_VALUE_RESTORE_MAX, LEVELDB_VALUE_RESTORE_MAX + 1,
base::Histogram::kUmaTargetedHistogramFlag);
base::trace_event::MemoryDumpManager::GetInstance()->RegisterDumpProvider(
this, "LeveldbValueStore", base::ThreadTaskRunnerHandle::Get());
}
LeveldbValueStore::~LeveldbValueStore() {
DCHECK_CURRENTLY_ON(BrowserThread::FILE);
base::trace_event::MemoryDumpManager::GetInstance()->UnregisterDumpProvider(
this);
// Delete the database from disk if it's empty (but only if we managed to
// open it!). This is safe on destruction, assuming that we have exclusive
// access to the database.
if (db_ && IsEmpty())
DeleteDbFile();
}
size_t LeveldbValueStore::GetBytesInUse(const std::string& key) {
// Let SettingsStorageQuotaEnforcer implement this.
NOTREACHED() << "Not implemented";
return 0;
}
size_t LeveldbValueStore::GetBytesInUse(
const std::vector<std::string>& keys) {
// Let SettingsStorageQuotaEnforcer implement this.
NOTREACHED() << "Not implemented";
return 0;
}
size_t LeveldbValueStore::GetBytesInUse() {
// Let SettingsStorageQuotaEnforcer implement this.
NOTREACHED() << "Not implemented";
return 0;
}
ValueStore::ReadResult LeveldbValueStore::Get(const std::string& key) {
DCHECK_CURRENTLY_ON(BrowserThread::FILE);
Status status = EnsureDbIsOpen();
if (!status.ok())
return MakeReadResult(status);
scoped_ptr<base::Value> setting;
status.Merge(ReadFromDb(key, &setting));
if (!status.ok())
return MakeReadResult(status);
base::DictionaryValue* settings = new base::DictionaryValue();
if (setting)
settings->SetWithoutPathExpansion(key, setting.release());
return MakeReadResult(make_scoped_ptr(settings), status);
}
ValueStore::ReadResult LeveldbValueStore::Get(
const std::vector<std::string>& keys) {
DCHECK_CURRENTLY_ON(BrowserThread::FILE);
Status status = EnsureDbIsOpen();
if (!status.ok())
return MakeReadResult(status);
scoped_ptr<base::DictionaryValue> settings(new base::DictionaryValue());
for (std::vector<std::string>::const_iterator it = keys.begin();
it != keys.end(); ++it) {
scoped_ptr<base::Value> setting;
status.Merge(ReadFromDb(*it, &setting));
if (!status.ok())
return MakeReadResult(status);
if (setting)
settings->SetWithoutPathExpansion(*it, setting.release());
}
return MakeReadResult(std::move(settings), status);
}
ValueStore::ReadResult LeveldbValueStore::Get() {
DCHECK_CURRENTLY_ON(BrowserThread::FILE);
Status status = EnsureDbIsOpen();
if (!status.ok())
return MakeReadResult(status);
base::JSONReader json_reader;
leveldb::ReadOptions options = leveldb::ReadOptions();
// All interaction with the db is done on the same thread, so snapshotting
// isn't strictly necessary. This is just defensive.
scoped_ptr<base::DictionaryValue> settings(new base::DictionaryValue());
ScopedSnapshot snapshot(db_.get());
options.snapshot = snapshot.get();
scoped_ptr<leveldb::Iterator> it(db_->NewIterator(options));
for (it->SeekToFirst(); it->Valid(); it->Next()) {
std::string key = it->key().ToString();
scoped_ptr<base::Value> value =
json_reader.ReadToValue(it->value().ToString());
if (!value) {
return MakeReadResult(
Status(CORRUPTION, Delete(key).ok() ? VALUE_RESTORE_DELETE_SUCCESS
: VALUE_RESTORE_DELETE_FAILURE,
kInvalidJson));
}
settings->SetWithoutPathExpansion(key, std::move(value));
}
if (it->status().IsNotFound()) {
NOTREACHED() << "IsNotFound() but iterating over all keys?!";
return MakeReadResult(std::move(settings), status);
}
if (!it->status().ok()) {
status.Merge(ToValueStoreError(it->status()));
return MakeReadResult(status);
}
return MakeReadResult(std::move(settings), status);
}
ValueStore::WriteResult LeveldbValueStore::Set(
WriteOptions options, const std::string& key, const base::Value& value) {
DCHECK_CURRENTLY_ON(BrowserThread::FILE);
Status status = EnsureDbIsOpen();
if (!status.ok())
return MakeWriteResult(status);
leveldb::WriteBatch batch;
scoped_ptr<ValueStoreChangeList> changes(new ValueStoreChangeList());
status.Merge(AddToBatch(options, key, value, &batch, changes.get()));
if (!status.ok())
return MakeWriteResult(status);
status.Merge(WriteToDb(&batch));
return status.ok() ? MakeWriteResult(std::move(changes), status)
: MakeWriteResult(status);
}
ValueStore::WriteResult LeveldbValueStore::Set(
WriteOptions options, const base::DictionaryValue& settings) {
DCHECK_CURRENTLY_ON(BrowserThread::FILE);
Status status = EnsureDbIsOpen();
if (!status.ok())
return MakeWriteResult(status);
leveldb::WriteBatch batch;
scoped_ptr<ValueStoreChangeList> changes(new ValueStoreChangeList());
for (base::DictionaryValue::Iterator it(settings);
!it.IsAtEnd(); it.Advance()) {
status.Merge(
AddToBatch(options, it.key(), it.value(), &batch, changes.get()));
if (!status.ok())
return MakeWriteResult(status);
}
status.Merge(WriteToDb(&batch));
return status.ok() ? MakeWriteResult(std::move(changes), status)
: MakeWriteResult(status);
}
ValueStore::WriteResult LeveldbValueStore::Remove(const std::string& key) {
DCHECK_CURRENTLY_ON(BrowserThread::FILE);
return Remove(std::vector<std::string>(1, key));
}
ValueStore::WriteResult LeveldbValueStore::Remove(
const std::vector<std::string>& keys) {
DCHECK_CURRENTLY_ON(BrowserThread::FILE);
Status status = EnsureDbIsOpen();
if (!status.ok())
return MakeWriteResult(status);
leveldb::WriteBatch batch;
scoped_ptr<ValueStoreChangeList> changes(new ValueStoreChangeList());
for (std::vector<std::string>::const_iterator it = keys.begin();
it != keys.end(); ++it) {
scoped_ptr<base::Value> old_value;
status.Merge(ReadFromDb(*it, &old_value));
if (!status.ok())
return MakeWriteResult(status);
if (old_value) {
changes->push_back(ValueStoreChange(*it, old_value.release(), NULL));
batch.Delete(*it);
}
}
leveldb::Status ldb_status = db_->Write(leveldb::WriteOptions(), &batch);
if (!ldb_status.ok() && !ldb_status.IsNotFound()) {
status.Merge(ToValueStoreError(ldb_status));
return MakeWriteResult(status);
}
return MakeWriteResult(std::move(changes), status);
}
ValueStore::WriteResult LeveldbValueStore::Clear() {
DCHECK_CURRENTLY_ON(BrowserThread::FILE);
scoped_ptr<ValueStoreChangeList> changes(new ValueStoreChangeList());
ReadResult read_result = Get();
if (!read_result->status().ok())
return MakeWriteResult(read_result->status());
base::DictionaryValue& whole_db = read_result->settings();
while (!whole_db.empty()) {
std::string next_key = base::DictionaryValue::Iterator(whole_db).key();
scoped_ptr<base::Value> next_value;
whole_db.RemoveWithoutPathExpansion(next_key, &next_value);
changes->push_back(ValueStoreChange(next_key, next_value.release(), NULL));
}
DeleteDbFile();
return MakeWriteResult(std::move(changes), read_result->status());
}
bool LeveldbValueStore::WriteToDbForTest(leveldb::WriteBatch* batch) {
return WriteToDb(batch).ok();
}
bool LeveldbValueStore::OnMemoryDump(
const base::trace_event::MemoryDumpArgs& args,
base::trace_event::ProcessMemoryDump* pmd) {
DCHECK_CURRENTLY_ON(BrowserThread::FILE);
// Return true so that the provider is not disabled.
if (!db_)
return true;
std::string value;
uint64_t size;
bool res = db_->GetProperty("leveldb.approximate-memory-usage", &value);
DCHECK(res);
res = base::StringToUint64(value, &size);
DCHECK(res);
auto dump = pmd->CreateAllocatorDump(
base::StringPrintf("leveldb/value_store/%s/%p",
open_histogram_->histogram_name().c_str(), this));
dump->AddScalar(base::trace_event::MemoryAllocatorDump::kNameSize,
base::trace_event::MemoryAllocatorDump::kUnitsBytes, size);
// Memory is allocated from system allocator (malloc).
const char* system_allocator_name =
base::trace_event::MemoryDumpManager::GetInstance()
->system_allocator_pool_name();
if (system_allocator_name)
pmd->AddSuballocation(dump->guid(), system_allocator_name);
return true;
}
ValueStore::BackingStoreRestoreStatus LeveldbValueStore::LogRestoreStatus(
BackingStoreRestoreStatus restore_status) {
switch (restore_status) {
case RESTORE_NONE:
NOTREACHED();
break;
case DB_RESTORE_DELETE_SUCCESS:
db_restore_histogram_->Add(LEVELDB_DB_RESTORE_DELETE_SUCCESS);
break;
case DB_RESTORE_DELETE_FAILURE:
db_restore_histogram_->Add(LEVELDB_DB_RESTORE_DELETE_FAILURE);
break;
case DB_RESTORE_REPAIR_SUCCESS:
db_restore_histogram_->Add(LEVELDB_DB_RESTORE_REPAIR_SUCCESS);
break;
case VALUE_RESTORE_DELETE_SUCCESS:
value_restore_histogram_->Add(LEVELDB_VALUE_RESTORE_DELETE_SUCCESS);
break;
case VALUE_RESTORE_DELETE_FAILURE:
value_restore_histogram_->Add(LEVELDB_VALUE_RESTORE_DELETE_FAILURE);
break;
}
return restore_status;
}
ValueStore::BackingStoreRestoreStatus LeveldbValueStore::FixCorruption(
const std::string* key) {
leveldb::Status s;
if (key && db_) {
s = Delete(*key);
// Deleting involves writing to the log, so it's possible to have a
// perfectly OK database but still have a delete fail.
if (s.ok())
return LogRestoreStatus(VALUE_RESTORE_DELETE_SUCCESS);
else if (s.IsIOError())
return LogRestoreStatus(VALUE_RESTORE_DELETE_FAILURE);
// Any other kind of failure triggers a db repair.
}
// Make sure database is closed.
db_.reset();
// First try the less lossy repair.
BackingStoreRestoreStatus restore_status = RESTORE_NONE;
leveldb::Options repair_options;
repair_options.create_if_missing = true;
repair_options.paranoid_checks = true;
// RepairDB can drop an unbounded number of leveldb tables (key/value sets).
s = leveldb::RepairDB(db_path_.AsUTF8Unsafe(), repair_options);
leveldb::DB* db = nullptr;
if (s.ok()) {
restore_status = DB_RESTORE_REPAIR_SUCCESS;
s = leveldb::DB::Open(open_options_, db_path_.AsUTF8Unsafe(), &db);
}
if (!s.ok()) {
if (DeleteDbFile()) {
restore_status = DB_RESTORE_DELETE_SUCCESS;
s = leveldb::DB::Open(open_options_, db_path_.AsUTF8Unsafe(), &db);
} else {
restore_status = DB_RESTORE_DELETE_FAILURE;
}
}
if (s.ok())
db_.reset(db);
else
db_unrecoverable_ = true;
if (s.ok() && key) {
s = Delete(*key);
if (s.ok()) {
restore_status = VALUE_RESTORE_DELETE_SUCCESS;
} else if (s.IsIOError()) {
restore_status = VALUE_RESTORE_DELETE_FAILURE;
} else {
db_.reset(db);
if (!DeleteDbFile())
db_unrecoverable_ = true;
restore_status = DB_RESTORE_DELETE_FAILURE;
}
}
// Only log for the final and most extreme form of database restoration.
LogRestoreStatus(restore_status);
return restore_status;
}
ValueStore::Status LeveldbValueStore::EnsureDbIsOpen() {
DCHECK_CURRENTLY_ON(BrowserThread::FILE);
if (db_)
return Status();
if (db_unrecoverable_) {
return ValueStore::Status(ValueStore::CORRUPTION,
ValueStore::DB_RESTORE_DELETE_FAILURE,
"Database corrupted");
}
leveldb::DB* db = NULL;
leveldb::Status ldb_status =
leveldb::DB::Open(open_options_, db_path_.AsUTF8Unsafe(), &db);
open_histogram_->Add(leveldb_env::GetLevelDBStatusUMAValue(ldb_status));
Status status = ToValueStoreError(ldb_status);
if (ldb_status.ok()) {
db_.reset(db);
} else if (ldb_status.IsCorruption()) {
status.restore_status = FixCorruption(nullptr);
if (status.restore_status != DB_RESTORE_DELETE_FAILURE) {
status.code = OK;
status.message = kRestoredDuringOpen;
}
}
return status;
}
ValueStore::Status LeveldbValueStore::ReadFromDb(
const std::string& key,
scoped_ptr<base::Value>* setting) {
DCHECK_CURRENTLY_ON(BrowserThread::FILE);
DCHECK(setting);
std::string value_as_json;
leveldb::Status s = db_->Get(read_options_, key, &value_as_json);
if (s.IsNotFound()) {
// Despite there being no value, it was still a success. Check this first
// because ok() is false on IsNotFound.
return Status();
}
if (!s.ok())
return ToValueStoreError(s);
scoped_ptr<base::Value> value = base::JSONReader().ReadToValue(value_as_json);
if (!value)
return Status(CORRUPTION, FixCorruption(&key), kInvalidJson);
*setting = std::move(value);
return Status();
}
ValueStore::Status LeveldbValueStore::AddToBatch(
ValueStore::WriteOptions options,
const std::string& key,
const base::Value& value,
leveldb::WriteBatch* batch,
ValueStoreChangeList* changes) {
bool write_new_value = true;
if (!(options & NO_GENERATE_CHANGES)) {
scoped_ptr<base::Value> old_value;
Status status = ReadFromDb(key, &old_value);
if (!status.ok())
return status;
if (!old_value || !old_value->Equals(&value)) {
changes->push_back(
ValueStoreChange(key, old_value.release(), value.DeepCopy()));
} else {
write_new_value = false;
}
}
if (write_new_value) {
std::string value_as_json;
if (!base::JSONWriter::Write(value, &value_as_json))
return Status(OTHER_ERROR, kCannotSerialize);
batch->Put(key, value_as_json);
}
return Status();
}
ValueStore::Status LeveldbValueStore::WriteToDb(leveldb::WriteBatch* batch) {
leveldb::Status status = db_->Write(leveldb::WriteOptions(), batch);
return ToValueStoreError(status);
}
bool LeveldbValueStore::IsEmpty() {
DCHECK_CURRENTLY_ON(BrowserThread::FILE);
scoped_ptr<leveldb::Iterator> it(db_->NewIterator(leveldb::ReadOptions()));
it->SeekToFirst();
bool is_empty = !it->Valid();
if (!it->status().ok()) {
LOG(ERROR) << "Checking DB emptiness failed: " << it->status().ToString();
return false;
}
return is_empty;
}
leveldb::Status LeveldbValueStore::Delete(const std::string& key) {
DCHECK_CURRENTLY_ON(BrowserThread::FILE);
DCHECK(db_.get());
leveldb::WriteBatch batch;
batch.Delete(key);
return db_->Write(leveldb::WriteOptions(), &batch);
}
bool LeveldbValueStore::DeleteDbFile() {
db_.reset(); // release any lock on the directory
if (!base::DeleteFile(db_path_, true /* recursive */)) {
LOG(WARNING) << "Failed to delete LeveldbValueStore database at " <<
db_path_.value();
return false;
}
return true;
}
ValueStore::Status LeveldbValueStore::ToValueStoreError(
const leveldb::Status& status) {
CHECK(!status.IsNotFound()); // not an error
std::string message = status.ToString();
// The message may contain |db_path_|, which may be considered sensitive
// data, and those strings are passed to the extension, so strip it out.
base::ReplaceSubstringsAfterOffset(
&message, 0u, db_path_.AsUTF8Unsafe(), "...");
return Status(LevelDbToValueStoreStatus(status), message);
}