PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
auto_prefetch_service.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
19
20#include <kcenon/common/interfaces/executor_interface.h>
21
22#include <algorithm>
23#include <chrono>
24#include <ranges>
25#include <sstream>
26
27namespace kcenon::pacs::workflow {
28
29// =========================================================================
30// Construction
31// =========================================================================
32
35 const prefetch_service_config& config)
36 : database_(database)
37 , config_(config) {
39 start();
40 }
41}
42
45 std::shared_ptr<kcenon::thread::thread_pool> thread_pool,
46 const prefetch_service_config& config)
47 : database_(database)
48 , thread_pool_(std::move(thread_pool))
49 , config_(config) {
51 start();
52 }
53}
54
57 std::shared_ptr<kcenon::common::interfaces::IExecutor> executor,
58 const prefetch_service_config& config)
59 : database_(database)
60 , executor_(std::move(executor))
61 , config_(config) {
63 start();
64 }
65}
66
70
71// =========================================================================
72// Lifecycle Management
73// =========================================================================
74
78
80 if (enabled_.exchange(true)) {
81 return; // Already enabled
82 }
83
84 stop_requested_.store(false);
85 next_cycle_time_ = std::chrono::steady_clock::now();
86
87 worker_thread_ = std::thread([this]() {
88 run_loop();
89 });
90
92 "Auto prefetch service started interval_seconds={} max_concurrent={}",
95}
96
97void auto_prefetch_service::disable(bool wait_for_completion) {
98 stop(wait_for_completion);
99}
100
101void auto_prefetch_service::stop(bool wait_for_completion) {
102 if (!enabled_.exchange(false)) {
103 return; // Already disabled
104 }
105
106 stop_requested_.store(true);
107
108 // Wake up the worker thread
109 cv_.notify_all();
110
111 if (wait_for_completion && worker_thread_.joinable()) {
112 worker_thread_.join();
113 } else if (worker_thread_.joinable()) {
114 worker_thread_.detach();
115 }
116
117 integration::logger_adapter::info("Auto prefetch service stopped");
118}
119
120auto auto_prefetch_service::is_enabled() const noexcept -> bool {
121 return enabled_.load();
122}
123
124auto auto_prefetch_service::is_running() const noexcept -> bool {
125 return is_enabled();
126}
127
128// =========================================================================
129// Manual Operations
130// =========================================================================
131
133 const std::string& patient_id,
134 std::chrono::days lookback) -> prefetch_result {
135
136 prefetch_request request;
137 request.patient_id = patient_id;
138 request.request_time = std::chrono::system_clock::now();
139
140 // Override criteria lookback if specified
141 auto saved_lookback = config_.criteria.lookback_period;
142 config_.criteria.lookback_period = lookback;
143
144 auto result = process_request(request);
145
146 config_.criteria.lookback_period = saved_lookback;
147
148 return result;
149}
150
152 const std::vector<storage::worklist_item>& worklist_items) {
153 on_worklist_query(worklist_items);
154}
155
157 std::lock_guard<std::mutex> lock(mutex_);
158 next_cycle_time_ = std::chrono::steady_clock::now();
159 cv_.notify_one();
160}
161
163 return execute_cycle();
164}
165
166// =========================================================================
167// Worklist Event Handler
168// =========================================================================
169
171 const std::vector<storage::worklist_item>& worklist_items) {
172
173 for (const auto& item : worklist_items) {
174 if (item.patient_id.empty()) {
175 continue;
176 }
177
178 prefetch_request request;
179 request.patient_id = item.patient_id;
180 request.patient_name = item.patient_name;
181 request.scheduled_modality = item.modality;
182 request.scheduled_study_uid = item.study_uid;
183 request.request_time = std::chrono::system_clock::now();
184
185 queue_request(request);
186 }
187
188 // Wake up worker if there are new requests
189 cv_.notify_one();
190
192 "Queued prefetch requests from worklist worklist_items={} queue_size={}",
193 worklist_items.size(),
195}
196
197// =========================================================================
198// Statistics and Monitoring
199// =========================================================================
200
202 -> std::optional<prefetch_result> {
203 std::lock_guard<std::mutex> lock(mutex_);
204 return last_result_;
205}
206
208 std::lock_guard<std::mutex> lock(mutex_);
209 return cumulative_stats_;
210}
211
213 -> std::optional<std::chrono::seconds> {
214 if (!enabled_.load()) {
215 return std::nullopt;
216 }
217
218 std::lock_guard<std::mutex> lock(mutex_);
219 auto now = std::chrono::steady_clock::now();
220 if (next_cycle_time_ <= now) {
221 return std::chrono::seconds{0};
222 }
223
224 return std::chrono::duration_cast<std::chrono::seconds>(
225 next_cycle_time_ - now);
226}
227
228auto auto_prefetch_service::cycles_completed() const noexcept -> std::size_t {
229 return cycles_count_.load();
230}
231
232auto auto_prefetch_service::pending_requests() const noexcept -> std::size_t {
233 std::lock_guard<std::mutex> lock(queue_mutex_);
234 return request_queue_.size();
235}
236
237// =========================================================================
238// Configuration
239// =========================================================================
240
242 std::chrono::seconds interval) {
243 std::lock_guard<std::mutex> lock(mutex_);
244 config_.prefetch_interval = interval;
245}
246
248 -> std::chrono::seconds {
249 std::lock_guard<std::mutex> lock(mutex_);
251}
252
254 const prefetch_criteria& criteria) {
255 std::lock_guard<std::mutex> lock(mutex_);
256 config_.criteria = criteria;
257}
258
260 -> const prefetch_criteria& {
261 std::lock_guard<std::mutex> lock(mutex_);
262 return config_.criteria;
263}
264
267 std::lock_guard<std::mutex> lock(mutex_);
268 config_.on_cycle_complete = std::move(callback);
269}
270
273 std::lock_guard<std::mutex> lock(mutex_);
274 config_.on_prefetch_error = std::move(callback);
275}
276
277// =========================================================================
278// Internal Methods
279// =========================================================================
280
282 integration::logger_adapter::debug("Prefetch service worker thread started");
283
284 while (!stop_requested_.load()) {
285 std::unique_lock<std::mutex> lock(mutex_);
286
287 // Wait until next cycle time or until woken up
288 auto wait_until = next_cycle_time_;
289 cv_.wait_until(lock, wait_until, [this]() {
290 return stop_requested_.load() ||
291 std::chrono::steady_clock::now() >= next_cycle_time_ ||
292 !request_queue_.empty();
293 });
294
295 if (stop_requested_.load()) {
296 break;
297 }
298
299 // Check if we should run a cycle
300 auto now = std::chrono::steady_clock::now();
301 bool should_run_cycle = (now >= next_cycle_time_) ||
302 (!request_queue_.empty());
303
304 if (should_run_cycle) {
305 lock.unlock();
306
307 cycle_in_progress_.store(true);
308 auto result = execute_cycle();
309 cycle_in_progress_.store(false);
310
311 lock.lock();
312
313 // Update last result
314 last_result_ = result;
315 update_stats(result);
317
318 // Schedule next cycle
319 next_cycle_time_ = std::chrono::steady_clock::now() +
321
322 // Invoke callback
324 lock.unlock();
326 lock.lock();
327 }
328 }
329 }
330
331 integration::logger_adapter::debug("Prefetch service worker thread stopped");
332}
333
335 prefetch_result cycle_result;
336 cycle_result.timestamp = std::chrono::system_clock::now();
337 auto cycle_start = std::chrono::steady_clock::now();
338
339 // Process all pending requests
340 while (auto request = dequeue_request()) {
341 if (stop_requested_.load()) {
342 break;
343 }
344
345 auto result = process_request(*request);
346 cycle_result += result;
347 }
348
349 cycle_result.duration = std::chrono::duration_cast<std::chrono::milliseconds>(
350 std::chrono::steady_clock::now() - cycle_start);
351
353 "Prefetch cycle completed patients={} studies_prefetched={} studies_failed={} duration_ms={}",
354 cycle_result.patients_processed,
355 cycle_result.studies_prefetched,
356 cycle_result.studies_failed,
357 cycle_result.duration.count());
358
359 // Record metrics
361 "prefetch_cycle_duration_ms",
362 static_cast<double>(cycle_result.duration.count()));
364 "prefetch_studies_total",
365 static_cast<int64_t>(cycle_result.studies_prefetched));
367 "prefetch_failures_total",
368 static_cast<int64_t>(cycle_result.studies_failed));
369
370 return cycle_result;
371}
372
374 -> prefetch_result {
375
376 prefetch_result result;
377 result.patients_processed = 1;
378 result.timestamp = std::chrono::system_clock::now();
379 auto start_time = std::chrono::steady_clock::now();
380
382 "Processing prefetch request patient_id={} scheduled_modality={}",
383 request.patient_id,
384 request.scheduled_modality);
385
386 // Query each remote PACS for prior studies
387 for (const auto& pacs : config_.remote_pacs) {
388 if (!pacs.is_valid()) {
389 continue;
390 }
391
392 // Query for prior studies
393 auto prior_studies = query_prior_studies(
394 pacs,
395 request.patient_id,
396 config_.criteria.lookback_period);
397
398 // Filter based on criteria
399 auto filtered_studies = filter_studies(prior_studies, request);
400
401 // Prefetch each study
402 for (const auto& study : filtered_studies) {
403 // Skip if already present locally
404 if (study_exists_locally(study.study_instance_uid)) {
406 continue;
407 }
408
409 // Skip the scheduled study itself
410 if (study.study_instance_uid == request.scheduled_study_uid) {
411 continue;
412 }
413
414 // Attempt prefetch
415 bool success = prefetch_study(pacs, study);
416
417 if (success) {
418 ++result.studies_prefetched;
419 result.series_prefetched += study.number_of_series;
420 result.instances_prefetched += study.number_of_instances;
421
422 if (config_.on_prefetch_complete) {
423 config_.on_prefetch_complete(
424 request.patient_id, study, true, "");
425 }
426 } else {
427 ++result.studies_failed;
428
429 if (config_.on_prefetch_error) {
430 config_.on_prefetch_error(
431 request.patient_id,
432 study.study_instance_uid,
433 "Failed to prefetch study");
434 }
435 }
436
437 // Check rate limiting
438 if (config_.rate_limit_per_minute > 0) {
439 auto elapsed = std::chrono::steady_clock::now() - start_time;
440 auto elapsed_minutes =
441 std::chrono::duration_cast<std::chrono::minutes>(elapsed)
442 .count();
443 if (elapsed_minutes < 1) {
444 auto prefetched_this_minute =
445 result.studies_prefetched + result.studies_failed;
446 if (prefetched_this_minute >= config_.rate_limit_per_minute) {
447 // Wait until the next minute
448 std::this_thread::sleep_for(
449 std::chrono::seconds(60) - elapsed);
450 }
451 }
452 }
453 }
454 }
455
456 result.duration = std::chrono::duration_cast<std::chrono::milliseconds>(
457 std::chrono::steady_clock::now() - start_time);
458
459 return result;
460}
461
463 const remote_pacs_config& pacs_config,
464 const std::string& patient_id,
465 std::chrono::days lookback) -> std::vector<prior_study_info> {
466
467 std::vector<prior_study_info> results;
468
469 // Calculate date range
470 auto now = std::chrono::system_clock::now();
471 auto from_time = now - lookback;
472
473 // Format dates as YYYYMMDD
474 auto format_date = [](std::chrono::system_clock::time_point tp) {
475 auto time_t = std::chrono::system_clock::to_time_t(tp);
476 std::tm tm = *std::localtime(&time_t);
477 char buffer[9];
478 std::strftime(buffer, sizeof(buffer), "%Y%m%d", &tm);
479 return std::string(buffer);
480 };
481
482 std::string from_date = format_date(from_time);
483 std::string to_date = format_date(now);
484
486 "Querying prior studies remote_pacs={} patient_id={} from_date={} to_date={}",
487 pacs_config.ae_title,
488 patient_id,
489 from_date,
490 to_date);
491
492 try {
493 // Configure association for C-FIND
494 network::association_config assoc_config;
495 assoc_config.calling_ae_title = pacs_config.local_ae_title;
496 assoc_config.called_ae_title = pacs_config.ae_title;
497 assoc_config.proposed_contexts.push_back({
498 1,
500 {"1.2.840.10008.1.2.1", "1.2.840.10008.1.2"}
501 });
502
503 // Connect to remote PACS
504 auto connect_result = network::association::connect(
505 pacs_config.host,
506 pacs_config.port,
507 assoc_config,
508 std::chrono::duration_cast<network::association::duration>(
509 pacs_config.connection_timeout));
510
511 if (connect_result.is_err()) {
513 "Failed to connect to remote PACS for C-FIND remote_pacs={} error={}",
514 pacs_config.ae_title,
515 connect_result.error().message);
516 return results;
517 }
518
519 auto assoc = std::move(connect_result.value());
520
521 // Build query keys
523 keys.patient_id = patient_id;
524 keys.study_date = from_date + "-" + to_date;
525
526 // Execute C-FIND query
527 services::query_scu_config query_config;
529 query_config.level = services::query_level::study;
530 query_config.timeout = std::chrono::duration_cast<std::chrono::milliseconds>(
531 pacs_config.association_timeout);
532
533 services::query_scu scu(query_config);
534 auto find_result = scu.find_studies(assoc, keys);
535
536 if (find_result.is_ok() && find_result.value().is_success()) {
537 for (const auto& dataset : find_result.value().matches) {
538 prior_study_info info;
539 info.study_instance_uid =
540 dataset.get_string(core::tags::study_instance_uid);
541 info.patient_id = patient_id;
542 info.patient_name =
543 dataset.get_string(core::tags::patient_name);
544 info.study_date =
545 dataset.get_string(core::tags::study_date);
546 info.study_description =
547 dataset.get_string(core::tags::study_description);
548
549 // Parse modalities in study (backslash-separated)
550 auto modalities_str =
551 dataset.get_string(core::tags::modalities_in_study);
552 if (!modalities_str.empty()) {
553 std::istringstream ss(modalities_str);
554 std::string mod;
555 while (std::getline(ss, mod, '\\')) {
556 if (!mod.empty()) {
557 info.modalities.insert(mod);
558 }
559 }
560 }
561
562 // Parse numeric fields
563 auto num_series_str = dataset.get_string(
565 if (!num_series_str.empty()) {
566 try {
567 info.number_of_series = std::stoull(num_series_str);
568 } catch (...) {}
569 }
570
571 auto num_instances_str = dataset.get_string(
573 if (!num_instances_str.empty()) {
574 try {
575 info.number_of_instances = std::stoull(num_instances_str);
576 } catch (...) {}
577 }
578
579 results.push_back(std::move(info));
580 }
581
583 "C-FIND query returned matches={} remote_pacs={} patient_id={}",
584 results.size(),
585 pacs_config.ae_title,
586 patient_id);
587 } else if (find_result.is_err()) {
589 "C-FIND query failed remote_pacs={} error={}",
590 pacs_config.ae_title,
591 find_result.error().message);
592 }
593
594 // Release association
595 (void)assoc.release();
596 } catch (const std::exception& e) {
598 "Exception querying prior studies remote_pacs={} error={}",
599 pacs_config.ae_title,
600 e.what());
601 }
602
603 return results;
604}
605
607 const std::vector<prior_study_info>& studies,
608 const prefetch_request& request) -> std::vector<prior_study_info> {
609
610 std::vector<prior_study_info> filtered;
611
612 for (const auto& study : studies) {
613 // Apply modality filters
614 bool modality_match = true;
615
616 // Check include list (if not empty, study must match)
617 if (!config_.criteria.include_modalities.empty()) {
618 modality_match = false;
619 for (const auto& mod : study.modalities) {
620 if (config_.criteria.include_modalities.count(mod) > 0) {
621 modality_match = true;
622 break;
623 }
624 }
625 }
626
627 // Check exclude list
628 if (modality_match && !config_.criteria.exclude_modalities.empty()) {
629 for (const auto& mod : study.modalities) {
630 if (config_.criteria.exclude_modalities.count(mod) > 0) {
631 modality_match = false;
632 break;
633 }
634 }
635 }
636
637 if (!modality_match) {
638 continue;
639 }
640
641 // Apply body part filter
642 if (!config_.criteria.include_body_parts.empty()) {
643 if (config_.criteria.include_body_parts.count(
644 study.body_part_examined) == 0) {
645 continue;
646 }
647 }
648
649 filtered.push_back(study);
650 }
651
652 // Sort by preference if enabled
653 if (config_.criteria.prefer_same_modality ||
654 config_.criteria.prefer_same_body_part) {
655 std::ranges::sort(filtered, [&](const auto& a, const auto& b) {
656 int score_a = 0;
657 int score_b = 0;
658
659 if (config_.criteria.prefer_same_modality) {
660 if (a.modalities.count(request.scheduled_modality) > 0) {
661 score_a += 10;
662 }
663 if (b.modalities.count(request.scheduled_modality) > 0) {
664 score_b += 10;
665 }
666 }
667
668 if (config_.criteria.prefer_same_body_part) {
669 if (a.body_part_examined == request.scheduled_body_part) {
670 score_a += 5;
671 }
672 if (b.body_part_examined == request.scheduled_body_part) {
673 score_b += 5;
674 }
675 }
676
677 // Higher score first, then by date (newer first)
678 if (score_a != score_b) {
679 return score_a > score_b;
680 }
681 return a.study_date > b.study_date;
682 });
683 }
684
685 // Limit results
686 if (config_.criteria.max_studies_per_patient > 0 &&
687 filtered.size() > config_.criteria.max_studies_per_patient) {
688 filtered.resize(config_.criteria.max_studies_per_patient);
689 }
690
691 return filtered;
692}
693
694auto auto_prefetch_service::study_exists_locally(const std::string& study_uid)
695 -> bool {
696 // Query local database to check if study exists
697 auto study_record = database_.find_study(study_uid);
698 return study_record.has_value();
699}
700
702 const remote_pacs_config& pacs_config,
703 const prior_study_info& study) -> bool {
704
706 "Prefetching study study_uid={} patient_id={} remote_pacs={}",
707 study.study_instance_uid,
708 study.patient_id,
709 pacs_config.ae_title);
710
711 try {
712 // Configure association for C-MOVE
713 network::association_config assoc_config;
714 assoc_config.calling_ae_title = pacs_config.local_ae_title;
715 assoc_config.called_ae_title = pacs_config.ae_title;
716 assoc_config.proposed_contexts.push_back({
717 1,
719 {"1.2.840.10008.1.2.1", "1.2.840.10008.1.2"}
720 });
721
722 // Connect to remote PACS
723 auto connect_result = network::association::connect(
724 pacs_config.host,
725 pacs_config.port,
726 assoc_config,
727 std::chrono::duration_cast<network::association::duration>(
728 pacs_config.connection_timeout));
729
730 if (connect_result.is_err()) {
732 "Failed to connect to remote PACS for C-MOVE remote_pacs={} error={}",
733 pacs_config.ae_title,
734 connect_result.error().message);
735 return false;
736 }
737
738 auto assoc = std::move(connect_result.value());
739
740 // Configure retrieve SCU with our local AE as move destination
741 services::retrieve_scu_config retrieve_config;
742 retrieve_config.mode = services::retrieve_mode::c_move;
744 retrieve_config.level = services::query_level::study;
745 retrieve_config.move_destination = pacs_config.local_ae_title;
746 retrieve_config.timeout = std::chrono::duration_cast<std::chrono::milliseconds>(
747 pacs_config.association_timeout);
748
749 services::retrieve_scu scu(retrieve_config);
750 auto move_result = scu.retrieve_study(
751 assoc, study.study_instance_uid);
752
753 // Release association
754 (void)assoc.release();
755
756 if (move_result.is_ok() && move_result.value().is_success()) {
758 "Successfully prefetched study study_uid={} completed={} remote_pacs={}",
759 study.study_instance_uid,
760 move_result.value().completed,
761 pacs_config.ae_title);
762 return true;
763 }
764
765 if (move_result.is_err()) {
767 "C-MOVE failed study_uid={} remote_pacs={} error={}",
768 study.study_instance_uid,
769 pacs_config.ae_title,
770 move_result.error().message);
771 } else if (move_result.value().has_failures()) {
773 "C-MOVE completed with failures study_uid={} completed={} failed={} remote_pacs={}",
774 study.study_instance_uid,
775 move_result.value().completed,
776 move_result.value().failed,
777 pacs_config.ae_title);
778 }
779
780 return false;
781 } catch (const std::exception& e) {
783 "Exception during prefetch study_uid={} remote_pacs={} error={}",
784 study.study_instance_uid,
785 pacs_config.ae_title,
786 e.what());
787 return false;
788 }
789}
790
794
796 std::lock_guard<std::mutex> lock(queue_mutex_);
797
798 // Deduplicate by patient ID
799 if (queued_patients_.count(request.patient_id) > 0) {
800 return;
801 }
802
803 request_queue_.push(request);
804 queued_patients_.insert(request.patient_id);
805}
806
808 -> std::optional<prefetch_request> {
809 std::lock_guard<std::mutex> lock(queue_mutex_);
810
811 if (request_queue_.empty()) {
812 return std::nullopt;
813 }
814
815 auto request = std::move(request_queue_.front());
816 request_queue_.pop();
817 queued_patients_.erase(request.patient_id);
818
819 return request;
820}
821
822} // namespace kcenon::pacs::workflow
DICOM Association management per PS3.8.
Automatic prefetch service for prior studies.
static void debug(kcenon::pacs::compat::format_string< Args... > fmt, Args &&... args)
Log a debug-level message.
static void info(kcenon::pacs::compat::format_string< Args... > fmt, Args &&... args)
Log an info-level message.
static void error(kcenon::pacs::compat::format_string< Args... > fmt, Args &&... args)
Log an error-level message.
static void record_histogram(std::string_view name, double value)
Record a histogram sample.
static void increment_counter(std::string_view name, std::int64_t value=1)
Increment a counter metric.
static Result< association > connect(const std::string &host, uint16_t port, const association_config &config, duration timeout=default_timeout)
Initiate an SCU association to a remote SCP.
network::Result< query_result > find_studies(network::association &assoc, const study_query_keys &keys)
Query for studies.
network::Result< retrieve_result > retrieve_study(network::association &assoc, std::string_view study_uid, retrieve_progress_callback progress=nullptr)
Retrieve a study by Study Instance UID.
std::set< std::string > queued_patients_
Set of patient IDs currently in queue (for deduplication)
auto get_last_result() const -> std::optional< prefetch_result >
Get the result of the last prefetch cycle.
auto get_cumulative_stats() const -> prefetch_result
Get cumulative statistics since service started.
void on_worklist_query(const std::vector< storage::worklist_item > &worklist_items)
Handle worklist query event.
std::optional< prefetch_result > last_result_
Last prefetch result.
std::condition_variable cv_
Condition variable for sleep/wake.
auto cycles_completed() const noexcept -> std::size_t
Get the number of cycles completed.
void set_prefetch_criteria(const prefetch_criteria &criteria)
Update the prefetch criteria.
void disable(bool wait_for_completion=true)
Disable/stop the prefetch service.
auto is_running() const noexcept -> bool
Check if the service is running (alias for is_enabled)
std::atomic< bool > enabled_
Flag indicating service is enabled.
std::queue< prefetch_request > request_queue_
Queue of pending prefetch requests.
auto_prefetch_service(storage::index_database &database, const prefetch_service_config &config={})
Construct auto prefetch service.
std::atomic< bool > cycle_in_progress_
Flag indicating a cycle is in progress.
void set_error_callback(prefetch_service_config::error_callback callback)
Set the error callback.
void trigger_cycle()
Trigger next cycle immediately.
void update_stats(const prefetch_result &result)
Update cumulative statistics.
auto execute_cycle() -> prefetch_result
Execute a single prefetch cycle.
std::thread worker_thread_
Background worker thread.
prefetch_result cumulative_stats_
Cumulative statistics.
auto is_enabled() const noexcept -> bool
Check if the service is enabled/running.
std::chrono::steady_clock::time_point next_cycle_time_
Time of next scheduled cycle.
auto prefetch_priors(const std::string &patient_id, std::chrono::days lookback=std::chrono::days{365}) -> prefetch_result
Manually prefetch prior studies for a patient.
std::atomic< std::size_t > cycles_count_
Number of completed cycles.
prefetch_service_config config_
Service configuration.
auto query_prior_studies(const remote_pacs_config &pacs_config, const std::string &patient_id, std::chrono::days lookback) -> std::vector< prior_study_info >
Query remote PACS for prior studies.
~auto_prefetch_service()
Destructor - ensures graceful shutdown.
auto process_request(const prefetch_request &request) -> prefetch_result
Process a single prefetch request.
auto filter_studies(const std::vector< prior_study_info > &studies, const prefetch_request &request) -> std::vector< prior_study_info >
Filter prior studies based on criteria.
std::mutex queue_mutex_
Mutex for request queue.
auto time_until_next_cycle() const -> std::optional< std::chrono::seconds >
Get the time until the next scheduled prefetch cycle.
std::atomic< bool > stop_requested_
Flag to signal shutdown.
auto run_prefetch_cycle() -> prefetch_result
Run a prefetch cycle manually.
auto get_prefetch_interval() const noexcept -> std::chrono::seconds
Get the current prefetch interval.
void set_prefetch_interval(std::chrono::seconds interval)
Update the prefetch interval.
void stop(bool wait_for_completion=true)
Stop the prefetch service (alias for disable)
void trigger_for_worklist(const std::vector< storage::worklist_item > &worklist_items)
Trigger prefetch for worklist items.
std::mutex mutex_
Mutex for thread synchronization.
void start()
Start the prefetch service (alias for enable)
void queue_request(const prefetch_request &request)
Add request to queue (deduplicated)
auto study_exists_locally(const std::string &study_uid) -> bool
Check if study already exists locally.
auto pending_requests() const noexcept -> std::size_t
Get the number of pending prefetch requests.
auto dequeue_request() -> std::optional< prefetch_request >
Get next request from queue.
void set_cycle_complete_callback(prefetch_service_config::cycle_complete_callback callback)
Set the cycle complete callback.
auto prefetch_study(const remote_pacs_config &pacs_config, const prior_study_info &study) -> bool
Prefetch a single study via C-MOVE.
auto get_prefetch_criteria() const noexcept -> const prefetch_criteria &
Get the current prefetch criteria.
Compile-time constants for commonly used DICOM tags.
Adapter for integrating common_system's IExecutor interface.
PACS index database for metadata storage and retrieval.
Adapter for DICOM audit logging using logger_system.
Adapter for PACS performance metrics and distributed tracing.
constexpr dicom_tag number_of_study_related_series
Number of Study Related Series.
constexpr dicom_tag modalities_in_study
Modalities in Study.
constexpr dicom_tag study_description
Study Description.
constexpr dicom_tag number_of_study_related_instances
Number of Study Related Instances.
constexpr dicom_tag study_instance_uid
Study Instance UID.
constexpr dicom_tag patient_name
Patient's Name.
constexpr dicom_tag study_date
Study Date.
constexpr std::string_view study_root_find_sop_class_uid
Study Root Query/Retrieve Information Model - FIND.
Definition query_scp.h:42
@ c_move
Request SCP to send to third party (requires move destination)
constexpr std::string_view study_root_move_sop_class_uid
Study Root Query/Retrieve Information Model - MOVE.
@ study
Study level - query study information.
@ study_root
Study Root Query/Retrieve Information Model.
DICOM Query SCU service (C-FIND sender)
DICOM Retrieve SCU service (C-MOVE/C-GET sender)
Configuration for SCU association request.
std::string called_ae_title
Remote AE Title (16 chars max)
std::string calling_ae_title
Our AE Title (16 chars max)
std::vector< proposed_presentation_context > proposed_contexts
Configuration for Query SCU service.
Definition query_scu.h:168
std::chrono::milliseconds timeout
Timeout for receiving query responses (milliseconds)
Definition query_scu.h:176
query_model model
Query information model (Patient Root or Study Root)
Definition query_scu.h:170
query_level level
Query level (Patient, Study, Series, or Image)
Definition query_scu.h:173
Configuration for Retrieve SCU service.
std::chrono::milliseconds timeout
Timeout for receiving responses (milliseconds)
query_level level
Query level (Study, Series, or Image)
query_model model
Query information model (Patient Root or Study Root)
retrieve_mode mode
Retrieve mode (C-MOVE or C-GET)
std::string move_destination
Move destination AE title (required for C-MOVE mode)
Query keys for STUDY level queries.
Definition query_scu.h:133
std::string patient_id
Patient ID (0010,0020) - for filtering.
Definition query_scu.h:134
std::string study_date
Study Date (0008,0020) - YYYYMMDD or range.
Definition query_scu.h:136
Prefetch request for a single patient.
std::string scheduled_study_uid
Study Instance UID of scheduled study (to avoid prefetching)
std::string scheduled_modality
Scheduled modality (for preference matching)
std::chrono::system_clock::time_point request_time
Request timestamp.
std::size_t studies_prefetched
Number of studies prefetched successfully.
std::size_t studies_already_present
Number of studies already present (skipped)
std::chrono::system_clock::time_point timestamp
Time when this result was recorded.
std::size_t series_prefetched
Number of series prefetched successfully.
std::size_t instances_prefetched
Number of instances (images) prefetched.
std::chrono::milliseconds duration
Duration of the prefetch operation.
std::size_t studies_failed
Number of studies that failed to prefetch.
std::size_t patients_processed
Number of patients processed.
Configuration for the auto prefetch service.
prefetch_criteria criteria
Selection criteria for prior studies.
bool auto_start
Whether to start automatically on construction.
std::function< void(const prefetch_result &result)> cycle_complete_callback
Callback for prefetch cycle completion.
std::size_t max_concurrent_prefetches
Maximum concurrent prefetch operations.
std::function< void(const std::string &patient_id, const std::string &study_uid, const std::string &error)> error_callback
Callback for prefetch errors.
bool enabled
Enable/disable the prefetch service.
std::chrono::seconds prefetch_interval
Interval between prefetch cycles (default: 5 minutes)
Remote PACS connection configuration.