[feedback] Report store does garbage collection
The store uses a simple FIFO-policy garbage collection when there isn't
enough space for a new report.
Bug: 47137
TESTED=`fx test crash-report-tests`
Change-Id: I7b3cf5ae258be8275c61f09fd0501b52161c25dc
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/404986
Commit-Queue: Alex Pankhurst <pankhurst@google.com>
Reviewed-by: Francois Rousseau <frousseau@google.com>
Testability-Review: Francois Rousseau <frousseau@google.com>
Testability-Review: Alex Pankhurst <pankhurst@google.com>
diff --git a/src/developer/forensics/crash_reports/BUILD.gn b/src/developer/forensics/crash_reports/BUILD.gn
index ec21e6b..927961d 100644
--- a/src/developer/forensics/crash_reports/BUILD.gn
+++ b/src/developer/forensics/crash_reports/BUILD.gn
@@ -350,7 +350,10 @@
"store.h",
]
- public_deps = [ ":report" ]
+ public_deps = [
+ ":report",
+ "//src/developer/forensics/utils:storage_size",
+ ]
deps = [
"//sdk/lib/syslog/cpp",
diff --git a/src/developer/forensics/crash_reports/store.cc b/src/developer/forensics/crash_reports/store.cc
index 804a45a..bafd4c9 100644
--- a/src/developer/forensics/crash_reports/store.cc
+++ b/src/developer/forensics/crash_reports/store.cc
@@ -48,8 +48,7 @@
return contents;
}
-bool WriteAnnotationsAsJson(const std::string& path,
- const std::map<std::string, std::string>& annotations) {
+std::string FormatAnnotationsAsJson(const std::map<std::string, std::string>& annotations) {
rapidjson::Document json(rapidjson::kObjectType);
auto& allocator = json.GetAllocator();
@@ -64,8 +63,7 @@
json.Accept(writer);
- const std::string annotations_json = buffer.GetString();
- return files::WriteFile(path, annotations_json);
+ return buffer.GetString();
}
bool ReadAnnotations(const std::string& path, std::map<std::string, std::string>* annotations) {
@@ -105,7 +103,8 @@
} // namespace
-Store::Store(const std::string& root_dir) : root_dir_(root_dir) {}
+Store::Store(const std::string& root_dir, StorageSize max_size)
+ : root_dir_(root_dir), max_size_(max_size), current_size_(0u) {}
std::optional<Store::Uid> Store::Add(const Report report) {
for (const auto& key : kReservedAttachmentNames) {
@@ -125,20 +124,36 @@
return std::nullopt;
}
- auto MakeFilepath = [dir](const std::string& filename) { return files::JoinPath(dir, filename); };
+ const std::string annotations_json = FormatAnnotationsAsJson(report.Annotations());
- if (!WriteAnnotationsAsJson(MakeFilepath(kAnnotationsFilename), report.Annotations())) {
- FX_LOGS(ERROR) << "Failed to write annotations";
+ // Determine the size of the report.
+ StorageSize report_size = StorageSize::Bytes(annotations_json.size());
+ for (const auto& [_, v] : report.Attachments()) {
+ report_size += StorageSize::Bytes(v.size());
+ }
+ if (report.Minidump().has_value()) {
+ report_size += StorageSize::Bytes(report.Minidump().value().size());
+ }
+
+ // Ensure there's enough space in the store for the report.
+ if (!MakeFreeSpace(report_size)) {
+ FX_LOGS(ERROR) << "Failed to make space for report";
return std::nullopt;
}
+ auto MakeFilepath = [dir](const std::string& filename) { return files::JoinPath(dir, filename); };
+
+ // Write the report's content to the the filesystem.
+ if (!files::WriteFile(MakeFilepath(kAnnotationsFilename), annotations_json)) {
+ FX_LOGS(ERROR) << "Failed to write annotations";
+ return std::nullopt;
+ }
for (const auto& [filename, attachment] : report.Attachments()) {
if (!WriteAttachment(MakeFilepath(filename), attachment)) {
FX_LOGS(ERROR) << "Failed to write attachment " << filename;
return std::nullopt;
}
}
-
if (report.Minidump().has_value()) {
if (!WriteAttachment(MakeFilepath(kMinidumpFilename), report.Minidump().value())) {
FX_LOGS(ERROR) << "Failed to write minidump";
@@ -148,8 +163,12 @@
id_to_metadata_[id] = ReportMetadata{
.dir = dir,
+ .size = report_size,
.program_shortname = report.ProgramShortname(),
};
+ uids_.push_back(id);
+ current_size_ += report_size;
+
cleanup_on_error.cancel();
return id;
}
@@ -221,7 +240,9 @@
}
}
+ current_size_ -= id_to_metadata_[id].size;
id_to_metadata_.erase(id);
+ uids_.erase(std::remove(uids_.begin(), uids_.end(), id), uids_.end());
}
void Store::RemoveAll() {
@@ -229,7 +250,22 @@
FX_LOGS(ERROR) << "Failed to delete all reports";
}
files::CreateDirectory(root_dir_);
+
+ current_size_ = StorageSize::Bytes(0u);
id_to_metadata_.clear();
+ uids_.clear();
+}
+
+bool Store::MakeFreeSpace(const StorageSize required_space) {
+ if (required_space > max_size_) {
+ return false;
+ }
+
+ while ((current_size_ + required_space) > max_size_ && !uids_.empty()) {
+ Remove(uids_.front());
+ }
+
+ return true;
}
} // namespace crash_reports
diff --git a/src/developer/forensics/crash_reports/store.h b/src/developer/forensics/crash_reports/store.h
index 3e4d794..26c7a31 100644
--- a/src/developer/forensics/crash_reports/store.h
+++ b/src/developer/forensics/crash_reports/store.h
@@ -5,11 +5,13 @@
#ifndef SRC_DEVELOPER_FORENSICS_CRASH_REPORTS_STORE_H_
#define SRC_DEVELOPER_FORENSICS_CRASH_REPORTS_STORE_H_
+#include <deque>
#include <map>
#include <optional>
#include <string>
#include "src/developer/forensics/crash_reports/report.h"
+#include "src/developer/forensics/utils/storage_size.h"
namespace forensics {
namespace crash_reports {
@@ -23,7 +25,9 @@
// |root_dir| is the location in the filesystem where reports will be stored. For example,
// if |root_dir| is /tmp/store and a report for "foo" is filed, that report
// will be stored in /tmp/store/foo/<report Uid>.
- explicit Store(const std::string& root_dir);
+ // |max_size| is the maximum size the store can take, garbage collecting the reports of lowest
+ // Uids.
+ Store(const std::string& root_dir, StorageSize max_size);
// Adds a report to the store. If the operation fails, std::nullopt is returned, else a unique
// identifier referring to the report is returned.
@@ -40,17 +44,32 @@
bool Contains(const Uid& id) const;
private:
+ // Remove reports until |required_space| is free in the store.
+ //
+ // Return false if |required_space| cannot be freed.
+ bool MakeFreeSpace(StorageSize required_space);
+
struct ReportMetadata {
// The directory containing the report's files, e.g., /tmp/crashes/foo/<report Uid>
std::string dir;
+ // The total size taken by the report's files.
+ StorageSize size;
+
// "foo" in the above example, it shouldn't contain forward slashes.
std::string program_shortname;
};
std::string root_dir_;
+
+ StorageSize max_size_;
+ StorageSize current_size_;
+
std::map<Uid, ReportMetadata> id_to_metadata_;
+ // We store the Uids in FIFO order for easy garbage collection.
+ std::deque<Uid> uids_;
+
// TODO(47137): Uids may collide across component instances, however this will no longer happen
// once we rebuild the store's metadata when the component starts up.
Uid next_id_{0};
diff --git a/src/developer/forensics/crash_reports/tests/BUILD.gn b/src/developer/forensics/crash_reports/tests/BUILD.gn
index b8b7cb6..f9fe7f2 100644
--- a/src/developer/forensics/crash_reports/tests/BUILD.gn
+++ b/src/developer/forensics/crash_reports/tests/BUILD.gn
@@ -249,7 +249,9 @@
"//src/developer/forensics/crash_reports:store",
"//src/developer/forensics/testing:gtest_with_syslog_main",
"//src/developer/forensics/utils:sized_data",
+ "//src/developer/forensics/utils:storage_size",
"//src/lib/files",
+ "//third_party/googletest:gmock",
"//third_party/googletest:gtest",
"//third_party/rapidjson",
]
diff --git a/src/developer/forensics/crash_reports/tests/store_unittest.cc b/src/developer/forensics/crash_reports/tests/store_unittest.cc
index e65789d..269eced 100644
--- a/src/developer/forensics/crash_reports/tests/store_unittest.cc
+++ b/src/developer/forensics/crash_reports/tests/store_unittest.cc
@@ -10,6 +10,7 @@
#include <string>
#include <vector>
+#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "src/developer/forensics/utils/sized_data.h"
@@ -23,15 +24,21 @@
namespace crash_reports {
namespace {
+using testing::UnorderedElementsAreArray;
+
SizedData MakeSizedData(const std::string& content) {
return SizedData(content.begin(), content.end());
}
class StoreTest : public testing::Test {
public:
- StoreTest() : store_(tmp_dir_.path()) {}
+ StoreTest() : store_(std::make_unique<Store>(tmp_dir_.path(), StorageSize::Megabytes(1))) {}
protected:
+ void MakeNewStore(const StorageSize max_size) {
+ store_ = std::make_unique<Store>(tmp_dir_.path(), max_size);
+ }
+
std::optional<Store::Uid> Add(const std::string& program_shortname,
const std::map<std::string, std::string>& annotations,
const std::map<std::string, std::string>& attachments,
@@ -49,13 +56,13 @@
auto report = Report(program_shortname, annotations, std::move(attachments_data),
std::move(minidump_data));
- return store_.Add(std::move(report));
+ return store_->Add(std::move(report));
}
bool Get(const Store::Uid& id, std::string* program_shortname,
std::map<std::string, std::string>* annotations,
std::map<std::string, std::string>* attachments, std::optional<std::string>* minidump) {
- const auto report = store_.Get(id);
+ const auto report = store_->Get(id);
if (!report.has_value()) {
return false;
}
@@ -143,7 +150,7 @@
files::ScopedTempDir tmp_dir_;
protected:
- Store store_;
+ std::unique_ptr<Store> store_;
};
TEST_F(StoreTest, Succeed_Add) {
@@ -171,7 +178,7 @@
std::map<std::string, std::string> attachments;
std::optional<std::string> minidump;
- ASSERT_TRUE(store_.Contains(id.value()));
+ ASSERT_TRUE(store_->Contains(id.value()));
ASSERT_TRUE(Read(expected_program_shortname, id.value(), &annotations, &attachments, &minidump));
EXPECT_EQ(expected_annotations, annotations);
@@ -229,13 +236,35 @@
const auto id =
Add("program_shortname", /*annotations=*/{}, /*attachments=*/{}, /*minidump=*/std::nullopt);
EXPECT_TRUE(id.has_value());
- ASSERT_TRUE(store_.Contains(id.value()));
+ ASSERT_TRUE(store_->Contains(id.value()));
- store_.Remove(id.value());
- EXPECT_FALSE(store_.Contains(id.value()));
+ store_->Remove(id.value());
+ EXPECT_FALSE(store_->Contains(id.value()));
EXPECT_TRUE(GetProgramShortnames().empty());
}
+TEST_F(StoreTest, Succeed_GarbageCollection) {
+ const std::string minidump = "minidump";
+
+ // We set up the store so it can only hold one report at most, evicting the oldest ones first.
+ MakeNewStore(StorageSize::Bytes(minidump.size() + 4u /*the empty annotations.json*/));
+
+ const auto id1 = Add("program_name1", /*annotations=*/{}, /*attachments=*/{}, minidump);
+ const auto id2 = Add("program_name2", /*annotations=*/{}, /*attachments=*/{}, minidump);
+
+ EXPECT_FALSE(store_->Contains(id1.value()));
+ EXPECT_TRUE(store_->Contains(id2.value()));
+
+ EXPECT_THAT(GetProgramShortnames(), UnorderedElementsAreArray({"program_name2"}));
+
+ const auto id3 = Add("program_name3", /*annotations=*/{}, /*attachments=*/{}, minidump);
+
+ EXPECT_FALSE(store_->Contains(id2.value()));
+ EXPECT_TRUE(store_->Contains(id3.value()));
+
+ EXPECT_THAT(GetProgramShortnames(), UnorderedElementsAreArray({"program_name3"}));
+}
+
} // namespace
} // namespace crash_reports
} // namespace forensics
diff --git a/src/developer/forensics/utils/storage_size.h b/src/developer/forensics/utils/storage_size.h
index c84a72f..b96ad84 100644
--- a/src/developer/forensics/utils/storage_size.h
+++ b/src/developer/forensics/utils/storage_size.h
@@ -16,6 +16,8 @@
// prevent integer under/over flow and should be used with caution.
class StorageSize final {
public:
+ constexpr StorageSize() = default;
+
explicit constexpr StorageSize(uint64_t bytes) : bytes_(bytes) {}
static constexpr StorageSize Bytes(uint64_t bytes) { return StorageSize(bytes); }