Skip to content

Commit d3be494

Browse files
committed
feat core: add prolong() to dist lock strategy for cheaper TTL refresh
commit_hash:177bb59df1c01236aa4a5228ed331745f2a4b8e0
1 parent 1622611 commit d3be494

3 files changed

Lines changed: 100 additions & 1 deletion

File tree

core/include/userver/dist_lock/dist_lock_strategy.hpp

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,24 @@ class DistLockStrategyBase {
3434
/// cleanup, Release won't be invoked.
3535
virtual void Acquire(std::chrono::milliseconds lock_ttl, const std::string& locker_id) = 0;
3636

37+
/// Prolongs (refreshes the TTL of) a lock already held by @a locker_id.
38+
///
39+
/// Called instead of Acquire() on every refresh while the lock is held, so
40+
/// backends may implement a cheaper query than the full Acquire().
41+
///
42+
/// @param lock_ttl The new duration for which the lock must be held.
43+
/// @param locker_id Globally unique ID of the locking entity, must be the
44+
/// same as in Acquire().
45+
/// @throws LockIsAcquiredByAnotherHostException when the lock is no longer
46+
/// held by @a locker_id (ownership was lost)
47+
/// @throws anything else when the prolongation fails, strategy is responsible
48+
/// for cleanup, Release won't be invoked.
49+
/// @note The default implementation simply calls Acquire(), preserving the
50+
/// legacy behaviour for strategies that do not override it.
51+
virtual void Prolong(std::chrono::milliseconds lock_ttl, const std::string& locker_id) {
52+
Acquire(lock_ttl, locker_id);
53+
}
54+
3755
/// Releases the lock.
3856
///
3957
/// @param locker_id Globally unique ID of the locking entity, must be the

core/src/dist_lock/dist_lock_test.cpp

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ class MockDistLockStrategy final : public dist_lock::DistLockStrategyBase {
4848
*locked_by = locker_id;
4949
}
5050

51+
void Prolong(std::chrono::milliseconds lock_ttl, const std::string& locker_id) override {
52+
Acquire(lock_ttl, locker_id);
53+
prolongs_++;
54+
}
55+
5156
void Release(const std::string& locker_id) override {
5257
auto locked_by = locked_by_var_.Lock();
5358
if (*locked_by == locker_id) {
@@ -69,15 +74,51 @@ class MockDistLockStrategy final : public dist_lock::DistLockStrategyBase {
6974

7075
size_t GetAttemptsCount() const { return attempts_; }
7176

77+
size_t GetProlongsCount() const { return prolongs_; }
78+
7279
private:
7380
concurrent::Variable<std::string> locked_by_var_;
7481
std::atomic<bool> allowed_{false};
7582
std::atomic<size_t> attempts_{0};
83+
std::atomic<size_t> prolongs_{0};
7684
};
7785
/// [Sample distlock strategy]
7886

7987
auto MakeMockStrategy() { return std::make_shared<MockDistLockStrategy>(); }
8088

89+
// A strategy that intentionally does NOT override Prolong(), so refreshes fall
90+
// back to the default DistLockStrategyBase::Prolong() (which calls Acquire()).
91+
class DefaultProlongStrategy final : public dist_lock::DistLockStrategyBase {
92+
public:
93+
~DefaultProlongStrategy() override {
94+
auto locked_by = locked_by_var_.Lock();
95+
EXPECT_TRUE(locked_by->empty());
96+
}
97+
98+
void Acquire(std::chrono::milliseconds, const std::string& locker_id) override {
99+
UASSERT(!locker_id.empty());
100+
acquires_++;
101+
auto locked_by = locked_by_var_.Lock();
102+
if (!locked_by->empty() && *locked_by != locker_id) {
103+
throw dist_lock::LockIsAcquiredByAnotherHostException();
104+
}
105+
*locked_by = locker_id;
106+
}
107+
108+
void Release(const std::string& locker_id) override {
109+
auto locked_by = locked_by_var_.Lock();
110+
if (*locked_by == locker_id) {
111+
locked_by->clear();
112+
}
113+
}
114+
115+
size_t GetAcquiresCount() const { return acquires_; }
116+
117+
private:
118+
concurrent::Variable<std::string> locked_by_var_;
119+
std::atomic<size_t> acquires_{0};
120+
};
121+
81122
class DistLockWorkload {
82123
public:
83124
explicit DistLockWorkload(bool abort_on_cancel = false)
@@ -470,4 +511,40 @@ UTEST_MT(LockedTask, SingleAttemptExample, 3) {
470511
/// [Sample distributed locked task SingleAttempt]
471512
}
472513

514+
UTEST_MT(LockedWorker, ProlongUsedForRefresh, 3) {
515+
auto strategy = MakeMockStrategy();
516+
DistLockWorkload work;
517+
dist_lock::DistLockedWorker locked_worker(kWorkerName, [&] { work.Work(); }, strategy, MakeSettings());
518+
519+
locked_worker.Start();
520+
strategy->Allow(true);
521+
EXPECT_TRUE(work.WaitForLocked(true, utest::kMaxTestWaitTime));
522+
523+
for (int i = 0; i < 100 && strategy->GetProlongsCount() < 2; ++i) {
524+
engine::InterruptibleSleepFor(kAttemptInterval);
525+
}
526+
527+
EXPECT_LE(2U, strategy->GetProlongsCount());
528+
EXPECT_LT(strategy->GetProlongsCount(), strategy->GetAttemptsCount());
529+
530+
locked_worker.Stop();
531+
}
532+
533+
UTEST_MT(LockedWorker, DefaultProlongRoutesThroughAcquire, 3) {
534+
auto strategy = std::make_shared<DefaultProlongStrategy>();
535+
DistLockWorkload work;
536+
dist_lock::DistLockedWorker locked_worker(kWorkerName, [&] { work.Work(); }, strategy, MakeSettings());
537+
538+
locked_worker.Start();
539+
EXPECT_TRUE(work.WaitForLocked(true, utest::kMaxTestWaitTime));
540+
541+
const auto acquires_after_take = strategy->GetAcquiresCount();
542+
for (int i = 0; i < 100 && strategy->GetAcquiresCount() <= acquires_after_take; ++i) {
543+
engine::InterruptibleSleepFor(kAttemptInterval);
544+
}
545+
EXPECT_LT(acquires_after_take, strategy->GetAcquiresCount());
546+
547+
locked_worker.Stop();
548+
}
549+
473550
USERVER_NAMESPACE_END

core/src/dist_lock/impl/locker.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,11 @@ void Locker::Run(LockerMode mode, dist_lock::DistLockWaitingMode waiting_mode, t
111111

112112
try {
113113
if (settings.is_enabled || is_locked_) {
114-
strategy_->Acquire(settings.lock_ttl, Id());
114+
if (is_locked_) {
115+
strategy_->Prolong(settings.lock_ttl, Id());
116+
} else {
117+
strategy_->Acquire(settings.lock_ttl, Id());
118+
}
115119
stats_.lock_successes++;
116120
if (!ExchangeLockState(true, attempt_start)) {
117121
LOG_DEBUG() << "Starting watchdog task";

0 commit comments

Comments
 (0)