PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
study_lock_manager.cpp
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2021-2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
11
12#include <kcenon/common/patterns/result.h>
13
14#include <algorithm>
15#include <iomanip>
16#include <random>
17#include <sstream>
18#include <thread>
19
20namespace kcenon::pacs::workflow {
21
22// =============================================================================
23// Construction / Destruction
24// =============================================================================
25
28
31
33 // Release all locks on destruction
34 std::unique_lock lock{mutex_};
35 locks_.clear();
36 token_to_study_.clear();
37}
38
40 : config_{std::move(other.config_)},
41 locks_{std::move(other.locks_)},
42 token_to_study_{std::move(other.token_to_study_)},
43 stats_{std::move(other.stats_)},
44 next_token_id_{other.next_token_id_.load()},
45 on_lock_acquired_{std::move(other.on_lock_acquired_)},
46 on_lock_released_{std::move(other.on_lock_released_)},
47 on_lock_expired_{std::move(other.on_lock_expired_)} {}
48
50 study_lock_manager&& other) noexcept {
51 if (this != &other) {
52 std::unique_lock lock{mutex_};
53 config_ = std::move(other.config_);
54 locks_ = std::move(other.locks_);
55 token_to_study_ = std::move(other.token_to_study_);
56 stats_ = std::move(other.stats_);
57 next_token_id_.store(other.next_token_id_.load());
58 on_lock_acquired_ = std::move(other.on_lock_acquired_);
59 on_lock_released_ = std::move(other.on_lock_released_);
60 on_lock_expired_ = std::move(other.on_lock_expired_);
61 }
62 return *this;
63}
64
65// =============================================================================
66// Lock Acquisition
67// =============================================================================
68
70 const std::string& study_uid,
71 const std::string& reason,
72 const std::string& holder,
73 std::chrono::seconds timeout) -> kcenon::common::Result<lock_token> {
74 return lock(study_uid, lock_type::exclusive, reason, holder, timeout);
75}
76
78 const std::string& study_uid,
79 lock_type type,
80 const std::string& reason,
81 const std::string& holder,
82 std::chrono::seconds timeout) -> kcenon::common::Result<lock_token> {
83 const auto resolved_holder = resolve_holder(holder);
84 const auto expiry = calculate_expiry(timeout);
85 const auto now = std::chrono::system_clock::now();
86
87 std::unique_lock lock{mutex_};
88
89 // Check if lock can be acquired
90 auto it = locks_.find(study_uid);
91 if (it != locks_.end()) {
92 auto& existing = it->second;
93
94 // Check if existing lock is expired
95 if (existing.info.is_expired()) {
96 // Clean up expired lock
97 auto token_it = token_to_study_.find(existing.info.token_id);
98 if (token_it != token_to_study_.end()) {
99 token_to_study_.erase(token_it);
100 }
101 locks_.erase(it);
102 it = locks_.end();
103
104 // Notify expiration
105 if (on_lock_expired_) {
106 lock.unlock();
107 on_lock_expired_(study_uid, existing.info);
108 lock.lock();
109 }
110 }
111 }
112
113 // Re-check after potential cleanup
114 it = locks_.find(study_uid);
115 if (it != locks_.end()) {
116 auto& existing = it->second;
117
118 // Handle shared lock upgrade/coexistence
119 if (type == lock_type::shared &&
120 existing.info.type == lock_type::shared) {
121 // Check max shared locks
122 if (existing.shared_holders.size() >= config_.max_shared_locks) {
124 kcenon::common::error_info{
126 "Maximum shared locks exceeded",
127 "study_lock_manager"});
128 }
129
130 // Add holder to shared list
131 existing.shared_holders.push_back(resolved_holder);
132 existing.info.shared_count = existing.shared_holders.size();
133
134 // Generate new token for this shared holder
135 const auto token_id = generate_token_id();
136 token_to_study_[token_id] = study_uid;
137
138 lock_token token;
139 token.token_id = token_id;
140 token.study_uid = study_uid;
141 token.type = lock_type::shared;
142 token.acquired_at = now;
143 token.expires_at = expiry;
144
145 record_acquisition(type);
146
147 if (on_lock_acquired_) {
148 lock_info info = existing.info;
149 info.token_id = token_id;
150 info.holder = resolved_holder;
151 lock.unlock();
152 on_lock_acquired_(study_uid, info);
153 }
154
156 }
157
158 // Cannot acquire lock - already locked
159 {
160 std::lock_guard stats_lock{stats_mutex_};
161 ++stats_.contention_count;
162 }
163
165 kcenon::common::error_info{
167 "Study is already locked by: " + existing.info.holder,
168 "study_lock_manager",
169 "Lock type: " + to_string(existing.info.type)});
170 }
171
172 // Create new lock
173 const auto token_id = generate_token_id();
174
175 lock_entry entry;
176 entry.info.study_uid = study_uid;
177 entry.info.type = type;
178 entry.info.reason = reason;
179 entry.info.holder = resolved_holder;
180 entry.info.token_id = token_id;
181 entry.info.acquired_at = now;
182 entry.info.expires_at = expiry;
183 entry.info.shared_count = (type == lock_type::shared) ? 1 : 0;
184
185 if (type == lock_type::shared) {
186 entry.shared_holders.push_back(resolved_holder);
187 }
188
189 locks_[study_uid] = std::move(entry);
190 token_to_study_[token_id] = study_uid;
191
192 lock_token token;
193 token.token_id = token_id;
194 token.study_uid = study_uid;
195 token.type = type;
196 token.acquired_at = now;
197 token.expires_at = expiry;
198
199 record_acquisition(type);
200
201 if (on_lock_acquired_) {
202 lock_info info = locks_[study_uid].info;
203 lock.unlock();
204 on_lock_acquired_(study_uid, info);
205 }
206
208}
209
211 const std::string& study_uid,
212 lock_type type,
213 const std::string& reason,
214 const std::string& holder,
215 std::chrono::seconds timeout) -> kcenon::common::Result<lock_token> {
216 // try_lock is the same as lock for this implementation
217 // since we don't block waiting
218 return lock(study_uid, type, reason, holder, timeout);
219}
220
221// =============================================================================
222// Lock Release
223// =============================================================================
224
227 std::unique_lock lock{mutex_};
228
229 // Find study by token
230 auto token_it = token_to_study_.find(token.token_id);
231 if (token_it == token_to_study_.end()) {
233 kcenon::common::error_info{
235 "Invalid or expired token",
236 "study_lock_manager"});
237 }
238
239 const auto study_uid = token_it->second;
240 auto lock_it = locks_.find(study_uid);
241 if (lock_it == locks_.end()) {
242 // Token exists but lock doesn't - clean up
243 token_to_study_.erase(token_it);
245 kcenon::common::error_info{
247 "Lock not found",
248 "study_lock_manager"});
249 }
250
251 auto& entry = lock_it->second;
252 const auto duration = entry.info.duration();
253 const auto type = entry.info.type;
254 lock_info released_info = entry.info;
255
256 // Handle shared lock release
257 if (type == lock_type::shared && entry.shared_holders.size() > 1) {
258 // Just remove this holder from shared list
259 auto holder_it = std::find(
260 entry.shared_holders.begin(),
261 entry.shared_holders.end(),
262 token.token_id); // Using token_id as holder identifier for shared
263
264 if (holder_it != entry.shared_holders.end()) {
265 entry.shared_holders.erase(holder_it);
266 }
267 token_to_study_.erase(token_it);
268 entry.info.shared_count = entry.shared_holders.size();
269
270 record_release(type, duration);
271
272 if (on_lock_released_) {
273 lock.unlock();
274 on_lock_released_(study_uid, released_info);
275 }
276
277 return kcenon::common::Result<std::monostate>::ok(std::monostate{});
278 }
279
280 // Remove lock entirely
281 token_to_study_.erase(token_it);
282 locks_.erase(lock_it);
283
284 record_release(type, duration);
285
286 if (on_lock_released_) {
287 lock.unlock();
288 on_lock_released_(study_uid, released_info);
289 }
290
291 return kcenon::common::Result<std::monostate>::ok(std::monostate{});
292}
293
295 const std::string& study_uid,
296 const std::string& holder)
298 const auto resolved_holder = resolve_holder(holder);
299
300 std::unique_lock lock{mutex_};
301
302 auto lock_it = locks_.find(study_uid);
303 if (lock_it == locks_.end()) {
305 kcenon::common::error_info{
307 "Lock not found for study",
308 "study_lock_manager"});
309 }
310
311 auto& entry = lock_it->second;
312
313 // Verify holder matches
314 if (entry.info.holder != resolved_holder) {
315 // Check shared holders
316 bool found_shared = false;
317 if (entry.info.type == lock_type::shared) {
318 auto holder_it = std::find(
319 entry.shared_holders.begin(),
320 entry.shared_holders.end(),
321 resolved_holder);
322 if (holder_it != entry.shared_holders.end()) {
323 found_shared = true;
324 entry.shared_holders.erase(holder_it);
325 entry.info.shared_count = entry.shared_holders.size();
326 }
327 }
328
329 if (!found_shared) {
331 kcenon::common::error_info{
333 "Lock held by different holder: " + entry.info.holder,
334 "study_lock_manager"});
335 }
336
337 if (!entry.shared_holders.empty()) {
338 // Other shared holders remain
339 record_release(entry.info.type, entry.info.duration());
340 return kcenon::common::Result<std::monostate>::ok(std::monostate{});
341 }
342 }
343
344 const auto duration = entry.info.duration();
345 const auto type = entry.info.type;
346 lock_info released_info = entry.info;
347
348 // Remove token mapping
349 auto token_it = token_to_study_.find(entry.info.token_id);
350 if (token_it != token_to_study_.end()) {
351 token_to_study_.erase(token_it);
352 }
353
354 locks_.erase(lock_it);
355
356 record_release(type, duration);
357
358 if (on_lock_released_) {
359 lock.unlock();
360 on_lock_released_(study_uid, released_info);
361 }
362
363 return kcenon::common::Result<std::monostate>::ok(std::monostate{});
364}
365
367 const std::string& study_uid,
368 [[maybe_unused]] const std::string& admin_reason)
370 // Note: admin_reason can be used for audit logging in future
371 if (!config_.allow_force_unlock) {
373 kcenon::common::error_info{
375 "Force unlock is not allowed",
376 "study_lock_manager"});
377 }
378
379 std::unique_lock lock{mutex_};
380
381 auto lock_it = locks_.find(study_uid);
382 if (lock_it == locks_.end()) {
384 kcenon::common::error_info{
386 "Lock not found for study",
387 "study_lock_manager"});
388 }
389
390 const auto duration = lock_it->second.info.duration();
391 const auto type = lock_it->second.info.type;
392 lock_info released_info = lock_it->second.info;
393
394 // Remove token mapping
395 auto token_it = token_to_study_.find(lock_it->second.info.token_id);
396 if (token_it != token_to_study_.end()) {
397 token_to_study_.erase(token_it);
398 }
399
400 // Remove all shared holder tokens
401 for (const auto& shared_holder : lock_it->second.shared_holders) {
402 // Shared holders may have separate tokens - clean up if needed
403 (void)shared_holder;
404 }
405
406 locks_.erase(lock_it);
407
408 {
409 std::lock_guard stats_lock{stats_mutex_};
410 ++stats_.force_unlock_count;
411 }
412 record_release(type, duration);
413
414 if (on_lock_released_) {
415 lock.unlock();
416 on_lock_released_(study_uid, released_info);
417 }
418
419 return kcenon::common::Result<std::monostate>::ok(std::monostate{});
420}
421
422auto study_lock_manager::unlock_all_by_holder(const std::string& holder)
423 -> std::size_t {
424 const auto resolved_holder = resolve_holder(holder);
425 std::size_t count = 0;
426
427 std::unique_lock lock{mutex_};
428
429 std::vector<std::string> to_remove;
430 for (auto& [study_uid, entry] : locks_) {
431 if (entry.info.holder == resolved_holder) {
432 to_remove.push_back(study_uid);
433 } else if (entry.info.type == lock_type::shared) {
434 // Check shared holders
435 auto it = std::find(
436 entry.shared_holders.begin(),
437 entry.shared_holders.end(),
438 resolved_holder);
439 if (it != entry.shared_holders.end()) {
440 entry.shared_holders.erase(it);
441 entry.info.shared_count = entry.shared_holders.size();
442 ++count;
443
444 if (entry.shared_holders.empty()) {
445 to_remove.push_back(study_uid);
446 }
447 }
448 }
449 }
450
451 for (const auto& study_uid : to_remove) {
452 auto it = locks_.find(study_uid);
453 if (it != locks_.end()) {
454 // Remove token mapping
455 auto token_it = token_to_study_.find(it->second.info.token_id);
456 if (token_it != token_to_study_.end()) {
457 token_to_study_.erase(token_it);
458 }
459
460 record_release(it->second.info.type, it->second.info.duration());
461 locks_.erase(it);
462 ++count;
463 }
464 }
465
466 return count;
467}
468
469// =============================================================================
470// Lock Status
471// =============================================================================
472
473auto study_lock_manager::is_locked(const std::string& study_uid) const -> bool {
474 std::shared_lock lock{mutex_};
475
476 auto it = locks_.find(study_uid);
477 if (it == locks_.end()) {
478 return false;
479 }
480
481 // Check if expired
482 if (it->second.info.is_expired()) {
483 return false;
484 }
485
486 return true;
487}
488
490 const std::string& study_uid,
491 lock_type type) const -> bool {
492 std::shared_lock lock{mutex_};
493
494 auto it = locks_.find(study_uid);
495 if (it == locks_.end()) {
496 return false;
497 }
498
499 if (it->second.info.is_expired()) {
500 return false;
501 }
502
503 return it->second.info.type == type;
504}
505
506auto study_lock_manager::get_lock_info(const std::string& study_uid) const
507 -> std::optional<lock_info> {
508 std::shared_lock lock{mutex_};
509
510 auto it = locks_.find(study_uid);
511 if (it == locks_.end()) {
512 return std::nullopt;
513 }
514
515 if (it->second.info.is_expired()) {
516 return std::nullopt;
517 }
518
519 return it->second.info;
520}
521
523 const std::string& token_id) const
524 -> std::optional<lock_info> {
525 std::shared_lock lock{mutex_};
526
527 auto token_it = token_to_study_.find(token_id);
528 if (token_it == token_to_study_.end()) {
529 return std::nullopt;
530 }
531
532 auto lock_it = locks_.find(token_it->second);
533 if (lock_it == locks_.end()) {
534 return std::nullopt;
535 }
536
537 if (lock_it->second.info.is_expired()) {
538 return std::nullopt;
539 }
540
541 return lock_it->second.info;
542}
543
544auto study_lock_manager::validate_token(const lock_token& token) const -> bool {
545 std::shared_lock lock{mutex_};
546
547 auto token_it = token_to_study_.find(token.token_id);
548 if (token_it == token_to_study_.end()) {
549 return false;
550 }
551
552 auto lock_it = locks_.find(token_it->second);
553 if (lock_it == locks_.end()) {
554 return false;
555 }
556
557 if (lock_it->second.info.is_expired()) {
558 return false;
559 }
560
561 return token_it->second == token.study_uid;
562}
563
565 const lock_token& token,
566 std::chrono::seconds extension)
568 std::unique_lock lock{mutex_};
569
570 auto token_it = token_to_study_.find(token.token_id);
571 if (token_it == token_to_study_.end()) {
573 kcenon::common::error_info{
575 "Invalid or expired token",
576 "study_lock_manager"});
577 }
578
579 auto lock_it = locks_.find(token_it->second);
580 if (lock_it == locks_.end()) {
582 kcenon::common::error_info{
584 "Lock not found",
585 "study_lock_manager"});
586 }
587
588 if (lock_it->second.info.is_expired()) {
590 kcenon::common::error_info{
592 "Lock has expired",
593 "study_lock_manager"});
594 }
595
596 // Calculate new expiry
597 auto new_extension = extension.count() > 0 ? extension : config_.default_timeout;
598 if (new_extension.count() > 0) {
599 lock_it->second.info.expires_at =
600 std::chrono::system_clock::now() + new_extension;
601 }
602
603 lock_token updated_token = token;
604 updated_token.expires_at = lock_it->second.info.expires_at;
605
606 return kcenon::common::Result<lock_token>::ok(updated_token);
607}
608
609// =============================================================================
610// Lock Queries
611// =============================================================================
612
613auto study_lock_manager::get_all_locks() const -> std::vector<lock_info> {
614 std::shared_lock lock{mutex_};
615
616 std::vector<lock_info> result;
617 result.reserve(locks_.size());
618
619 for (const auto& [study_uid, entry] : locks_) {
620 if (!entry.info.is_expired()) {
621 result.push_back(entry.info);
622 }
623 }
624
625 return result;
626}
627
628auto study_lock_manager::get_locks_by_holder(const std::string& holder) const
629 -> std::vector<lock_info> {
630 const auto resolved_holder = resolve_holder(holder);
631 std::shared_lock lock{mutex_};
632
633 std::vector<lock_info> result;
634
635 for (const auto& [study_uid, entry] : locks_) {
636 if (entry.info.is_expired()) continue;
637
638 if (entry.info.holder == resolved_holder) {
639 result.push_back(entry.info);
640 } else if (entry.info.type == lock_type::shared) {
641 auto it = std::find(
642 entry.shared_holders.begin(),
643 entry.shared_holders.end(),
644 resolved_holder);
645 if (it != entry.shared_holders.end()) {
646 result.push_back(entry.info);
647 }
648 }
649 }
650
651 return result;
652}
653
655 -> std::vector<lock_info> {
656 std::shared_lock lock{mutex_};
657
658 std::vector<lock_info> result;
659
660 for (const auto& [study_uid, entry] : locks_) {
661 if (!entry.info.is_expired() && entry.info.type == type) {
662 result.push_back(entry.info);
663 }
664 }
665
666 return result;
667}
668
670 std::shared_lock lock{mutex_};
671
672 std::vector<lock_info> result;
673
674 for (const auto& [study_uid, entry] : locks_) {
675 if (entry.info.is_expired()) {
676 result.push_back(entry.info);
677 }
678 }
679
680 return result;
681}
682
683// =============================================================================
684// Maintenance
685// =============================================================================
686
688 std::unique_lock lock{mutex_};
689
690 std::vector<std::string> expired_studies;
691
692 for (const auto& [study_uid, entry] : locks_) {
693 if (entry.info.is_expired()) {
694 expired_studies.push_back(study_uid);
695 }
696 }
697
698 for (const auto& study_uid : expired_studies) {
699 auto it = locks_.find(study_uid);
700 if (it != locks_.end()) {
701 lock_info expired_info = it->second.info;
702
703 // Remove token mapping
704 auto token_it = token_to_study_.find(it->second.info.token_id);
705 if (token_it != token_to_study_.end()) {
706 token_to_study_.erase(token_it);
707 }
708
709 locks_.erase(it);
710
711 if (on_lock_expired_) {
712 lock.unlock();
713 on_lock_expired_(study_uid, expired_info);
714 lock.lock();
715 }
716 }
717 }
718
719 return expired_studies.size();
720}
721
723 std::lock_guard stats_lock{stats_mutex_};
724 std::shared_lock lock{mutex_};
725
726 lock_manager_stats current_stats = stats_;
727 current_stats.active_locks = 0;
728 current_stats.exclusive_locks = 0;
729 current_stats.shared_locks = 0;
730 current_stats.migration_locks = 0;
731
732 for (const auto& [study_uid, entry] : locks_) {
733 if (!entry.info.is_expired()) {
734 ++current_stats.active_locks;
735 switch (entry.info.type) {
737 ++current_stats.exclusive_locks;
738 break;
740 ++current_stats.shared_locks;
741 break;
743 ++current_stats.migration_locks;
744 break;
745 }
746 }
747 }
748
749 return current_stats;
750}
751
753 std::lock_guard stats_lock{stats_mutex_};
755}
756
758 -> const study_lock_manager_config& {
759 return config_;
760}
761
763 std::unique_lock lock{mutex_};
764 config_ = config;
765}
766
767// =============================================================================
768// Event Callbacks
769// =============================================================================
770
774
778
780 on_lock_expired_ = std::move(callback);
781}
782
783// =============================================================================
784// Internal Methods
785// =============================================================================
786
787auto study_lock_manager::generate_token_id() const -> std::string {
788 const auto id = next_token_id_.fetch_add(1);
789 const auto now = std::chrono::system_clock::now();
790 const auto time_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
791 now.time_since_epoch()).count();
792
793 std::ostringstream oss;
794 oss << "lock_" << std::hex << std::setfill('0')
795 << std::setw(12) << time_ms
796 << "_" << std::setw(8) << id;
797
798 return oss.str();
799}
800
801auto study_lock_manager::resolve_holder(const std::string& holder) const
802 -> std::string {
803 if (!holder.empty()) {
804 return holder;
805 }
806
807 // Use thread ID as default holder
808 std::ostringstream oss;
809 oss << "thread_" << std::this_thread::get_id();
810 return oss.str();
811}
812
813auto study_lock_manager::calculate_expiry(std::chrono::seconds timeout) const
814 -> std::optional<std::chrono::system_clock::time_point> {
815 auto effective_timeout = timeout.count() > 0 ? timeout : config_.default_timeout;
816 if (effective_timeout.count() <= 0) {
817 return std::nullopt; // No expiration
818 }
819
820 return std::chrono::system_clock::now() + effective_timeout;
821}
822
824 const std::string& study_uid,
825 lock_type type) const -> bool {
826 auto it = locks_.find(study_uid);
827 if (it == locks_.end()) {
828 return true;
829 }
830
831 if (it->second.info.is_expired()) {
832 return true;
833 }
834
835 // Shared locks can coexist with other shared locks
836 if (type == lock_type::shared &&
837 it->second.info.type == lock_type::shared) {
838 return it->second.shared_holders.size() < config_.max_shared_locks;
839 }
840
841 return false;
842}
843
845 std::lock_guard stats_lock{stats_mutex_};
847 (void)type; // Type-specific stats are calculated in get_stats()
848}
849
851 lock_type type,
852 std::chrono::milliseconds duration) {
853 std::lock_guard stats_lock{stats_mutex_};
855
856 // Update average duration
857 if (stats_.total_releases > 0) {
858 auto total_duration =
860 duration.count();
862 std::chrono::milliseconds{total_duration / stats_.total_releases};
863 }
864
865 // Update max duration
866 if (duration > stats_.max_lock_duration) {
867 stats_.max_lock_duration = duration;
868 }
869
870 (void)type; // Type-specific stats are calculated in get_stats()
871}
872
873} // namespace kcenon::pacs::workflow
Manages locks on DICOM studies for concurrent access control.
auto calculate_expiry(std::chrono::seconds timeout) const -> std::optional< std::chrono::system_clock::time_point >
Calculate expiration time.
auto is_locked(const std::string &study_uid) const -> bool
Check if a study is locked.
void set_on_lock_expired(lock_event_callback callback)
Set callback for lock expiration events.
std::function< void( const std::string &study_uid, const lock_info &info)> lock_event_callback
Callback type for lock events.
auto force_unlock(const std::string &study_uid, const std::string &admin_reason="") -> kcenon::common::Result< std::monostate >
Force release a lock (admin operation)
study_lock_manager()
Construct lock manager with default configuration.
std::map< std::string, lock_entry > locks_
Lock entries (study_uid -> lock_entry)
auto get_locks_by_type(lock_type type) const -> std::vector< lock_info >
Get all locks of a specific type.
auto get_lock_info(const std::string &study_uid) const -> std::optional< lock_info >
Get lock information for a study.
study_lock_manager & operator=(const study_lock_manager &)=delete
void set_on_lock_released(lock_event_callback callback)
Set callback for lock release events.
auto get_stats() const -> lock_manager_stats
Get lock manager statistics.
void reset_stats()
Reset statistics counters.
auto unlock_all_by_holder(const std::string &holder) -> std::size_t
Release all locks held by a specific holder.
auto get_config() const -> const study_lock_manager_config &
Get the current configuration.
lock_event_callback on_lock_acquired_
Event callbacks.
auto lock(const std::string &study_uid, const std::string &reason, const std::string &holder="", std::chrono::seconds timeout=std::chrono::seconds{0}) -> kcenon::common::Result< lock_token >
Acquire an exclusive lock on a study.
auto resolve_holder(const std::string &holder) const -> std::string
Get holder identifier (uses thread ID if empty)
void set_on_lock_acquired(lock_event_callback callback)
Set callback for lock acquisition events.
auto get_all_locks() const -> std::vector< lock_info >
Get all currently held locks.
auto unlock(const lock_token &token) -> kcenon::common::Result< std::monostate >
Release a lock using its token.
void record_acquisition(lock_type type)
Update statistics on lock acquisition.
auto get_locks_by_holder(const std::string &holder) const -> std::vector< lock_info >
Get all locks held by a specific holder.
~study_lock_manager()
Destructor - releases all locks.
auto get_lock_info_by_token(const std::string &token_id) const -> std::optional< lock_info >
Get lock information by token ID.
auto generate_token_id() const -> std::string
Generate a unique token ID.
std::map< std::string, std::string > token_to_study_
Token to study UID mapping for fast lookup.
auto try_lock(const std::string &study_uid, lock_type type, const std::string &reason, const std::string &holder="", std::chrono::seconds timeout=std::chrono::seconds{0}) -> kcenon::common::Result< lock_token >
Try to acquire a lock without blocking.
std::mutex stats_mutex_
Mutex for statistics.
std::atomic< uint64_t > next_token_id_
Next token ID counter.
auto validate_token(const lock_token &token) const -> bool
Validate a lock token.
auto can_acquire_lock(const std::string &study_uid, lock_type type) const -> bool
Check if a lock can be acquired.
std::shared_mutex mutex_
Mutex for thread-safe access.
auto get_expired_locks() const -> std::vector< lock_info >
Get all expired locks.
study_lock_manager_config config_
Configuration.
auto refresh_lock(const lock_token &token, std::chrono::seconds extension=std::chrono::seconds{0}) -> kcenon::common::Result< lock_token >
Refresh a lock (extend its timeout)
auto cleanup_expired_locks() -> std::size_t
Clean up expired locks.
void record_release(lock_type type, std::chrono::milliseconds duration)
Update statistics on lock release.
void set_config(const study_lock_manager_config &config)
Update configuration.
constexpr int max_shared_exceeded
Maximum shared locks exceeded.
constexpr int permission_denied
Permission denied (force unlock not allowed)
constexpr int expired
Lock has expired.
constexpr int already_locked
Lock already held by another holder.
constexpr int invalid_token
Invalid token.
constexpr int not_found
Lock not found.
lock_type
Type of lock to acquire on a study.
@ shared
Read-only access allowed (for read operations)
@ exclusive
No other access allowed (for modifications)
@ migration
Special lock for migration operations (highest priority)
auto to_string(lock_type type) -> std::string
Convert lock_type to string.
Detailed information about a lock on a study.
lock_type type
Type of lock held.
std::size_t shared_count
Number of shared lock holders (for shared locks)
std::string token_id
Lock token ID.
std::string reason
Reason for the lock.
std::chrono::system_clock::time_point acquired_at
When the lock was acquired.
std::string holder
Who holds the lock (user/service identifier)
std::string study_uid
Study UID that is locked.
std::optional< std::chrono::system_clock::time_point > expires_at
When the lock expires (if timeout set)
Statistics for lock manager operations.
std::chrono::milliseconds avg_lock_duration
Average lock duration.
std::size_t total_acquisitions
Total locks acquired.
std::chrono::milliseconds max_lock_duration
Maximum lock duration observed.
std::size_t active_locks
Number of currently held locks.
std::size_t shared_locks
Number of shared locks.
std::size_t exclusive_locks
Number of exclusive locks.
std::size_t migration_locks
Number of migration locks.
std::size_t total_releases
Total locks released.
Unique identifier for a lock.
std::string token_id
Unique token ID.
std::string study_uid
Study UID that is locked.
std::optional< std::chrono::system_clock::time_point > expires_at
When the lock expires (if timeout set)
std::chrono::system_clock::time_point acquired_at
When the lock was acquired.
lock_type type
Type of lock held.
lock_info info
std::vector< std::string > shared_holders
Configuration for the study lock manager.
Study lock manager for modification control and concurrent access.