162 [[nodiscard]]
const std::string&
journalMode() const noexcept {
return _journalMode; }
200 std::optional<std::size_t>
maxDepth = std::nullopt,
202 std::chrono::milliseconds busyTimeout = std::chrono::milliseconds{
kBusyTimeoutMillis},
203 ::core::platform::WallClockRef wallClock = ::core::platform::defaultSystemWallClock())
205 busyTimeout, wallClock)} {}
207 ~SqliteOfflineQueue()
override =
default;
218 [[nodiscard]] uint64_t
enqueue(std::string payload)
override {
219 return _owned.read(
"SqliteOfflineQueue::enqueue").enqueue(payload);
235 [[nodiscard]] uint64_t
enqueue(std::string payload, std::string idempotencyKey)
override {
236 return _owned.read(
"SqliteOfflineQueue::enqueue").enqueue(payload, idempotencyKey);
241 [[nodiscard]] std::vector<QueueItem>
drain()
const override {
242 return _owned.read(
"SqliteOfflineQueue::drain").drain();
251 _owned.apply(
"SqliteOfflineQueue::markDone", [itemId](State& state) { state.markDone(itemId); });
259 _owned.apply(
"SqliteOfflineQueue::setAttempts",
260 [itemId, attempts](State& state) { state.setAttempts(itemId, attempts); });
265 [[nodiscard]] std::size_t
size()
const override {
return _owned.read(
"SqliteOfflineQueue::size").size(); }
270 [[nodiscard]] std::optional<std::size_t>
maxDepth()
const override {
return _maxDepth; }
282 _owned.apply(
"SqliteOfflineQueue::setIdempotencyKey",
283 [itemId, idempotencyKey = std::move(idempotencyKey)](State& state)
mutable {
284 state.setIdempotencyKey(itemId, idempotencyKey);
295 std::string idempotencyKey)
override {
296 return _owned.ask<uint64_t>(
"SqliteOfflineQueue::enqueue", replyExec,
297 [payload = std::move(payload), idempotencyKey = std::move(idempotencyKey)](
298 State& state)
mutable {
return state.enqueue(payload, idempotencyKey); });
306 return _owned.ask<std::vector<QueueItem>>(
"SqliteOfflineQueue::drain", replyExec,
307 [](State& state) {
return state.drain(); });
314 return _owned.ask<std::size_t>(
"SqliteOfflineQueue::size", replyExec,
315 [](State& state) {
return state.size(); });
325 _owned{owner, std::move(state)} {}
333 Synchronous synchronous, std::chrono::milliseconds busyTimeout, ::core::platform::WallClockRef wallClock)
334 : _path{std::move(path)},
336 _io{std::move(ioOps)},
337 _synchronous{synchronous},
338 _busyTimeout{busyTimeout},
339 _wallClock{wallClock} {
340 if (sqlite3_open(_path.string().c_str(), &_db) != SQLITE_OK) {
341 std::string const msg =
"SqliteOfflineQueue: failed to open " + _path.string() +
": " +
342 (_db != nullptr ? sqlite3_errmsg(_db) :
"unknown error");
343 if (_db != nullptr) {
347 throw SqliteOfflineQueueError{msg};
363 execOrThrow((
"PRAGMA busy_timeout=" + std::to_string(_busyTimeout.count()) +
";").c_str());
372 (
"PRAGMA synchronous=" + std::string{_synchronous ==
Synchronous::full ?
"FULL" :
"NORMAL"} +
";")
380 detail::StatementGuard
const guard{prepare(
"PRAGMA synchronous;")};
381 if (sqlite3_step(guard.get()) != SQLITE_ROW) {
382 throw SqliteOfflineQueueError{
"SqliteOfflineQueue: failed to read back synchronous"};
384 _synchronousLevel = sqlite3_column_int(guard.get(), 0);
387 execOrThrow(
"PRAGMA journal_mode=WAL;");
404 detail::StatementGuard
const guard{prepare(
"PRAGMA journal_mode;")};
405 if (sqlite3_step(guard.get()) != SQLITE_ROW) {
406 throw SqliteOfflineQueueError{
"SqliteOfflineQueue: failed to read back journal_mode"};
408 _journalMode = textColumn(guard.get(), 0);
409 if (_journalMode !=
"wal") {
410 ::morph::log::logWarn(
411 "SqliteOfflineQueue: PRAGMA journal_mode=WAL did not take for {} (got '{}'); continuing "
413 "that mode -- durability is carried by PRAGMA synchronous, but concurrent readers and the "
414 "writer will not overlap as they do under WAL",
415 _path.string(), _journalMode);
419 "CREATE TABLE IF NOT EXISTS morph_offline_queue ("
420 " id INTEGER PRIMARY KEY AUTOINCREMENT,"
421 " payload TEXT NOT NULL,"
422 " idempotency_key TEXT NOT NULL DEFAULT '',"
423 " attempts INTEGER NOT NULL DEFAULT 0,"
424 " enqueued_at INTEGER NOT NULL"
427 "CREATE UNIQUE INDEX IF NOT EXISTS ix_queue_idem "
428 "ON morph_offline_queue(idempotency_key) WHERE idempotency_key <> '';");
450 const char*
const backingFile = sqlite3_db_filename(_db,
"main");
451 if (backingFile !=
nullptr && *backingFile !=
'\0') {
452 std::filesystem::path
const backingPath{backingFile};
453 auto const dirSync = ::morph::core::classifyDirectorySync(_io.syncPath(backingPath.parent_path()));
454 if (dirSync == ::morph::core::DirectorySync::failed) {
455 throw SqliteOfflineQueueError{
"SqliteOfflineQueue: failed to fsync directory after creating " +
456 backingPath.string()};
458 if (dirSync == ::morph::core::DirectorySync::unsupported) {
459 ::morph::log::logWarn(
460 "SqliteOfflineQueue: cannot fsync the directory containing {}; SQLite's own fsyncs still "
461 "cover the database contents, but its directory entry is only as durable as this "
464 backingPath.string());
476 if (_db !=
nullptr) {
481 State(
const State&) =
delete;
482 State& operator=(
const State&) =
delete;
483 State(State&&) =
delete;
484 State& operator=(State&&) =
delete;
490 [[nodiscard]] uint64_t
enqueue(
const std::string& payload)
const {
492 detail::StatementGuard
const guard{
493 prepare(
"INSERT INTO morph_offline_queue (payload, idempotency_key, attempts, enqueued_at) "
494 "VALUES (?, '', 0, ?);")};
495 bindText(guard.get(), 1, payload);
496 bindInt64(guard.get(), 2, nowMillis());
497 stepOrThrow(guard.get(),
"enqueue");
498 return static_cast<uint64_t
>(sqlite3_last_insert_rowid(_db));
513 [[nodiscard]] uint64_t
enqueue(
const std::string& payload,
const std::string& idempotencyKey)
const {
514 if (idempotencyKey.empty()) {
516 detail::StatementGuard
const guard{
517 prepare(
"INSERT INTO morph_offline_queue (payload, idempotency_key, attempts, enqueued_at) "
518 "VALUES (?, '', 0, ?);")};
519 bindText(guard.get(), 1, payload);
520 bindInt64(guard.get(), 2, nowMillis());
521 stepOrThrow(guard.get(),
"enqueue");
522 return static_cast<uint64_t
>(sqlite3_last_insert_rowid(_db));
526 detail::StatementGuard
const insertGuard{
527 prepare(
"INSERT INTO morph_offline_queue (payload, idempotency_key, attempts, enqueued_at) "
528 "VALUES (?, ?, 0, ?) "
529 "ON CONFLICT(idempotency_key) WHERE idempotency_key <> '' DO NOTHING;")};
530 bindText(insertGuard.get(), 1, payload);
531 bindText(insertGuard.get(), 2, idempotencyKey);
532 bindInt64(insertGuard.get(), 3, nowMillis());
533 stepOrThrow(insertGuard.get(),
"enqueue");
534 if (sqlite3_changes(_db) > 0) {
535 return static_cast<uint64_t
>(sqlite3_last_insert_rowid(_db));
539 detail::StatementGuard
const lookupGuard{
540 prepare(
"SELECT id FROM morph_offline_queue WHERE idempotency_key = ?;")};
541 bindText(lookupGuard.get(), 1, idempotencyKey);
542 int const lookupResult = sqlite3_step(lookupGuard.get());
543 if (lookupResult != SQLITE_ROW) {
553 throw SqliteOfflineQueueError{
554 std::string{
"SqliteOfflineQueue: enqueue could not resolve the existing id "
555 "for a deduplicated idempotency key: "} +
556 sqlite3_errmsg(_db)};
558 return static_cast<uint64_t
>(sqlite3_column_int64(lookupGuard.get(), 0));
563 [[nodiscard]] std::vector<QueueItem>
drain()
const {
564 detail::StatementGuard
const guard{
565 prepare(
"SELECT id, payload, idempotency_key, attempts FROM morph_offline_queue ORDER BY id;")};
566 std::vector<QueueItem> out;
568 int const stepResult = sqlite3_step(guard.get());
569 if (stepResult == SQLITE_DONE) {
572 if (stepResult != SQLITE_ROW) {
578 throw SqliteOfflineQueueError{
579 std::string{
"SqliteOfflineQueue: drain failed part-way through after "} +
580 std::to_string(out.size()) +
" row(s): " + sqlite3_errmsg(_db)};
583 item.id =
static_cast<uint64_t
>(sqlite3_column_int64(guard.get(), 0));
584 item.payload = textColumn(guard.get(), 1);
585 item.idempotencyKey = textColumn(guard.get(), 2);
586 item.attempts =
static_cast<uint32_t
>(sqlite3_column_int64(guard.get(), 3));
587 out.push_back(std::move(item));
594 void markDone(uint64_t itemId)
const {
595 detail::StatementGuard
const guard{prepare(
"DELETE FROM morph_offline_queue WHERE id = ?;")};
596 bindInt64(guard.get(), 1,
static_cast<std::int64_t
>(itemId));
597 stepOrThrow(guard.get(),
"markDone");
603 void setAttempts(uint64_t itemId, Attempts attempts)
const {
604 detail::StatementGuard
const guard{prepare(
"UPDATE morph_offline_queue SET attempts = ? WHERE id = ?;")};
605 bindInt64(guard.get(), 1,
static_cast<std::int64_t
>(attempts.value()));
606 bindInt64(guard.get(), 2,
static_cast<std::int64_t
>(itemId));
607 stepOrThrow(guard.get(),
"setAttempts");
612 [[nodiscard]] std::size_t
size()
const {
return countRows(); }
616 [[nodiscard]] std::optional<std::size_t>
maxDepth()
const {
return _maxDepth; }
618 [[nodiscard]]
int synchronousLevel() const noexcept {
return _synchronousLevel; }
620 [[nodiscard]]
const std::string&
journalMode() const noexcept {
return _journalMode; }
629 void setIdempotencyKey(uint64_t itemId,
const std::string& idempotencyKey)
const {
643 detail::StatementGuard
const guard{
644 prepare(
"UPDATE morph_offline_queue SET idempotency_key = ?1 WHERE id = ?2 "
645 "AND (?1 = '' OR NOT EXISTS (SELECT 1 FROM morph_offline_queue "
646 "WHERE idempotency_key = ?1 AND id <> ?2));")};
647 bindText(guard.get(), 1, idempotencyKey);
648 bindInt64(guard.get(), 2,
static_cast<std::int64_t
>(itemId));
649 stepOrThrow(guard.get(),
"setIdempotencyKey");
652 void execOrThrow(
const char* sql)
const {
653 char* errMsg =
nullptr;
654 if (sqlite3_exec(_db, sql,
nullptr,
nullptr, &errMsg) != SQLITE_OK) {
655 std::string
const msg = errMsg !=
nullptr ? errMsg :
"unknown sqlite error";
656 sqlite3_free(errMsg);
657 throw SqliteOfflineQueueError{
"SqliteOfflineQueue: " + msg};
661 sqlite3_stmt* prepare(
const char* sql)
const {
662 sqlite3_stmt* stmt =
nullptr;
663 if (sqlite3_prepare_v2(_db, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
664 throw SqliteOfflineQueueError{std::string{
"SqliteOfflineQueue: prepare failed: "} +
665 sqlite3_errmsg(_db)};
682 void bindText(sqlite3_stmt* stmt,
int index,
const std::string& value)
const {
687 if (value.size() >
static_cast<std::size_t
>(INT_MAX)) {
688 throw SqliteOfflineQueueError{
"SqliteOfflineQueue: value exceeds INT_MAX bytes"};
690 if (sqlite3_bind_text(stmt, index, value.c_str(),
static_cast<int>(value.size()),
691 detail::kSqliteTransient) != SQLITE_OK) {
692 throw SqliteOfflineQueueError{std::string{
"SqliteOfflineQueue: bind failed: "} + sqlite3_errmsg(_db)};
696 void bindInt64(sqlite3_stmt* stmt,
int index, std::int64_t value)
const {
697 if (sqlite3_bind_int64(stmt, index, value) != SQLITE_OK) {
698 throw SqliteOfflineQueueError{std::string{
"SqliteOfflineQueue: bind failed: "} + sqlite3_errmsg(_db)};
702 void stepOrThrow(sqlite3_stmt* stmt,
const char* what)
const {
708 if (sqlite3_step(stmt) != SQLITE_DONE) {
709 throw SqliteOfflineQueueError{std::string{
"SqliteOfflineQueue: "} + what +
710 " failed: " + sqlite3_errmsg(_db)};
714 static std::string textColumn(sqlite3_stmt* stmt,
int index) {
721 const auto*
text =
reinterpret_cast<const char*
>(sqlite3_column_text(stmt, index));
722 return text !=
nullptr ? std::string{
text,
static_cast<std::size_t
>(sqlite3_column_bytes(stmt, index))}
726 [[nodiscard]] std::int64_t nowMillis() const noexcept {
727 return std::chrono::duration_cast<std::chrono::milliseconds>(_wallClock.now().time_since_epoch()).count();
732 std::size_t countRows()
const {
733 detail::StatementGuard
const guard{prepare(
"SELECT COUNT(*) FROM morph_offline_queue;")};
734 sqlite3_step(guard.get());
735 return static_cast<std::size_t
>(sqlite3_column_int64(guard.get(), 0));
740 void checkCapacity()
const {
744 std::size_t
const current = countRows();
745 if (current >= *_maxDepth) {
746 ::morph::observe::detail::emitMetric(::morph::observe::Metric::queueOverflow,
747 static_cast<double>(current));
748 throw OfflineQueueFullError{*_maxDepth, current};
753 std::filesystem::path _path;
754 mutable sqlite3* _db =
nullptr;
755 std::optional<std::size_t> _maxDepth;
756 ::morph::core::FileIoOps _io;
759 ::core::platform::WallClockRef _wallClock;
761 int _synchronousLevel{-1};
764 std::string _journalMode;
767 std::optional<std::size_t> _maxDepth;
769 int _synchronousLevel{-1};
772 std::string _journalMode;
773 ::morph::exec::detail::OwnedState<State> _owned;