173 : _maxDepth{
maxDepth}, _owned{owner, std::make_shared<State>(std::move(path), std::move(ioOps))} {}
175 ~FileOfflineQueue()
override =
default;
185 [[nodiscard]] uint64_t
enqueue(std::string payload)
override {
return enqueue(std::move(payload), {}); }
196 [[nodiscard]] uint64_t
enqueue(std::string payload, std::string idempotencyKey)
override {
197 return _owned.read(
"FileOfflineQueue::enqueue")
198 .enqueue(std::move(payload), std::move(idempotencyKey), _maxDepth);
203 [[nodiscard]] std::vector<QueueItem>
drain()
const override {
204 return _owned.read(
"FileOfflineQueue::drain").drain();
209 [[nodiscard]] std::size_t
size()
const override {
return _owned.read(
"FileOfflineQueue::size").size(); }
214 [[nodiscard]] std::optional<std::size_t>
maxDepth()
const override {
return _maxDepth; }
222 _owned.apply(
"FileOfflineQueue::markDone", [itemId](State& state) { state.markDone(itemId); });
230 _owned.apply(
"FileOfflineQueue::setAttempts",
231 [itemId, attempts](State& state) { state.setAttempts(itemId, attempts); });
243 _owned.apply(
"FileOfflineQueue::setIdempotencyKey",
244 [itemId, idempotencyKey = std::move(idempotencyKey)](State& state)
mutable {
245 state.setIdempotencyKey(itemId, std::move(idempotencyKey));
256 std::string idempotencyKey)
override {
257 return _owned.ask<uint64_t>(
"FileOfflineQueue::enqueue", replyExec,
258 [
maxDepth = _maxDepth, payload = std::move(payload),
259 idempotencyKey = std::move(idempotencyKey)](State& state)
mutable {
260 return state.enqueue(std::move(payload), std::move(idempotencyKey),
maxDepth);
269 return _owned.ask<std::vector<QueueItem>>(
"FileOfflineQueue::drain", replyExec,
270 [](State& state) {
return state.drain(); });
277 return _owned.ask<std::size_t>(
"FileOfflineQueue::size", replyExec, [](State& state) {
return state.size(); });
287 : _path{std::move(path)}, _io{std::move(ioOps)} {
310 _file = _io.
fopen(_path.string(),
"a");
311 ::morph::core::positionAtEnd(_file);
312 if (_file ==
nullptr) {
313 throw std::runtime_error(
"FileOfflineQueue: failed to open " + _path.string());
318 if (_file !=
nullptr) {
325 State(
const State&) =
delete;
326 State& operator=(
const State&) =
delete;
327 State(State&&) =
delete;
328 State& operator=(State&&) =
delete;
330 uint64_t enqueue(std::string payload, std::string idempotencyKey, std::optional<std::size_t>
maxDepth) {
332 if (!idempotencyKey.empty()) {
333 for (
const auto& [existingId, item] : _items) {
334 if (item.idempotencyKey == idempotencyKey) {
340 ::morph::observe::detail::emitMetric(::morph::observe::Metric::queueOverflow,
341 static_cast<double>(_items.size()));
342 throw OfflineQueueFullError{*
maxDepth, _items.size()};
344 uint64_t
const itemId = ++_nextId;
345 QueueItem item{.id = itemId, .payload = std::move(payload), .idempotencyKey = std::move(idempotencyKey)};
347 _items.emplace(itemId, std::move(item));
351 [[nodiscard]] std::vector<QueueItem>
drain()
const {
352 std::vector<QueueItem> out;
353 out.reserve(_items.size());
354 for (
const auto& entry : _items) {
355 out.push_back(entry.second);
360 [[nodiscard]] std::size_t
size()
const {
return _items.size(); }
362 void markDone(uint64_t itemId) {
363 auto iter = _items.find(itemId);
364 if (iter == _items.end()) {
378 void setAttempts(uint64_t itemId, Attempts attempts) {
379 auto iter = _items.find(itemId);
380 if (iter == _items.end()) {
387 auto updated = iter->second;
388 updated.attempts = attempts.value();
390 iter->second.attempts = attempts.value();
393 void setIdempotencyKey(uint64_t itemId, std::string idempotencyKey) {
394 auto iter = _items.find(itemId);
395 if (iter == _items.end()) {
398 iter->second.idempotencyKey = std::move(idempotencyKey);
399 appendPut(iter->second);
403 void appendPut(
const QueueItem& item) {
404 detail::FileQueueRecord
const record{.op =
"put",
406 .payload = item.payload,
407 .idempotencyKey = item.idempotencyKey,
408 .attempts = item.attempts};
409 writeLine(detail::toJson(record));
412 void appendDone(uint64_t itemId) {
413 detail::FileQueueRecord
const record{
414 .op =
"done", .id = itemId, .payload = {}, .idempotencyKey = {}, .attempts = 0};
415 writeLine(detail::toJson(record));
422 void writeLine(
const std::string& json) {
424 throw std::runtime_error(
425 "FileOfflineQueue: refusing to write to " + _path.string() +
426 " after a short write that could not be rolled back; reopen the queue to have it repaired");
428 std::string line = json;
429 line.push_back(
'\n');
430 long long const offsetBeforeWrite = ::morph::core::wideFtell(_file);
441 auto const rollBackAndThrow = [&](
const std::string& what) {
442 if (::morph::core::rollBackShortWrite(_io, _file, _path, offsetBeforeWrite) ==
443 ::morph::core::RollBack::torn) {
455 throw std::runtime_error(
"FileOfflineQueue: " + what +
" " + _path.string());
457 if (_io.fwrite(line.data(), line.size(), _file) != line.size()) {
473 rollBackAndThrow(
"short write to");
475 if (_io.fflush(_file) != 0) {
476 rollBackAndThrow(
"failed to flush");
478 if (_io.fsync(_file) != 0) {
484 rollBackAndThrow(
"failed to fsync");
488 void syncFile(std::FILE* file,
const std::string& what)
const {
489 if (_io.fflush(file) != 0) {
490 throw std::runtime_error(
"FileOfflineQueue: failed to flush " + what);
492 if (_io.fsync(file) != 0) {
493 throw std::runtime_error(
"FileOfflineQueue: failed to fsync " + what);
499 if (!std::filesystem::exists(_path)) {
502 std::ifstream input{_path};
510 throw std::runtime_error(
"FileOfflineQueue: cannot read " + _path.string());
512 std::vector<std::string> lines;
514 while (std::getline(input, line)) {
516 lines.push_back(line);
519 uint64_t highestId = 0;
523 throw std::runtime_error(
"FileOfflineQueue: read error on " + _path.string());
525 for (std::size_t i = 0; i < lines.size(); ++i) {
526 detail::FileQueueRecord record;
528 record = detail::fromJson(lines.at(i));
529 }
catch (
const std::exception& exc) {
530 if (i + 1 == lines.size()) {
531 ::morph::log::logWarn(
"FileOfflineQueue: skipping malformed trailing line in " +
532 _path.string() +
": " + std::string{exc.what()});
537 highestId = std::max(highestId, record.id);
538 if (record.op ==
"done") {
539 _items.erase(record.id);
541 _items[record.id] = QueueItem{.id = record.id,
542 .payload = record.payload,
543 .idempotencyKey = record.idempotencyKey,
544 .attempts = record.attempts};
555 std::string
const tmp = _path.string() +
".compact-tmp";
556 std::FILE* out = _io.fopen(tmp,
"w");
557 if (out ==
nullptr) {
558 throw std::runtime_error(
"FileOfflineQueue: failed to open " + tmp +
" for compaction");
567 class TempFileGuard {
569 TempFileGuard(std::FILE* file, std::string path) : _file{file}, _path{std::move(path)} {}
572 if (_file !=
nullptr) {
579 std::error_code errorCode;
580 std::filesystem::remove(_path, errorCode);
584 TempFileGuard(
const TempFileGuard&) =
delete;
585 TempFileGuard& operator=(
const TempFileGuard&) =
delete;
586 TempFileGuard(TempFileGuard&&) =
delete;
587 TempFileGuard& operator=(TempFileGuard&&) =
delete;
590 void releaseHandle() noexcept { _file =
nullptr; }
592 void commit() noexcept { _committed =
true; }
597 bool _committed =
false;
599 TempFileGuard guard{out, tmp};
601 auto writeRecord = [&](
const detail::FileQueueRecord& record) {
602 std::string outLine = detail::toJson(record);
603 outLine.push_back(
'\n');
604 if (_io.fwrite(outLine.data(), outLine.size(), out) != outLine.size()) {
605 throw std::runtime_error(
"FileOfflineQueue: short write during compaction of " + _path.string());
609 for (
const auto& [entryId, item] : _items) {
610 writeRecord(detail::FileQueueRecord{.op =
"put",
612 .payload = item.payload,
613 .idempotencyKey = item.idempotencyKey,
614 .attempts = item.attempts});
633 uint64_t
const maxSurviving = _items.empty() ? 0 : _items.rbegin()->first;
634 if (_nextId > maxSurviving) {
635 writeRecord(detail::FileQueueRecord{
636 .op =
"done", .id = _nextId, .payload = {}, .idempotencyKey = {}, .attempts = 0});
642 guard.releaseHandle();
643 std::filesystem::rename(tmp, _path);
651 auto const dirSync = ::morph::core::classifyDirectorySync(_io.syncPath(_path.parent_path()));
652 if (dirSync == ::morph::core::DirectorySync::failed) {
653 throw std::runtime_error(
"FileOfflineQueue: failed to fsync directory after compacting " +
656 if (dirSync == ::morph::core::DirectorySync::unsupported) {
657 ::morph::log::logWarn(
658 "FileOfflineQueue: cannot fsync the directory containing {}; the queue's contents are still "
660 "but its directory entry is only as durable as this filesystem makes it",
665 std::filesystem::path _path;
666 ::morph::core::FileIoOps _io;
667 std::FILE* _file =
nullptr;
673 bool _tornTail =
false;
674 std::map<uint64_t, QueueItem> _items;
678 std::optional<std::size_t> _maxDepth;
679 ::morph::exec::detail::OwnedState<State> _owned;