blob: 36941993d052dd461ecc5914fd2061ac1a0f950c [file] [log] [blame]
// Copyright 2018 The Fuchsia 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 "util/consistent_proto_store.h"
#include <google/protobuf/io/zero_copy_stream_impl.h>
#include <fstream>
#include <iostream>
#include <utility>
#include "third_party/statusor/status_macros.h"
namespace cobalt {
namespace util {
using google::protobuf::MessageLite;
const char kTmpSuffix[] = ".tmp";
const char kOverrideSuffix[] = ".override";
ConsistentProtoStore::ConsistentProtoStore(std::string filename,
std::unique_ptr<FileSystem> fs)
: primary_file_(std::move(filename)),
tmp_file_(primary_file_ + kTmpSuffix),
override_file_(primary_file_ + kOverrideSuffix),
fs_(std::move(fs)) {}
// Write uses a series of operations to write new data. The goal of each of
// these operations is that if the operation is interrupted or fails, the
// store will still be valid.
//
// 1. Ensure a valid state of the files
// - Check if a file exists at override_file_. A file here indicates an
// interrupted Write operation, so it is cleaned up by attempting to
// delete the file at primary_file_, and rename override_file_ to
// primary_file_. (This is equivalent performing steps 4/5 which were not
// completed after the previous write)
//
// 2. Write to a temporary file (tmp_file_)
// - Writing to a file is not atomic, but this temp file is guaranteed
// never to be read, so if the write is not finished, it is not an issue.
//
// 3. Rename the temp file to override_file_
// - Renames are as close to an atomic operation you can get in a
// filesystem. If the rename fails, nothing changes, but if it succeeds,
// then the store will immediately start reading from this file.
//
// 4. Delete original file (at primary_file_)
// - Since the Read operation has switched to using the file at
// override_file_ for read operations, it is safe to delete the
// original file.
//
// 5. Rename the file at override_file_ to primary_file_
// - Again, this is a rename so essentially atomic. If this succeeds, then
// Read operations should immediately switch to using this file. If it
// fails, the file should still exist at override_file_ so the store
// should continue reading it successfully.
//
Status ConsistentProtoStore::Write(const MessageLite &proto) {
// Check if override_file_ exists
{
if (fs_->FileExists(override_file_)) {
DeletePrimary(); // Ignore errors since primary_file_ might not exist
auto status = MoveOverrideToPrimary();
if (!status.ok()) {
// If renaming override_file_ to primary_file_ fails, we should bail
// since a write operation is not safe.
return Status(status.error_code(),
"Error during recovery: " + status.error_message(),
status.error_details());
}
}
}
RETURN_IF_ERROR(WriteToTmp(proto));
RETURN_IF_ERROR(MoveTmpToOverride());
RETURN_IF_ERROR(DeletePrimary());
RETURN_IF_ERROR(MoveOverrideToPrimary());
return Status::OK;
}
Status ConsistentProtoStore::Read(MessageLite *proto) {
{
std::ifstream new_ifstream(override_file_);
if (new_ifstream) {
google::protobuf::io::IstreamInputStream istream(&new_ifstream);
if (proto->ParseFromZeroCopyStream(&istream)) {
return Status::OK;
}
}
}
std::ifstream input(primary_file_);
if (!input) {
return Status(StatusCode::NOT_FOUND,
"Unable to open the file at `" + primary_file_ + "`",
strerror(errno));
}
google::protobuf::io::IstreamInputStream istream(&input);
if (!proto->ParseFromZeroCopyStream(&istream)) {
return Status(
StatusCode::INVALID_ARGUMENT,
"Unable to parse the protobuf from the store. Data is corrupt.");
}
if (!input.eof()) {
return Status(StatusCode::DATA_LOSS,
"Reading from the file at `" + primary_file_ +
"` did not reach the end of the file.",
strerror(errno));
}
return Status::OK;
}
Status ConsistentProtoStore::WriteToTmp(const MessageLite &proto) {
std::ofstream tmpfile(tmp_file_);
if (!tmpfile) {
return Status(
StatusCode::DATA_LOSS,
"Unable to open the temp file to write the proto `" + tmp_file_ + "`",
strerror(errno));
}
google::protobuf::io::OstreamOutputStream outstream(&tmpfile);
if (!proto.SerializeToZeroCopyStream(&outstream)) {
return Status(StatusCode::DATA_LOSS,
"Unable to serialize proto to the output stream.");
}
if (!tmpfile) {
return Status(StatusCode::DATA_LOSS,
"Writing proto to temp file (" + tmp_file_ + ") failed.",
strerror(errno));
}
return Status::OK;
}
Status ConsistentProtoStore::MoveTmpToOverride() {
if (!fs_->Rename(tmp_file_, override_file_)) {
return Status(
StatusCode::DATA_LOSS,
"Unable to rename `" + tmp_file_ + "` => `" + override_file_ + "`.",
strerror(errno));
}
return Status::OK;
}
Status ConsistentProtoStore::DeletePrimary() {
// If the primary file doesn't exist. We don't care.
if (!fs_->FileExists(primary_file_)) {
return Status::OK;
}
if (!fs_->Delete(primary_file_)) {
return Status(StatusCode::ABORTED,
"Unable to remove old file `" + primary_file_ + "`.",
strerror(errno));
}
return Status::OK;
}
Status ConsistentProtoStore::MoveOverrideToPrimary() {
if (!fs_->Rename(override_file_, primary_file_)) {
return Status(
StatusCode::ABORTED,
"Unable to rename `" + override_file_ + "` => `" + primary_file_ + "`.",
strerror(errno));
}
return Status::OK;
}
} // namespace util
} // namespace cobalt