89 : _owned{
owner, std::make_shared<State>(std::move(path), std::move(ioOps))} {}
91 ~FileActionLog()
override =
default;
116 _owned.read(
"FileActionLog::append").append(std::move(entry));
119 _owned.apply(
"FileActionLog::append", [entry = std::move(entry)](State& state)
mutable {
121 state.append(std::move(entry));
122 }
catch (
const std::exception& exc) {
123 state.notePostedFailure(exc.what());
150 void flush()
override { _owned.read(
"FileActionLog::flush").flush(); }
160 [[nodiscard]] std::vector<LogEntry>
entries(std::string_view entityKey = {})
const override {
161 return _owned.read(
"FileActionLog::entries").entries(entityKey);
204 void rotate(
const std::filesystem::path& sealedPath) { _owned.read(
"FileActionLog::rotate").rotate(sealedPath); }
212 std::string entityKey)
const override {
213 return _owned.ask<std::vector<LogEntry>>(
214 "FileActionLog::entries", replyExec,
215 [entityKey = std::move(entityKey)](State& state) {
return state.entries(entityKey); });
222 return _owned.ask<
bool>(
"FileActionLog::flush", replyExec, [](State& state) {
235 : _path{std::move(path)}, _io{std::move(ioOps)} {
245 ::morph::core::repairTornTail(_io, _path,
"FileActionLog");
259 for (
const auto& existing : entries({})) {
260 if (!existing.idempotencyKey.empty()) {
261 _seenIdempotencyKeys.insert(existing.idempotencyKey);
264 _file = _io.fopen(_path.string(),
"a");
265 ::morph::core::positionAtEnd(_file);
266 if (_file ==
nullptr) {
267 throw std::runtime_error(
"FileActionLog: failed to open " + _path.string());
281 auto const dirSync = ::morph::core::classifyDirectorySync(_io.syncPath(_path.parent_path()));
282 if (dirSync == ::morph::core::DirectorySync::failed) {
286 throw std::runtime_error(
"FileActionLog: failed to fsync directory after creating " + _path.string());
288 if (dirSync == ::morph::core::DirectorySync::unsupported) {
289 ::morph::log::logWarn(
290 "FileActionLog: cannot fsync the directory containing {}; the log's contents are still fsynced, "
291 "but its directory entry is only as durable as this filesystem makes it",
302 if (_file !=
nullptr) {
309 State(
const State&) =
delete;
310 State& operator=(
const State&) =
delete;
311 State(State&&) =
delete;
312 State& operator=(State&&) =
delete;
314 void append(LogEntry entry) {
316 requireOpen(
"append");
318 throw std::runtime_error(
"FileActionLog::append: refusing to append to " + _path.string() +
319 " after a short write that could not be rolled back; reopen the log to have "
322 if (!entry.idempotencyKey.empty() && (_seenIdempotencyKeys.contains(entry.idempotencyKey) ||
323 _unflushedIdempotencyKeys.contains(entry.idempotencyKey))) {
326 entry.seq = ++_nextSeq;
327 auto line = toJson(entry);
328 line.push_back(
'\n');
329 long long const offsetBeforeWrite = ::morph::core::wideFtell(_file);
330 if (_io.fwrite(line.data(), line.size(), _file) != line.size()) {
344 if (::morph::core::rollBackShortWrite(_io, _file, _path, offsetBeforeWrite) ==
345 ::morph::core::RollBack::torn) {
355 throw std::runtime_error(
"FileActionLog::append: short write to " + _path.string());
357 if (!entry.idempotencyKey.empty()) {
358 _unflushedIdempotencyKeys.insert(std::move(entry.idempotencyKey));
363 void notePostedFailure(std::string_view what) {
364 if (_postedFailure.empty()) {
365 _postedFailure = what;
370 requireOpen(
"flush");
371 if (_io.fflush(_file) != 0) {
372 _unflushedIdempotencyKeys.clear();
373 throw std::runtime_error(
"FileActionLog::flush: failed to flush " + _path.string());
375 if (_io.fsync(_file) != 0) {
376 _unflushedIdempotencyKeys.clear();
377 throw std::runtime_error(
"FileActionLog::flush: failed to fsync " + _path.string());
380 for (
const auto& key : _unflushedIdempotencyKeys) {
381 _seenIdempotencyKeys.insert(key);
383 _unflushedIdempotencyKeys.clear();
384 if (!_postedFailure.empty()) {
385 std::string
const what = std::exchange(_postedFailure, {});
386 throw std::runtime_error(
387 "FileActionLog::flush: an append posted from another thread was not written: " + what);
391 [[nodiscard]] std::vector<LogEntry> entries(std::string_view entityKey)
const {
392 std::ifstream input{_path};
393 if (!input && std::filesystem::exists(_path)) {
399 throw std::runtime_error(
"FileActionLog: cannot read " + _path.string());
401 std::vector<std::string> lines;
403 while (std::getline(input, line)) {
405 lines.push_back(line);
408 std::vector<LogEntry> out;
409 for (std::size_t i = 0; i < lines.size(); ++i) {
414 std::string
const& rawLine = lines.at(i);
418 }
catch (
const std::exception& exc) {
423 if (i + 1 == lines.size()) {
424 ::morph::log::logWarn(
"FileActionLog: skipping malformed trailing line in " + _path.string() +
425 ": " + std::string{exc.what()});
430 if (entityKey.empty() || entry.entityKey == entityKey) {
431 out.push_back(std::move(entry));
437 void rotate(
const std::filesystem::path& sealedPath) {
438 requireOpen(
"rotate");
443 if (_io.fflush(_file) != 0) {
444 throw std::runtime_error(
"FileActionLog::rotate: failed to flush " + _path.string());
446 if (_io.fsync(_file) != 0) {
447 throw std::runtime_error(
"FileActionLog::rotate: failed to fsync " + _path.string());
450 for (
const auto& key : _unflushedIdempotencyKeys) {
451 _seenIdempotencyKeys.insert(key);
453 _unflushedIdempotencyKeys.clear();
458 std::error_code renameError;
459 std::filesystem::rename(_path, sealedPath, renameError);
466 _file = _io.fopen(_path.string(),
"a");
467 ::morph::core::positionAtEnd(_file);
483 auto const classifyAndWarn = [
this](
const std::filesystem::path& dir) {
484 auto const outcome = ::morph::core::classifyDirectorySync(_io.syncPath(dir));
485 if (outcome == ::morph::core::DirectorySync::unsupported) {
486 ::morph::log::logWarn(
487 "FileActionLog::rotate: cannot fsync the directory {}; the log's contents are still fsynced, "
488 "but the rotated directory entries are only as durable as this filesystem makes them",
493 bool const dirSyncFailed = classifyAndWarn(_path.parent_path()) == ::morph::core::DirectorySync::failed;
494 bool const sealedDirSyncFailed =
495 sealedPath.parent_path() != _path.parent_path() &&
496 classifyAndWarn(sealedPath.parent_path()) == ::morph::core::DirectorySync::failed;
498 if (_file ==
nullptr) {
499 throw std::runtime_error(
"FileActionLog::rotate: failed to reopen " + _path.string() +
" after " +
500 (renameError ?
"a failed" :
"a successful") +
" rename to " +
501 sealedPath.string());
504 throw std::runtime_error(
"FileActionLog::rotate: failed to rename " + _path.string() +
" to " +
505 sealedPath.string() +
": " + renameError.message());
507 if (dirSyncFailed || sealedDirSyncFailed) {
508 throw std::runtime_error(
"FileActionLog::rotate: failed to fsync the directory after rotating " +
509 _path.string() +
" to " + sealedPath.string());
517 void requireOpen(std::string_view what)
const {
518 if (_file ==
nullptr) {
519 throw std::runtime_error(
"FileActionLog::" + std::string{what} +
": " + _path.string() +
520 " is not open (a previous rotate() failed to reopen it)");
524 std::filesystem::path _path;
525 ::morph::core::FileIoOps _io;
526 std::FILE* _file =
nullptr;
531 bool _tornTail =
false;
532 uint64_t _nextSeq{0};
534 std::unordered_set<std::string> _seenIdempotencyKeys;
538 std::unordered_set<std::string> _unflushedIdempotencyKeys;
540 std::string _postedFailure;
543 ::morph::exec::detail::OwnedState<State> _owned;
One recorded execution of an action against a model instance.