| #include "persistent_storage_impl.h" |
| |
| #include <cerrno> |
| #include <cstddef> |
| #include <cstdint> |
| #include <cstring> |
| #include <filesystem> // NOLINT(build/c++17) |
| #include <iterator> |
| #include <optional> |
| #include <ostream> |
| #include <string> |
| #include <system_error> // NOLINT(build/c++11) |
| #include <utility> |
| #include <vector> |
| |
| #include "google/protobuf/timestamp.pb.h" |
| #include "action_context.h" |
| #include "flight_record.h" |
| #include "action.pb.h" |
| #include "safepower_agent.pb.h" |
| #include "safepower_agent_config.pb.h" |
| #include "state_persistence.pb.h" |
| #include "proto_reader.h" |
| #include "absl/algorithm/container.h" |
| #include "absl/container/flat_hash_map.h" |
| #include "absl/log/check.h" |
| #include "absl/log/log.h" |
| #include "absl/status/status.h" |
| #include "absl/status/statusor.h" |
| #include "absl/strings/numbers.h" |
| #include "absl/strings/str_cat.h" |
| #include "absl/strings/str_split.h" |
| #include "absl/strings/string_view.h" |
| #include "absl/synchronization/mutex.h" |
| #include "absl/time/clock.h" |
| #include "absl/time/time.h" |
| #include "absl/types/span.h" |
| #include "bmc/status_macros.h" |
| #include "google/protobuf/io/zero_copy_stream_impl.h" |
| |
| namespace persistent_storage { |
| |
| using safepower_agent::ActionContext; |
| using safepower_agent::FlightRecordWrapper; |
| using safepower_agent_persistence_proto::SavedAction; |
| using safepower_agent_persistence_proto::SavedActionRecord; |
| using safepower_agent_persistence_proto::SavedActions; |
| |
| // TODO: b/493660834 - |
| // Convert this to use posix file APIs, and not std::filesystem |
| inline absl::Status WriteSavedActions(const std::string& proto_path, |
| const SavedActions& in_proto, |
| bool append = true) { |
| int flags = O_WRONLY | O_CREAT; |
| if (append) { |
| flags |= O_APPEND; |
| } else { |
| flags |= O_TRUNC; |
| } |
| int fd = open(proto_path.c_str(), flags, 0644); |
| if (fd < 0) { |
| int ec = errno; |
| LOG(ERROR) << "Failed to open file " << proto_path << " " << strerror(ec); |
| return absl::UnavailableError( |
| absl::StrCat("Failed to open file: ", proto_path, ":", strerror(ec))); |
| } |
| |
| google::protobuf::io::FileOutputStream stream(fd); |
| stream.SetCloseOnDelete(true); |
| if (!in_proto.SerializeToZeroCopyStream(&stream)) { |
| int ec = stream.GetErrno(); |
| LOG(ERROR) << "Failed to serialize the proto file" << proto_path |
| << " error:" << strerror(ec); |
| return absl::InternalError( |
| absl::StrCat("Failed to serialize the proto file: ", proto_path, |
| " error:", strerror(ec))); |
| } |
| |
| if (!stream.Flush()) { |
| int ec = stream.GetErrno(); |
| LOG(ERROR) << "Failed to flush the proto file" << proto_path |
| << " error:" << strerror(ec); |
| return absl::InternalError( |
| absl::StrCat("Failed to flush the proto file: ", proto_path, |
| " error:", strerror(ec))); |
| } |
| |
| int ret = fsync(fd); |
| if (ret < 0) { |
| int ec = errno; |
| LOG(ERROR) << "Failed to sync the file " << proto_path << " " |
| << strerror(ec); |
| return absl::DataLossError(absl::StrCat( |
| "Failed to sync the file: ", proto_path, ":", strerror(ec))); |
| } |
| return absl::OkStatus(); |
| } |
| |
| PersistentStorageManagerImpl::PersistentStorageManagerImpl( |
| const safepower_agent_config::PersistentStorageConfig& config) { |
| if (!config.dir_path().empty()) { |
| dir_path_ = config.dir_path(); |
| } |
| if (config.max_file_size() > 0) { |
| max_file_size_ = config.max_file_size(); |
| } |
| if (config.proto_keep_number() > 0) { |
| proto_keep_number_ = config.proto_keep_number(); |
| } |
| } |
| |
| // Writes a delta of actions. If the active file exceeds the size limit, |
| // it rotates (compresses and writes to the other file). |
| // TODO: b/469169011 - Rename these functions |
| absl::Status PersistentStorageManagerImpl::WriteSavedActionsChange( |
| const SavedActions& actions) { |
| absl::MutexLock lock(mu_); |
| auto info = GetActiveFileInfo(); |
| if (!info.ok()) { |
| return info.status(); |
| } |
| |
| uint64_t write_id = 0; |
| int64_t next_sequence = 1; |
| |
| if (info->exists) { |
| if (info->id > 1) { |
| // Migrating from old unbounded files |
| write_id = 0; |
| next_sequence = 1; |
| |
| SavedActions merged_actions = info->proto; |
| for (const auto& record : actions.saved_action_records()) { |
| *merged_actions.add_saved_action_records() = record; |
| } |
| if (!actions.boot_id().empty()) { |
| merged_actions.set_boot_id(actions.boot_id()); |
| } |
| if (actions.boot_counter() != 0) { |
| merged_actions.set_boot_counter(actions.boot_counter()); |
| } |
| |
| auto compressed = CompressLogs(merged_actions, /*prune=*/true); |
| if (!compressed.ok()) { |
| return compressed.status(); |
| } |
| compressed->set_sequence_number(next_sequence); |
| |
| RETURN_IF_ERROR(WriteSavedActions(MakeFilePath(dir_path_, write_id), |
| *compressed, /*append=*/false)); |
| RETURN_IF_ERROR(DeleteOldFiles()); |
| return absl::OkStatus(); |
| } else { |
| write_id = info->id; |
| next_sequence = info->sequence_number; |
| } |
| } else { |
| write_id = 0; |
| next_sequence = 1; |
| } |
| |
| std::string file_path = MakeFilePath(dir_path_, write_id); |
| SavedActions actions_to_write = actions; |
| actions_to_write.set_sequence_number(next_sequence); |
| |
| RETURN_IF_ERROR( |
| WriteSavedActions(file_path, actions_to_write, /*append=*/true)); |
| |
| // if the file is too large, merge the files |
| std::error_code ec; |
| std::uintmax_t file_size = std::filesystem::file_size(file_path, ec); |
| if (ec) { |
| return absl::UnavailableError( |
| absl::StrCat("Failed to get file size", ec.message())); |
| } |
| |
| if (file_size > max_file_size_) { |
| LOG(INFO) << "file size too large merging" << std::endl; |
| auto full_actions = ReadProtoForId(write_id); |
| if (!full_actions.ok()) { |
| return full_actions.status(); |
| } |
| |
| uint64_t next_write_id = 1 - write_id; |
| uint64_t next_seq = next_sequence + 1; |
| |
| auto compressed = CompressLogs(*full_actions, /*prune=*/true); |
| if (!compressed.ok()) { |
| return compressed.status(); |
| } |
| compressed->set_sequence_number(next_seq); |
| |
| RETURN_IF_ERROR(WriteSavedActions(MakeFilePath(dir_path_, next_write_id), |
| *compressed, /*append=*/false)); |
| } |
| return absl::OkStatus(); |
| } |
| |
| absl::Status PersistentStorageManagerImpl::ConvertAppendToMap( |
| absl::flat_hash_map<FlightRecordWrapper, SavedAction>& map, |
| const SavedActions& saved_actions) { |
| for (const auto& i : saved_actions.saved_action_records()) { |
| FlightRecordWrapper flight_record( |
| i.actions().original_request().flight_record()); |
| auto [it, inserted] = map.try_emplace(flight_record, i.actions()); |
| if (!inserted) { |
| it->second.MergeFrom(i.actions()); |
| } |
| } |
| return absl::OkStatus(); |
| } |
| |
| absl::StatusOr<SavedActions> PersistentStorageManagerImpl::CompressLogs( |
| const SavedActions& saved_actions, bool prune) { |
| // convert to map, and back to append log |
| absl::flat_hash_map<FlightRecordWrapper, SavedAction> new_saved_actions_map; |
| absl::Status status = |
| this->ConvertAppendToMap(new_saved_actions_map, saved_actions); |
| if (!status.ok()) { |
| return status; |
| } |
| if (prune) { |
| absl::Status keep_status = keepMostRecentProtos(new_saved_actions_map); |
| if (!keep_status.ok()) { |
| return keep_status; |
| } |
| } |
| SavedActions new_log; |
| for (auto& i : new_saved_actions_map) { |
| SavedActionRecord* ptr_record = new_log.add_saved_action_records(); |
| *(ptr_record->mutable_actions()) = i.second; |
| } |
| if (!saved_actions.boot_id().empty()) { |
| new_log.set_boot_id(saved_actions.boot_id()); |
| } |
| if (saved_actions.boot_counter() != 0) { |
| new_log.set_boot_counter(saved_actions.boot_counter()); |
| } |
| new_log.set_sequence_number(saved_actions.sequence_number()); |
| return new_log; |
| } |
| |
| absl::StatusOr<SavedActions> PersistentStorageManagerImpl::ReadSavedActions() { |
| absl::MutexLock lock(mu_); |
| auto info = GetActiveFileInfo(); |
| if (!info.ok()) { |
| return info.status(); |
| } |
| if (!info->exists) { |
| LOG(INFO) << "No saved actions files found in " << dir_path_; |
| return SavedActions(); |
| } |
| auto compressed = CompressLogs(info->proto); |
| if (!compressed.ok()) { |
| LOG(ERROR) << "Unable to compress SavedActions: " << compressed.status(); |
| return compressed.status(); |
| } |
| return *compressed; |
| } |
| |
| namespace { |
| google::protobuf::Timestamp GetChangeTime(const SavedAction& action, |
| absl::Time now) { |
| if (!action.has_action_state_log() || |
| (action.action_state_log().history().empty())) { |
| // if there is no history, return the time of the last 24 hours |
| absl::Time one_day_ago = now - absl::Hours(24); |
| google::protobuf::Timestamp timestamp; |
| timestamp.set_seconds(absl::ToUnixSeconds(one_day_ago)); |
| return timestamp; |
| } |
| return action.action_state_log().history().rbegin()->changed_at(); |
| } |
| |
| bool AtEndState(const SavedAction& action) { |
| if ((!action.has_action_state_log()) || |
| !action.action_state_log().has_current_state()) { |
| return false; // if there is no state |
| // we should depend on the timing logic to clear it |
| } |
| const safepower_agent_proto::ActionState& state = |
| action.action_state_log().current_state(); |
| return ActionContext::IsFinalState(state); |
| } |
| } // namespace |
| |
| // when the persistence proto is cleaned up, we need to remove the protos that |
| // we need to keep the most recent N protos |
| absl::Status PersistentStorageManagerImpl::keepMostRecentProtos( |
| absl::flat_hash_map<FlightRecordWrapper, SavedAction>& map) { |
| if (map.size() < proto_keep_number_) { |
| LOG(WARNING) << " Assumptions about the size of the" |
| << " saved protos are wrong" << std::endl |
| << "The number of saved protos is " << map.size() |
| << "less than the number we want to keep " |
| << proto_keep_number_; |
| return absl::OkStatus(); |
| } |
| |
| // sort the protos by time |
| std::vector<SavedActionRecord> records; |
| for (const auto& i : map) { |
| SavedActionRecord record; |
| *(record.mutable_actions()) = i.second; |
| records.push_back(record); |
| } |
| |
| absl::Time now = absl::Now(); |
| // Sort the records so that all records in a final state come before those |
| // that do not. Within each grouping, sort from oldest to youngest. |
| auto compare_records = [now](const SavedActionRecord& a, |
| const SavedActionRecord& b) { |
| bool final_a = AtEndState(a.actions()); |
| bool final_b = AtEndState(b.actions()); |
| if (final_a != final_b) { |
| // If a is in a final state, it is "<" b, which is not. |
| // If a is _not_ in a final state, b is, and a ">" b. |
| return final_a; |
| } |
| // In ascending time, so oldest to youngest |
| auto time_a = GetChangeTime(a.actions(), now); |
| auto time_b = GetChangeTime(b.actions(), now); |
| return (time_a.seconds() < time_b.seconds()) || |
| (time_a.seconds() == time_b.seconds() && |
| time_a.nanos() < time_b.nanos()); |
| }; |
| absl::c_stable_sort(records, compare_records); |
| |
| // erase the oldest protos, keeping proto_keep_number_ protos |
| LOG(INFO) << "cleaning up the saved protos"; |
| LOG(INFO) << "current:" << records.size(); |
| LOG(INFO) << "keeping:" << proto_keep_number_; |
| const size_t num_to_delete = records.size() - proto_keep_number_; |
| LOG(INFO) << "deleting:" << num_to_delete; |
| if (proto_keep_number_ > records.size()) { |
| LOG(INFO) << "keeping all protos"; |
| return absl::OkStatus(); |
| } |
| |
| auto end_delete = std::next(records.begin(), num_to_delete); |
| for (auto i = records.begin(); i != end_delete; i++) { |
| FlightRecordWrapper action_to_delete( |
| i->actions().original_request().flight_record()); |
| // In ideal circumstances, only actions that are completed are deleted. |
| if (AtEndState(i->actions())) { |
| LOG(INFO) << "deleting end-state action record:" << action_to_delete; |
| } else { |
| LOG(ERROR) << "deleting an unfinished action record:" << action_to_delete; |
| } |
| map.erase(action_to_delete); |
| } |
| return absl::OkStatus(); |
| } |
| |
| // find the largest file id, and return it |
| |
| // the file id is a number, and the file name is in the format of |
| // "savedactions_1" |
| // "savedactions_2" |
| // where the largest id is the most recent file |
| std::optional<uint64_t> PersistentStorageManagerImpl::findLargestFileId( |
| absl::Span<const std::string> files) { |
| std::optional<uint64_t> largest_id; |
| for (const auto& entry : files) { |
| std::vector<absl::string_view> file_name_parts = absl::StrSplit(entry, '_'); |
| if (file_name_parts.size() != 2) { |
| LOG(WARNING) << "unable to parse the file name in the directory " << entry |
| << " size:" << file_name_parts.size(); |
| continue; |
| } |
| uint64_t file_id; |
| if (!absl::SimpleAtoi(file_name_parts[1], &file_id)) { |
| LOG(WARNING) << "unable to parse the file ID as unint64_t" << entry; |
| continue; |
| } |
| if (file_name_parts[0] != kfileNamePrefix) { |
| LOG(WARNING) << "unknown file name prefix" << entry; |
| continue; |
| } |
| if (!largest_id.has_value() || file_id > *largest_id) { |
| largest_id = file_id; |
| } |
| } |
| return largest_id; |
| } |
| |
| absl::StatusOr<std::vector<std::string> > |
| PersistentStorageManagerImpl::listFiles(absl::string_view file_path) { |
| // check if the file path exists and is a directory |
| std::error_code ec_is_directory; |
| bool is_directory = std::filesystem::is_directory(file_path, ec_is_directory); |
| if (ec_is_directory) { |
| const std::string error_message = |
| absl::StrCat("Failed to check if file is a directory ", file_path, |
| " error:", ec_is_directory.message()); |
| LOG(ERROR) << error_message; |
| return absl::InternalError(error_message); |
| } |
| if (!is_directory) { |
| const std::string error_message = |
| absl::StrCat("File path is not a directory:", file_path, |
| "is_directory: ", is_directory); |
| LOG(ERROR) << error_message; |
| return absl::InternalError(error_message); |
| } |
| // list the files in the directory |
| std::vector<std::string> files; |
| std::error_code ec; |
| std::filesystem::directory_iterator it(file_path, ec), end; |
| if (ec) { |
| const std::string error_message = |
| absl::StrCat("Failed to create directory iterator ", file_path, |
| " error:", ec.message()); |
| LOG(ERROR) << error_message; |
| return absl::InternalError(error_message); |
| } |
| while (it != end) { |
| files.push_back(it->path().filename().string()); |
| std::error_code ec_increment; |
| it.increment(ec_increment); |
| if (ec_increment) { |
| const std::string error_message = |
| absl::StrCat("Failed to iterate the directory ", file_path, |
| " error:", ec_increment.message()); |
| LOG(ERROR) << error_message; |
| return absl::InternalError(error_message); |
| } |
| } |
| return files; |
| } |
| |
| absl::StatusOr<PersistentStorageManagerImpl::ActiveFileInfo> |
| PersistentStorageManagerImpl::GetFileInfo(uint64_t id) { |
| ActiveFileInfo info; |
| info.id = id; |
| |
| std::error_code ec; |
| info.exists = std::filesystem::exists(MakeFilePath(dir_path_, id), ec); |
| if (ec && ec != std::errc::no_such_file_or_directory) { |
| return absl::InternalError(absl::StrCat("Failed to check if file ", id, |
| " exists: ", ec.message())); |
| } |
| |
| if (!info.exists) { |
| return info; |
| } |
| |
| auto proto = ReadProtoForId(id); |
| if (!proto.ok()) { |
| return absl::DataLossError(absl::StrCat( |
| "Savedactions file ", id, " is corrupt: ", proto.status().message())); |
| } |
| |
| info.sequence_number = proto->sequence_number(); |
| info.proto = *proto; |
| return info; |
| } |
| |
| absl::StatusOr<PersistentStorageManagerImpl::ActiveFileInfo> |
| PersistentStorageManagerImpl::GetActiveFileInfo() { |
| auto res_0 = GetFileInfo(0); |
| if (!res_0.status().ok() && !absl::IsDataLoss(res_0.status())) { |
| return res_0.status(); |
| } |
| auto res_1 = GetFileInfo(1); |
| if (!res_1.status().ok() && !absl::IsDataLoss(res_1.status())) { |
| return res_1.status(); |
| } |
| |
| bool corrupt_0 = absl::IsDataLoss(res_0.status()); |
| bool corrupt_1 = absl::IsDataLoss(res_1.status()); |
| |
| if (corrupt_0 && corrupt_1) { |
| return absl::DataLossError(absl::StrCat( |
| "Both savedactions files are corrupt: ", res_0.status().message(), "; ", |
| res_1.status().message())); |
| } |
| |
| bool ok_0 = res_0.ok() && res_0->exists; |
| bool ok_1 = res_1.ok() && res_1->exists; |
| |
| if (ok_0 && ok_1) { |
| // If both files have the same sequence number (e.g., they are both |
| // unpopulated old files from before the transition), we favor file 1 |
| // because it would have been the newer file in the old scheme. |
| if (res_0->sequence_number > res_1->sequence_number) { |
| return *res_0; |
| } else { |
| return *res_1; |
| } |
| } |
| |
| if (ok_0) { |
| return *res_0; |
| } |
| |
| if (ok_1) { |
| return *res_1; |
| } |
| |
| if (corrupt_0) { |
| return res_0.status(); |
| } |
| if (corrupt_1) { |
| return res_1.status(); |
| } |
| |
| DCHECK(res_0.ok() && res_1.ok()); |
| DCHECK(!res_0->exists && !res_1->exists); |
| |
| // Migration path: If neither savedactions_0 nor savedactions_1 exists, but |
| // older files (with IDs > 1) exist, we find the largest one and treat it as |
| // the base state. This allows seamless migration to the new ping-pong storage |
| // scheme. |
| auto files = listFiles(dir_path_); |
| if (!files.ok()) { |
| return files.status(); |
| } |
| std::optional<uint64_t> old_id = findLargestFileId(*files); |
| if (old_id.has_value()) { |
| auto res_old = GetFileInfo(old_id.value()); |
| if (res_old.ok() && res_old->exists) { |
| return *res_old; |
| } |
| if (old_id.value() > 0) { |
| auto res_fallback = GetFileInfo(old_id.value() - 1); |
| if (res_fallback.ok() && res_fallback->exists) { |
| return *res_fallback; |
| } |
| } |
| return absl::DataLossError("Old files exist but are corrupt"); |
| } |
| |
| ActiveFileInfo info; |
| info.exists = false; |
| return info; |
| } |
| |
| absl::StatusOr<SavedActions> PersistentStorageManagerImpl::ReadProtoForId( |
| uint64_t file_id) { |
| std::string file_path = MakeFilePath(dir_path_, file_id); |
| return proto_reader::ReadProto<SavedActions>(file_path); |
| } |
| |
| absl::Status PersistentStorageManagerImpl::DeleteOldFiles() { |
| auto files = listFiles(dir_path_); |
| if (!files.ok()) { |
| return files.status(); |
| } |
| for (const auto& entry : *files) { |
| if (entry == MakeFileName(0) || entry == MakeFileName(1)) { |
| continue; |
| } |
| const std::string del_path = absl::StrCat(dir_path_, "/", entry); |
| std::error_code ec; |
| bool removed = std::filesystem::remove(del_path, ec); |
| if (!removed || ec) { |
| LOG(WARNING) << "Failed to delete old file " << del_path |
| << " remove result:" << removed << " error:" << ec.message(); |
| } |
| } |
| return absl::OkStatus(); |
| } |
| |
| absl::Status PersistentStorageManagerImpl::InitializeSavedActions() { |
| absl::MutexLock lock(mu_); |
| std::error_code ec; |
| if (!std::filesystem::is_directory(dir_path_, ec)) { |
| return absl::FailedPreconditionError( |
| absl::StrCat("Directory does not exist: ", dir_path_)); |
| } |
| if (ec) { |
| const std::string error_message = |
| absl::StrCat("Failed to check if directory exists ", dir_path_, |
| " error:", ec.message()); |
| LOG(ERROR) << error_message; |
| return absl::InternalError(error_message); |
| } |
| std::filesystem::directory_iterator it(dir_path_, ec), end; |
| if (ec) { |
| const std::string error_message = |
| absl::StrCat("Failed to create directory iterator ", dir_path_, |
| " error:", ec.message()); |
| LOG(ERROR) << error_message; |
| return absl::InternalError(error_message); |
| } |
| while (it != end) { |
| const std::filesystem::path& file = it->path(); |
| std::error_code remove_ec; |
| bool removed = std::filesystem::remove(file, remove_ec); |
| if (!removed || remove_ec) { |
| const std::string error_message = |
| absl::StrCat("Failed to delete the file ", file.string(), |
| ": error:", remove_ec.message()); |
| LOG(ERROR) << error_message; |
| return absl::UnavailableError(error_message); |
| } |
| LOG(WARNING) << "Deleted " << file.string(); |
| std::error_code ec_increment; |
| it.increment(ec_increment); |
| if (ec_increment) { |
| LOG(ERROR) << "Failed to iterate the directory " << dir_path_ |
| << " error:" << ec_increment.message(); |
| return absl::InternalError(absl::StrCat( |
| "Failed to iterate the directory, file_path: ", dir_path_, |
| " error:", ec_increment.message())); |
| } |
| } |
| return absl::OkStatus(); |
| } |
| |
| std::string PersistentStorageManagerImpl::MakeFileName(const uint64_t file_id) { |
| return absl::StrCat(kfileNamePrefix, "_", std::to_string(file_id)); |
| } |
| |
| std::string PersistentStorageManagerImpl::MakeFilePath( |
| absl::string_view dir_path, const uint64_t file_id) { |
| return absl::StrCat(dir_path, "/", MakeFileName(file_id)); |
| } |
| } // namespace persistent_storage |