Lightweight 0.20260522.0
Loading...
Searching...
No Matches
SqlScopedLock.hpp
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4
5#include "Api.hpp"
6#include "SqlAdvisoryLock.hpp"
7#include "SqlConnection.hpp"
8
9#include <chrono>
10#include <expected>
11#include <string>
12#include <string_view>
13
14namespace Lightweight
15{
16
17/// RAII-style cross-process advisory lock.
18///
19/// Provides a distributed locking mechanism so that only one process at a
20/// time can hold a named token. The dialect-specific primitive is selected
21/// by the active `SqlQueryFormatter`'s `AdvisoryLockOps()` handler:
22/// - SQL Server: `sp_getapplock` / `sp_releaseapplock`
23/// - PostgreSQL: `pg_advisory_lock` / `pg_advisory_unlock`
24/// - SQLite: `_lightweight_locks` table guarded by a unique constraint
25///
26/// `SqlScopedLock` itself contains zero per-DBMS branching — adding a new
27/// dialect only requires implementing a `SqlAdvisoryLockHandler` and wiring
28/// it to the new formatter's `AdvisoryLockOps()`.
29///
30/// @code
31/// // Throws on failure (timeout, deadlock, or driver error):
32/// auto lock = SqlScopedLock { connection, "my-resource" };
33///
34/// // Or, with structured error handling:
35/// auto maybeLock = SqlScopedLock::TryConstruct(connection, "my-resource");
36/// if (!maybeLock)
37/// std::println(stderr, "{}", maybeLock.error().message);
38/// @endcode
40{
41 public:
42 /// Acquire a named advisory lock.
43 ///
44 /// @param connection Database connection to use for locking.
45 /// @param lockName Name of the lock (cooperative; any string).
46 /// @param timeout Maximum time to wait for lock acquisition.
47 /// @throws std::runtime_error if the lock cannot be acquired (timeout, deadlock,
48 /// or driver error). The thrown exception's `what()` contains a
49 /// human-readable explanation. For programmatic access to the failure
50 /// reason, use the non-throwing `TryConstruct` factory.
51 LIGHTWEIGHT_API explicit SqlScopedLock(SqlConnection& connection,
52 std::string_view lockName,
53 std::chrono::milliseconds timeout = std::chrono::seconds(30));
54
55 /// Non-throwing factory that returns either a held lock or a structured
56 /// `SqlLockError` describing why the acquire failed. Prefer this when
57 /// the caller wants to react programmatically to the failure reason
58 /// (timeout vs deadlock vs driver error) — for example, to retry with
59 /// backoff or to show a tailored UI message.
60 [[nodiscard]] LIGHTWEIGHT_API static std::expected<SqlScopedLock, SqlLockError> TryConstruct(
61 SqlConnection& connection, std::string_view lockName, std::chrono::milliseconds timeout = std::chrono::seconds(30));
62
63 /// Releases the lock on destruction. Release-time errors are routed to
64 /// `SqlLogger::OnWarning` rather than being silently swallowed.
65 LIGHTWEIGHT_API ~SqlScopedLock();
66
67 SqlScopedLock(SqlScopedLock const&) = delete;
68 SqlScopedLock& operator=(SqlScopedLock const&) = delete;
69
70 /// Move constructor.
71 LIGHTWEIGHT_API SqlScopedLock(SqlScopedLock&& other) noexcept;
72
73 /// Move assignment operator.
74 LIGHTWEIGHT_API SqlScopedLock& operator=(SqlScopedLock&& other) noexcept;
75
76 /// Check if the lock is currently held by this instance.
77 [[nodiscard]] bool IsLocked() const noexcept
78 {
79 return _locked;
80 }
81
82 /// Returns the lock name passed at construction.
83 [[nodiscard]] std::string_view Name() const noexcept
84 {
85 return _lockName;
86 }
87
88 /// Release the lock early.
89 ///
90 /// Automatically called in the destructor; can be invoked manually for
91 /// finer scope control. Returns the underlying `SqlLockError` if the
92 /// release round-trip fails so callers can log or surface it.
93 [[nodiscard]] LIGHTWEIGHT_API std::expected<void, SqlLockError> Release();
94
95 private:
96 /// Internal constructor used by `TryConstruct` to skip the throwing
97 /// acquire path — the caller has already inspected the handler's
98 /// `TryAcquire` result and confirmed success.
99 struct AlreadyLockedTag
100 {
101 };
102 SqlScopedLock(SqlConnection& connection,
103 std::string_view lockName,
104 SqlAdvisoryLockHandler const& handler,
105 AlreadyLockedTag /*tag*/) noexcept;
106
107 SqlConnection* _connection;
108 std::string _lockName;
109 SqlAdvisoryLockHandler const* _handler { nullptr };
110 bool _locked { false };
111};
112
113} // namespace Lightweight
Represents a connection to a SQL database.
LIGHTWEIGHT_API SqlScopedLock(SqlScopedLock &&other) noexcept
Move constructor.
bool IsLocked() const noexcept
Check if the lock is currently held by this instance.
static LIGHTWEIGHT_API std::expected< SqlScopedLock, SqlLockError > TryConstruct(SqlConnection &connection, std::string_view lockName, std::chrono::milliseconds timeout=std::chrono::seconds(30))
LIGHTWEIGHT_API SqlScopedLock(SqlConnection &connection, std::string_view lockName, std::chrono::milliseconds timeout=std::chrono::seconds(30))
LIGHTWEIGHT_API SqlScopedLock & operator=(SqlScopedLock &&other) noexcept
Move assignment operator.
LIGHTWEIGHT_API std::expected< void, SqlLockError > Release()
std::string_view Name() const noexcept
Returns the lock name passed at construction.
LIGHTWEIGHT_API ~SqlScopedLock()