# HG changeset patch # User Sebastien Jodogne # Date 1583858629 -3600 # Node ID 0540b54324f19180c6e9c3fa24f368d21ac9b303 # Parent 77183afbf55e79d123a29268572f87f6074eb016 StorageCommitmentReports diff -r 77183afbf55e -r 0540b54324f1 CMakeLists.txt --- a/CMakeLists.txt Tue Mar 10 13:59:14 2020 +0100 +++ b/CMakeLists.txt Tue Mar 10 17:43:49 2020 +0100 @@ -106,6 +106,7 @@ OrthancServer/ServerJobs/StorageCommitmentScpJob.cpp OrthancServer/ServerToolbox.cpp OrthancServer/SliceOrdering.cpp + OrthancServer/StorageCommitmentReports.cpp ) diff -r 77183afbf55e -r 0540b54324f1 Core/DicomNetworking/IStorageCommitmentRequestHandler.h --- a/Core/DicomNetworking/IStorageCommitmentRequestHandler.h Tue Mar 10 13:59:14 2020 +0100 +++ b/Core/DicomNetworking/IStorageCommitmentRequestHandler.h Tue Mar 10 17:43:49 2020 +0100 @@ -58,6 +58,7 @@ const std::vector& successSopInstanceUids, const std::vector& failedSopClassUids, const std::vector& failedSopInstanceUids, + const std::vector& failureReasons, const std::string& remoteIp, const std::string& remoteAet, const std::string& calledAet) = 0; diff -r 77183afbf55e -r 0540b54324f1 Core/DicomNetworking/Internals/CommandDispatcher.cpp --- a/Core/DicomNetworking/Internals/CommandDispatcher.cpp Tue Mar 10 13:59:14 2020 +0100 +++ b/Core/DicomNetworking/Internals/CommandDispatcher.cpp Tue Mar 10 17:43:49 2020 +0100 @@ -928,15 +928,22 @@ } - static void ReadSopSequence(std::vector& sopClassUids, - std::vector& sopInstanceUids, - DcmDataset& dataset, - const DcmTagKey& tag, - bool mandatory) + static void ReadSopSequence( + std::vector& sopClassUids, + std::vector& sopInstanceUids, + std::vector* failureReasons, // Can be NULL + DcmDataset& dataset, + const DcmTagKey& tag, + bool mandatory) { sopClassUids.clear(); sopInstanceUids.clear(); + if (failureReasons) + { + failureReasons->clear(); + } + DcmSequenceOfItems* sequence = NULL; if (!dataset.findAndGetSequence(tag, sequence).good() || sequence == NULL) @@ -957,6 +964,11 @@ sopClassUids.reserve(sequence->card()); sopInstanceUids.reserve(sequence->card()); + if (failureReasons) + { + failureReasons->reserve(sequence->card()); + } + for (unsigned long i = 0; i < sequence->card(); i++) { const char* a = NULL; @@ -968,11 +980,24 @@ { throw OrthancException(ErrorCode_NetworkProtocol, "Missing Referenced SOP Class/Instance UID " - "in storage commitment request"); + "in storage commitment dataset"); } sopClassUids.push_back(a); sopInstanceUids.push_back(b); + + if (failureReasons != NULL) + { + Uint16 reason; + if (!sequence->getItem(i)->findAndGetUint16(DCM_FailureReason, reason).good()) + { + throw OrthancException(ErrorCode_NetworkProtocol, + "Missing Failure Reason (0008,1197) " + "in storage commitment dataset"); + } + + failureReasons->push_back(static_cast(reason)); + } } } @@ -1034,7 +1059,7 @@ std::string transactionUid = ReadString(*dataset, DCM_TransactionUID); std::vector sopClassUid, sopInstanceUid; - ReadSopSequence(sopClassUid, sopInstanceUid, + ReadSopSequence(sopClassUid, sopInstanceUid, NULL, *dataset, DCM_ReferencedSOPSequence, true /* mandatory */); LOG(INFO) << "Incoming storage commitment request, with transaction UID: " << transactionUid; @@ -1157,15 +1182,16 @@ std::string transactionUid = ReadString(*dataset, DCM_TransactionUID); std::vector successSopClassUid, successSopInstanceUid; - ReadSopSequence(successSopClassUid, successSopInstanceUid, + ReadSopSequence(successSopClassUid, successSopInstanceUid, NULL, *dataset, DCM_ReferencedSOPSequence, (report.EventTypeID == 1) /* mandatory in the case of success */); std::vector failedSopClassUid, failedSopInstanceUid; + std::vector failureReasons; if (report.EventTypeID == 2 /* failures exist */) { - ReadSopSequence(failedSopClassUid, failedSopInstanceUid, + ReadSopSequence(failedSopClassUid, failedSopInstanceUid, &failureReasons, *dataset, DCM_FailedSOPSequence, true); } @@ -1200,7 +1226,7 @@ ConstructStorageCommitmentRequestHandler()); handler->HandleReport(transactionUid, successSopClassUid, successSopInstanceUid, - failedSopClassUid, failedSopInstanceUid, + failedSopClassUid, failedSopInstanceUid, failureReasons, remoteIp_, remoteAet_, calledAet_); dimseStatus = 0; // Success diff -r 77183afbf55e -r 0540b54324f1 NEWS --- a/NEWS Tue Mar 10 13:59:14 2020 +0100 +++ b/NEWS Tue Mar 10 17:43:49 2020 +0100 @@ -40,6 +40,7 @@ Maintenance ----------- +* New configuration options: "DefaultPrivateCreator" and "StorageCommitmentReportsSize" * Support of MPEG4 transfer syntaxes in C-Store SCP * C-FIND SCU at Instance level now sets the 0008,0052 tag to IMAGE per default (was INSTANCE). Therefore, the "ClearCanvas" and "Dcm4Chee" modality manufacturer have now been deprecated. diff -r 77183afbf55e -r 0540b54324f1 OrthancServer/ServerContext.cpp --- a/OrthancServer/ServerContext.cpp Tue Mar 10 13:59:14 2020 +0100 +++ b/OrthancServer/ServerContext.cpp Tue Mar 10 17:43:49 2020 +0100 @@ -49,6 +49,7 @@ #include "Search/DatabaseLookup.h" #include "ServerJobs/OrthancJobUnserializer.h" #include "ServerToolbox.h" +#include "StorageCommitmentReports.h" #include #include @@ -259,6 +260,9 @@ findStorageAccessMode_ = StringToFindStorageAccessMode(lock.GetConfiguration().GetStringParameter("StorageAccessOnFind", "Always")); limitFindInstances_ = lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindInstances", 0); limitFindResults_ = lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindResults", 0); + + // New configuration option in Orthanc 1.6.0 + storageCommitmentReports_.reset(new StorageCommitmentReports(lock.GetConfiguration().GetUnsignedIntegerParameter("StorageCommitmentReportsSize", 100))); } jobsEngine_.SetThreadSleep(unitTesting ? 20 : 200); diff -r 77183afbf55e -r 0540b54324f1 OrthancServer/ServerContext.h --- a/OrthancServer/ServerContext.h Tue Mar 10 13:59:14 2020 +0100 +++ b/OrthancServer/ServerContext.h Tue Mar 10 17:43:49 2020 +0100 @@ -54,6 +54,7 @@ class SetOfInstancesJob; class SharedArchive; class SharedMessageQueue; + class StorageCommitmentReports; /** @@ -221,6 +222,8 @@ bool isHttpServerSecure_; bool isExecuteLuaEnabled_; + std::unique_ptr storageCommitmentReports_; + public: class DicomCacheLocker : public boost::noncopyable { @@ -430,5 +433,10 @@ const std::vector& sopInstanceUids, const std::string& remoteAet, const std::string& calledAet) ORTHANC_OVERRIDE; + + StorageCommitmentReports& GetStorageCommitmentReports() + { + return *storageCommitmentReports_; + } }; } diff -r 77183afbf55e -r 0540b54324f1 OrthancServer/StorageCommitmentReports.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/StorageCommitmentReports.cpp Tue Mar 10 17:43:49 2020 +0100 @@ -0,0 +1,185 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "PrecompiledHeadersServer.h" +#include "StorageCommitmentReports.h" + +#include "../Core/OrthancException.h" + +namespace Orthanc +{ + void StorageCommitmentReports::Report::MarkAsComplete() + { + if (isComplete_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + isComplete_ = true; + } + } + + void StorageCommitmentReports::Report::AddSuccess(const std::string& sopClassUid, + const std::string& sopInstanceUid) + { + if (isComplete_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + Success success; + success.sopClassUid_ = sopClassUid; + success.sopInstanceUid_ = sopInstanceUid; + successes_.push_back(success); + } + } + + void StorageCommitmentReports::Report::AddFailure(const std::string& sopClassUid, + const std::string& sopInstanceUid, + StorageCommitmentFailureReason reason) + { + if (isComplete_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + Failure failure; + failure.sopClassUid_ = sopClassUid; + failure.sopInstanceUid_ = sopInstanceUid; + failure.reason_ = reason; + failures_.push_back(failure); + } + } + + + StorageCommitmentReports::Report::Status StorageCommitmentReports::Report::GetStatus() const + { + if (!isComplete_) + { + return Status_Pending; + } + else if (failures_.empty()) + { + return Status_Success; + } + else + { + return Status_Failure; + } + } + + + StorageCommitmentReports::~StorageCommitmentReports() + { + while (!content_.IsEmpty()) + { + Report* report = NULL; + content_.RemoveOldest(report); + + assert(report != NULL); + delete report; + } + } + + + void StorageCommitmentReports::Store(const std::string& transactionUid, + Report* report) + { + std::unique_ptr protection(report); + + boost::mutex::scoped_lock lock(mutex_); + + { + Report* previous = NULL; + if (content_.Contains(transactionUid, previous)) + { + assert(previous != NULL); + delete previous; + + content_.Invalidate(transactionUid); + } + } + + assert(maxSize_ == 0 || + content_.GetSize() <= maxSize_); + + if (maxSize_ != 0 && + content_.GetSize() == maxSize_) + { + assert(!content_.IsEmpty()); + + Report* oldest = NULL; + content_.RemoveOldest(oldest); + + assert(oldest != NULL); + delete oldest; + } + + assert(maxSize_ == 0 || + content_.GetSize() < maxSize_); + + content_.Add(transactionUid, protection.release()); + } + + + StorageCommitmentReports::Accessor::Accessor(StorageCommitmentReports& that, + const std::string& transactionUid) : + lock_(that.mutex_), + transactionUid_(transactionUid) + { + if (that.content_.Contains(transactionUid, report_)) + { + that.content_.MakeMostRecent(transactionUid); + } + else + { + report_ = NULL; + } + } + + const StorageCommitmentReports::Report& + StorageCommitmentReports::Accessor::GetReport() const + { + if (report_ == NULL) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + return *report_; + } + } +} diff -r 77183afbf55e -r 0540b54324f1 OrthancServer/StorageCommitmentReports.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/StorageCommitmentReports.h Tue Mar 10 17:43:49 2020 +0100 @@ -0,0 +1,143 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../Core/Cache/LeastRecentlyUsedIndex.h" + +namespace Orthanc +{ + class StorageCommitmentReports + { + public: + class Report : public boost::noncopyable + { + public: + enum Status + { + Status_Success, + Status_Failure, + Status_Pending + }; + + private: + struct Success + { + std::string sopClassUid_; + std::string sopInstanceUid_; + }; + + struct Failure + { + std::string sopClassUid_; + std::string sopInstanceUid_; + StorageCommitmentFailureReason reason_; + }; + + bool isComplete_; + std::list successes_; + std::list failures_; + std::string remoteAet_; + + public: + Report(const std::string& remoteAet) : + isComplete_(false), + remoteAet_(remoteAet) + { + } + + const std::string& GetRemoteAet() const + { + return remoteAet_; + } + + void MarkAsComplete(); + + void AddSuccess(const std::string& sopClassUid, + const std::string& sopInstanceUid); + + void AddFailure(const std::string& sopClassUid, + const std::string& sopInstanceUid, + StorageCommitmentFailureReason reason); + + Status GetStatus() const; + }; + + private: + typedef LeastRecentlyUsedIndex Content; + + boost::mutex mutex_; + Content content_; + size_t maxSize_; + + public: + StorageCommitmentReports(size_t maxSize) : + maxSize_(maxSize) + { + } + + ~StorageCommitmentReports(); + + size_t GetMaxSize() const + { + return maxSize_; + } + + void Store(const std::string& transactionUid, + Report* report); // Takes ownership + + class Accessor : public boost::noncopyable + { + private: + boost::mutex::scoped_lock lock_; + std::string transactionUid_; + Report *report_; + + public: + Accessor(StorageCommitmentReports& that, + const std::string& transactionUid); + + const std::string& GetTransactionUid() const + { + return transactionUid_; + } + + bool IsValid() const + { + return report_ != NULL; + } + + const Report& GetReport() const; + }; + }; +} diff -r 77183afbf55e -r 0540b54324f1 OrthancServer/main.cpp --- a/OrthancServer/main.cpp Tue Mar 10 13:59:14 2020 +0100 +++ b/OrthancServer/main.cpp Tue Mar 10 17:43:49 2020 +0100 @@ -52,6 +52,7 @@ #include "ServerContext.h" #include "ServerJobs/StorageCommitmentScpJob.h" #include "ServerToolbox.h" +#include "StorageCommitmentReports.h" using namespace Orthanc; @@ -133,12 +134,34 @@ const std::vector& successSopInstanceUids, const std::vector& failedSopClassUids, const std::vector& failedSopInstanceUids, + const std::vector& failureReasons, const std::string& remoteIp, const std::string& remoteAet, const std::string& calledAet) { - // TODO - printf("HANDLE REPORT\n"); + if (successSopClassUids.size() != successSopInstanceUids.size() || + failedSopClassUids.size() != failedSopInstanceUids.size() || + failedSopClassUids.size() != failureReasons.size()) + { + throw OrthancException(ErrorCode_InternalError); + } + + std::unique_ptr report( + new StorageCommitmentReports::Report(remoteAet)); + + for (size_t i = 0; i < successSopClassUids.size(); i++) + { + report->AddSuccess(successSopClassUids[i], successSopInstanceUids[i]); + } + + for (size_t i = 0; i < failedSopClassUids.size(); i++) + { + report->AddFailure(failedSopClassUids[i], failedSopInstanceUids[i], failureReasons[i]); + } + + report->MarkAsComplete(); + + context_.GetStorageCommitmentReports().Store(transactionUid, report.release()); } }; diff -r 77183afbf55e -r 0540b54324f1 Resources/Configuration.json --- a/Resources/Configuration.json Tue Mar 10 13:59:14 2020 +0100 +++ b/Resources/Configuration.json Tue Mar 10 17:43:49 2020 +0100 @@ -528,5 +528,9 @@ // Set the default private creator that is used by Orthanc when it // looks for a private tag in its dictionary (cf. "Dictionary" // option), or when it creates/modifies a DICOM file (new in Orthanc 1.6.0). - "DefaultPrivateCreator" : "" + "DefaultPrivateCreator" : "", + + // Maximum number of storage commitment reports (i.e. received from + // remote modalities) to be kept in memory (new in Orthanc 1.6.0). + "StorageCommitmentReportsSize" : 100 } diff -r 77183afbf55e -r 0540b54324f1 UnitTestsSources/MemoryCacheTests.cpp --- a/UnitTestsSources/MemoryCacheTests.cpp Tue Mar 10 13:59:14 2020 +0100 +++ b/UnitTestsSources/MemoryCacheTests.cpp Tue Mar 10 17:43:49 2020 +0100 @@ -44,6 +44,7 @@ #include "../Core/Cache/SharedArchive.h" #include "../Core/IDynamicObject.h" #include "../Core/Logging.h" +#include "../OrthancServer/StorageCommitmentReports.h" TEST(LRU, Basic) @@ -366,3 +367,94 @@ ASSERT_FALSE(c.Fetch(v, "hello")); ASSERT_TRUE(c.Fetch(v, "hello2")); ASSERT_EQ("b", v); } + + +TEST(StorageCommitmentReports, Basic) +{ + Orthanc::StorageCommitmentReports reports(2); + ASSERT_EQ(2u, reports.GetMaxSize()); + + { + Orthanc::StorageCommitmentReports::Accessor accessor(reports, "nope"); + ASSERT_EQ("nope", accessor.GetTransactionUid()); + ASSERT_FALSE(accessor.IsValid()); + ASSERT_THROW(accessor.GetReport(), Orthanc::OrthancException); + } + + reports.Store("a", new Orthanc::StorageCommitmentReports::Report("aet_a")); + reports.Store("b", new Orthanc::StorageCommitmentReports::Report("aet_b")); + reports.Store("c", new Orthanc::StorageCommitmentReports::Report("aet_c")); + + { + Orthanc::StorageCommitmentReports::Accessor accessor(reports, "a"); + ASSERT_FALSE(accessor.IsValid()); + } + + { + Orthanc::StorageCommitmentReports::Accessor accessor(reports, "b"); + ASSERT_TRUE(accessor.IsValid()); + ASSERT_EQ("aet_b", accessor.GetReport().GetRemoteAet()); + ASSERT_EQ(Orthanc::StorageCommitmentReports::Report::Status_Pending, + accessor.GetReport().GetStatus()); + } + + { + Orthanc::StorageCommitmentReports::Accessor accessor(reports, "c"); + ASSERT_EQ("aet_c", accessor.GetReport().GetRemoteAet()); + ASSERT_TRUE(accessor.IsValid()); + } + + { + std::unique_ptr report + (new Orthanc::StorageCommitmentReports::Report("aet")); + report->AddSuccess("class1", "instance1"); + report->AddFailure("class2", "instance2", + Orthanc::StorageCommitmentFailureReason_ReferencedSOPClassNotSupported); + report->MarkAsComplete(); + reports.Store("a", report.release()); + } + + { + Orthanc::StorageCommitmentReports::Accessor accessor(reports, "a"); + ASSERT_TRUE(accessor.IsValid()); + ASSERT_EQ("aet", accessor.GetReport().GetRemoteAet()); + ASSERT_EQ(Orthanc::StorageCommitmentReports::Report::Status_Failure, + accessor.GetReport().GetStatus()); + } + + { + Orthanc::StorageCommitmentReports::Accessor accessor(reports, "b"); + ASSERT_FALSE(accessor.IsValid()); + } + + { + Orthanc::StorageCommitmentReports::Accessor accessor(reports, "c"); + ASSERT_TRUE(accessor.IsValid()); + } + + { + std::unique_ptr report + (new Orthanc::StorageCommitmentReports::Report("aet")); + report->AddSuccess("class1", "instance1"); + report->MarkAsComplete(); + reports.Store("a", report.release()); + } + + { + Orthanc::StorageCommitmentReports::Accessor accessor(reports, "a"); + ASSERT_TRUE(accessor.IsValid()); + ASSERT_EQ("aet", accessor.GetReport().GetRemoteAet()); + ASSERT_EQ(Orthanc::StorageCommitmentReports::Report::Status_Success, + accessor.GetReport().GetStatus()); + } + + { + Orthanc::StorageCommitmentReports::Accessor accessor(reports, "b"); + ASSERT_FALSE(accessor.IsValid()); + } + + { + Orthanc::StorageCommitmentReports::Accessor accessor(reports, "c"); + ASSERT_TRUE(accessor.IsValid()); + } +}