Mercurial > hg > orthanc
changeset 6668:5ce108190752
merged machine-spirits -> default
| author | Alain Mazy <am@orthanc.team> |
|---|---|
| date | Fri, 03 Apr 2026 11:46:14 +0200 |
| parents | 38f8a3b3d04e (diff) eeeca0b5fab6 (current diff) |
| children | 906d50d05430 |
| files | NEWS OrthancServer/Resources/Configuration.json OrthancServer/Sources/ServerContext.cpp |
| diffstat | 28 files changed, 801 insertions(+), 401 deletions(-) [+] |
line wrap: on
line diff
--- a/NEWS Wed Mar 25 08:26:53 2026 +0100 +++ b/NEWS Fri Apr 03 11:46:14 2026 +0200 @@ -4,9 +4,23 @@ General ------- +* New configuration "LoaderThreads" that is a general configuration for all actions + that require loading files, possibly in parallel: + - downloading zip archive/media, + - performing a C-Store to a remote modality + - performing a peer transfer to a remote Orthanc. + - responding to a C-Get request from a remote modality. + Note: the old "ZipLoaderThreads" configuration is still available for backward + compatibility reasons. +* When performing a C-Store or a peer transfer, the instances are now + grouped per series and ordered by their InstanceNumber instead of using + a random order in previous Orthanc versions. +* New experimental configuration "PatientLevelEnabled". * New configuration option "MaximumRequestBodySizeMB" to limit the body size in HTTP requests * New configuration option "MaximumFileSizeInArchiveMB" to limit the file size in ZIP/gzip archives -* New experimental configuration "PatientLevelEnabled" (TODO: work in progree) + +TODO before release: re-enable test_content_disposition + REST API -------- @@ -30,6 +44,8 @@ * New "Content.Resources" field in "DicomModalityStore", "OrthancPeerStore" and "ResourceModification" jobs that contains JSON objects with "ID" and "Type" of each resource. The "Content.ParentResources" field that only contains the IDs is preserved for backward compatibility. +* In /system: renamed "DefaultRetrieveMethod" into "DicomDefaultRetrieveMethod" to match the name + in the configuration file. The configuration returned was actually not reflecting the configuration. Lua --- @@ -98,7 +114,7 @@ "ErrorPayload" in the HTTP response with details about the failed operation. This is currently implemented for C-Store, C-Move, and C-Get operations. * C-Move jobs, C-Get jobs, and HTTP responses now include a new "Details" field with the - "DimseErrorStatus" of each operation and the list of "RetrievedInstancesIds". + "DimseErrorStatus" of each operation and the list of "ReceivedInstancesIds". Plugin SDK ----------
--- a/OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.cpp Wed Mar 25 08:26:53 2026 +0100 +++ b/OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.cpp Fri Apr 03 11:46:14 2026 +0200 @@ -274,4 +274,15 @@ hasTrailingStep_ = false; } } + + void SetOfInstancesJob::GetInstancesIds(std::vector<std::string>& target) const + { + target.clear(); + target.reserve(GetCommandsCount()); + + for (size_t i = 0; i < GetCommandsCount(); ++i) + { + target.push_back(dynamic_cast<const InstanceCommand&>(GetCommand(i)).GetInstance()); + } + } }
--- a/OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.h Wed Mar 25 08:26:53 2026 +0100 +++ b/OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.h Fri Apr 03 11:46:14 2026 +0200 @@ -51,6 +51,7 @@ // Hiding this method, use AddInstance() instead using SetOfCommandsJob::AddCommand; + void GetInstancesIds(std::vector<std::string>& target) const; public: SetOfInstancesJob();
--- a/OrthancServer/CMakeLists.txt Wed Mar 25 08:26:53 2026 +0100 +++ b/OrthancServer/CMakeLists.txt Fri Apr 03 11:46:14 2026 +0200 @@ -160,7 +160,9 @@ ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/OrthancPeerStoreJob.cpp ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/ResourceModificationJob.cpp ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/SplitStudyJob.cpp + ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/StoreJob.cpp ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/StorageCommitmentScpJob.cpp + ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/ThreadedInstancesLoader.cpp ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/ThreadedSetOfInstancesJob.cpp ${CMAKE_SOURCE_DIR}/Sources/ServerToolbox.cpp ${CMAKE_SOURCE_DIR}/Sources/SimpleInstanceOrdering.cpp
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp Wed Mar 25 08:26:53 2026 +0100 +++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp Fri Apr 03 11:46:14 2026 +0200 @@ -4943,6 +4943,11 @@ CheckKeyValueStoresSupport(lock.GetContext()); + if (parameters.valueSize == 0 || parameters.value == NULL) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "You must provide a non-null and non-empty value when adding a value in a Key-Value store."); + } + lock.GetContext().GetIndex().StoreKeyValue(parameters.storeId, parameters.key, parameters.value, parameters.valueSize); } @@ -5006,6 +5011,11 @@ CheckQueuesSupport(lock.GetContext()); + if (parameters.valueSize == 0 || parameters.value == NULL) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "You must provide a non-null and non-empty value when enqueing a value in a Queue."); + } + lock.GetContext().GetIndex().EnqueueValue(parameters.queueId, parameters.value, parameters.valueSize); }
--- a/OrthancServer/Resources/Configuration.json Wed Mar 25 08:26:53 2026 +0100 +++ b/OrthancServer/Resources/Configuration.json Fri Apr 03 11:46:14 2026 +0200 @@ -1009,12 +1009,19 @@ // as soon as one DICOM file gets compressed (new in Orthanc 1.9.4) "SynchronousZipStream" : true, - // Default number of loader threads when generating Zip archive/media. - // A value of 0 means reading and writing are performed in sequence - // (default behaviour). A value > 1 is meaningful only if the storage + // Default number of loader threads when reading files from disk when + // executing some tasks: + // - generating archive/media, + // - executing a C-Store + // - transmitting resources through Orthanc peering. + // A value of 0 and 1 are equivalent: a single thread is used. + // A value > 1 is meaningful only if the storage // is a distributed network storage (e.g object storage plugin). - // (new experimental feature in Orthanc 1.10.0) - "ZipLoaderThreads": 0, + // (new in Orthanc 1.12.11) + // Note, from 1.10.0 to 1.12.10, a "ZipLoaderThreads" configuration + // was available only for the zip archive/media. It is still valid + // for backward compatibility. + "LoaderThreads": 1, // Extra Main Dicom tags that are stored in DB together with all default // Main Dicom tags that are already stored.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Resources/ImplementationNotes/JobsEngineClasses.txt Fri Apr 03 11:46:14 2026 +0200 @@ -0,0 +1,27 @@ +class IJob : + virtual void Start() = 0; + virtual JobStepResult Step(const std::string& jobId) = 0; + virtual bool NeedsProgressUpdateBetweenSteps() const // only for jobs whose progress is updated by outside events (like C-Move and C-Get) + virtual float GetProgress() const = 0; + ... + +class SetOfCommandsJob : IJob + class ICommand + virtual bool Execute(const std::string& jobId) = 0; + virtual void Serialize(Json::Value& target) const = 0; + class ICommandUnserializer + virtual ICommand* Unserialize(const Json::Value& source) const = 0; + + virtual void Start() ORTHANC_OVERRIDE; + virtual float GetProgress() ORTHANC_OVERRIDE { returns position_/commands_.size()} + void SetPermissive(bool permissive); // if a command fails, the complete job does not fails + virtual JobStepResult Step(const std::string& jobId) = commands_[position_]->Execute() + +class SetOfInstancesJob : SetOfCommandsJob + virtual bool HandleInstance(const std::string& instance) = 0; + virtual bool HandleTrailingStep() = 0; + + +class StoreJob : SetOfInstancesJob (contains a ThreadedInstancesLoader) + +Class ModalityStoreJob : StoreJob \ No newline at end of file
--- a/OrthancServer/Sources/OrthancConfiguration.cpp Wed Mar 25 08:26:53 2026 +0100 +++ b/OrthancServer/Sources/OrthancConfiguration.cpp Fri Apr 03 11:46:14 2026 +0200 @@ -47,6 +47,8 @@ static const char* const WARNINGS = "Warnings"; static const char* const JOBS_ENGINE_THREADS_COUNT = "JobsEngineThreadsCount"; static const char* const DICOM_LOSSY_TRANSCODING_QUALITY = "DicomLossyTranscodingQuality"; +static const char* const CONFIG_LOADER_THREADS = "LoaderThreads"; +static const char* const CONFIG_ZIP_LOADER_THREADS = "ZipLoaderThreads"; // for backward compatibility only namespace Orthanc { @@ -1347,4 +1349,18 @@ throw OrthancException(ErrorCode_BadFileFormat); } } + + unsigned int OrthancConfiguration::GetLoaderThreads() const + { + // from 1.10.0 to 1.12.10, only CONFIG_ZIP_LOADER_THREADS was available -> read from it if CONFIG_LOADER_THREADS is not specified. + unsigned int loaderThreads = GetUnsignedIntegerParameter(CONFIG_LOADER_THREADS, GetUnsignedIntegerParameter(CONFIG_ZIP_LOADER_THREADS, 1)); + + if (loaderThreads <= 1) + { + return 1; // 0 is not a valid internal value anymore + } + + return loaderThreads; + } + }
--- a/OrthancServer/Sources/OrthancConfiguration.h Wed Mar 25 08:26:53 2026 +0100 +++ b/OrthancServer/Sources/OrthancConfiguration.h Fri Apr 03 11:46:14 2026 +0200 @@ -248,6 +248,7 @@ void RemovePeer(const std::string& symbolicName); + unsigned int GetLoaderThreads() const; void Format(std::string& result) const;
--- a/OrthancServer/Sources/OrthancGetRequestHandler.cpp Wed Mar 25 08:26:53 2026 +0100 +++ b/OrthancServer/Sources/OrthancGetRequestHandler.cpp Fri Apr 03 11:46:14 2026 +0200 @@ -30,6 +30,7 @@ #include "OrthancConfiguration.h" #include "ServerContext.h" #include "ServerJobs/DicomModalityStoreJob.h" +#include "ServerJobs/ThreadedInstancesLoader.h" #include <dcmtk/dcmdata/dcdeftag.h> #include <dcmtk/dcmdata/dcfilefo.h> @@ -59,15 +60,20 @@ bool OrthancGetRequestHandler::DoNext(T_ASC_Association* assoc) { - if (position_ >= instances_.size()) + if (position_ >= instancesIds_.size()) { throw OrthancException(ErrorCode_ParameterOutOfRange); } - const std::string& id = instances_[position_++]; + if (instancesLoader_.get() == NULL) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + const std::string& id = instancesIds_[position_++]; std::string dicom; - context_.ReadDicom(dicom, id); + instancesLoader_->GetDicom(dicom, id); if (dicom.empty()) { @@ -544,7 +550,8 @@ localAet_ = context_.GetDefaultLocalApplicationEntityTitle(); position_ = 0; originatorAet_ = originatorAet; - + unsigned int loaderThreads = 1; + { OrthancConfiguration::ReaderLock lock; @@ -567,22 +574,25 @@ throw OrthancException(ErrorCode_InexistentItem, "C-GET: Rejecting SCU request from unknown modality with AET: " + originatorAet); } + + loaderThreads = lock.GetConfiguration().GetLoaderThreads(); } + instancesLoader_.reset(new ThreadedInstancesLoader(context_, loaderThreads, false, DicomTransferSyntax_BigEndianExplicit /* dummy unused value */, 0, "CGET")); + std::vector<FileInfo> filesInfo; + for (std::list<std::string>::const_iterator - resource = publicIds.begin(); resource != publicIds.end(); ++resource) + resourceId = publicIds.begin(); resourceId != publicIds.end(); ++resourceId) { - CLOG(INFO, DICOM) << "C-GET: Sending resource " << *resource + CLOG(INFO, DICOM) << "C-GET: Sending resource " << *resourceId << " to modality \"" << originatorAet << "\""; - - std::list<std::string> tmp; - context_.GetIndex().GetChildInstances(tmp, *resource); - - instances_.reserve(tmp.size()); - for (std::list<std::string>::iterator it = tmp.begin(); it != tmp.end(); ++it) - { - instances_.push_back(*it); - } + + context_.GetOrderedChildInstances(instancesIds_, filesInfo, *resourceId, level); + } + + for (size_t i = 0; i < instancesIds_.size(); ++i) + { + instancesLoader_->PrepareDicom(instancesIds_[i], filesInfo[i]); } failedUIDs_.clear();
--- a/OrthancServer/Sources/OrthancGetRequestHandler.h Wed Mar 25 08:26:53 2026 +0100 +++ b/OrthancServer/Sources/OrthancGetRequestHandler.h Fri Apr 03 11:46:14 2026 +0200 @@ -35,16 +35,18 @@ namespace Orthanc { class ServerContext; + class ThreadedInstancesLoader; class OrthancGetRequestHandler : public IGetRequestHandler { private: ServerContext& context_; std::string localAet_; - std::vector<std::string> instances_; + std::vector<std::string> instancesIds_; size_t position_; std::string originatorAet_; - + std::unique_ptr<ThreadedInstancesLoader> instancesLoader_; + unsigned int completedCount_; unsigned int warningCount_; unsigned int failedCount_; @@ -78,7 +80,7 @@ virtual unsigned int GetSubOperationCount() const ORTHANC_OVERRIDE { - return static_cast<unsigned int>(instances_.size()); + return static_cast<unsigned int>(instancesIds_.size()); } virtual unsigned int GetCompletedCount() const ORTHANC_OVERRIDE
--- a/OrthancServer/Sources/OrthancMoveRequestHandler.cpp Wed Mar 25 08:26:53 2026 +0100 +++ b/OrthancServer/Sources/OrthancMoveRequestHandler.cpp Fri Apr 03 11:46:14 2026 +0200 @@ -33,6 +33,7 @@ #include "OrthancConfiguration.h" #include "ServerContext.h" #include "ServerJobs/DicomModalityStoreJob.h" +#include "ServerJobs/ThreadedInstancesLoader.h" namespace Orthanc @@ -46,7 +47,9 @@ private: ServerContext& context_; const std::string& localAet_; - std::vector<std::string> instances_; + std::vector<std::string> instancesIds_; + // std::vector<FileInfo> filesInfo_; + std::unique_ptr<ThreadedInstancesLoader> instancesLoader_; size_t position_; RemoteModalityParameters remote_; std::string originatorAet_; @@ -57,6 +60,7 @@ SynchronousMove(ServerContext& context, const std::string& targetAet, const std::vector<std::string>& publicIds, + ResourceType resourceType, const std::string& originatorAet, uint16_t originatorId) : context_(context), @@ -65,43 +69,49 @@ originatorAet_(originatorAet), originatorId_(originatorId) { + unsigned int loaderThreads; + { OrthancConfiguration::ReaderLock lock; remote_ = lock.GetConfiguration().GetModalityUsingAet(targetAet); + loaderThreads = lock.GetConfiguration().GetLoaderThreads(); } + instancesLoader_.reset(new ThreadedInstancesLoader(context_, loaderThreads, false, DicomTransferSyntax_BigEndianExplicit /* dummy value*/, 0, "CSTO")); + + std::vector<FileInfo> filesInfo; for (size_t i = 0; i < publicIds.size(); i++) { - CLOG(INFO, DICOM) << "Sending resource " << publicIds[i] << " to modality \"" + const std::string resourceId = publicIds[i]; + + CLOG(INFO, DICOM) << "Sending resource " << resourceId << " to modality \"" << targetAet << "\" in synchronous mode"; - std::list<std::string> tmp; - context_.GetIndex().GetChildInstances(tmp, publicIds[i]); + context.GetOrderedChildInstances(instancesIds_, filesInfo, resourceId, resourceType); + } - instances_.reserve(tmp.size()); - for (std::list<std::string>::iterator it = tmp.begin(); it != tmp.end(); ++it) - { - instances_.push_back(*it); - } + for (size_t i = 0; i < instancesIds_.size(); ++i) + { + instancesLoader_->PrepareDicom(instancesIds_[i], filesInfo[i]); } } virtual unsigned int GetSubOperationCount() const ORTHANC_OVERRIDE { - return instances_.size(); + return instancesIds_.size(); } virtual Status DoNext() ORTHANC_OVERRIDE { - if (position_ >= instances_.size()) + if (position_ >= instancesIds_.size()) { return Status_Failure; } - const std::string& id = instances_[position_++]; + const std::string& id = instancesIds_[position_++]; std::string dicom; - context_.ReadDicom(dicom, id); + instancesLoader_->GetDicom(dicom, id); if (connection_.get() == NULL) { @@ -130,6 +140,7 @@ AsynchronousMove(ServerContext& context, const std::string& targetAet, const std::vector<std::string>& publicIds, + ResourceType resourceType, const std::string& originatorAet, uint16_t originatorId) : context_(context), @@ -151,23 +162,23 @@ job_->SetMoveOriginator(originatorAet, originatorId); } + std::vector<std::string> instancesIds; + std::vector<FileInfo> filesInfo; + for (size_t i = 0; i < publicIds.size(); i++) { - CLOG(INFO, DICOM) << "Sending resource " << publicIds[i] << " to modality \"" + const std::string resourceId = publicIds[i]; + + CLOG(INFO, DICOM) << "Sending resource " << resourceId << " to modality \"" << targetAet << "\" in asynchronous mode"; - std::list<std::string> tmp; - context_.GetIndex().GetChildInstances(tmp, publicIds[i]); - - countInstances_ = tmp.size(); + job_->AddParentResource(resourceId, resourceType); - job_->Reserve(job_->GetCommandsCount() + tmp.size()); + context.GetOrderedChildInstances(instancesIds, filesInfo, resourceId, resourceType); + } - for (std::list<std::string>::iterator it = tmp.begin(); it != tmp.end(); ++it) - { - job_->AddInstance(*it); - } - } + job_->AddInstances(instancesIds, filesInfo); + countInstances_ = instancesIds.size(); } virtual unsigned int GetSubOperationCount() const ORTHANC_OVERRIDE @@ -289,6 +300,7 @@ static IMoveRequestIterator* CreateIterator(ServerContext& context, const std::string& targetAet, const std::vector<std::string>& publicIds, + ResourceType resourceType, const std::string& originatorAet, uint16_t originatorId) { @@ -307,11 +319,11 @@ if (synchronous) { - return new SynchronousMove(context, targetAet, publicIds, originatorAet, originatorId); + return new SynchronousMove(context, targetAet, publicIds, resourceType, originatorAet, originatorId); } else { - return new AsynchronousMove(context, targetAet, publicIds, originatorAet, originatorId); + return new AsynchronousMove(context, targetAet, publicIds, resourceType, originatorAet, originatorId); } } @@ -356,12 +368,21 @@ std::vector<std::string> publicIds; - if (LookupIdentifiers(publicIds, ResourceType_Instance, input) || - LookupIdentifiers(publicIds, ResourceType_Series, input) || - LookupIdentifiers(publicIds, ResourceType_Study, input) || - LookupIdentifiers(publicIds, ResourceType_Patient, input)) + if (LookupIdentifiers(publicIds, ResourceType_Instance, input)) + { + return CreateIterator(context_, targetAet, publicIds, ResourceType_Instance, connection.GetRemoteAet(), originatorId); + } + else if (LookupIdentifiers(publicIds, ResourceType_Series, input)) { - return CreateIterator(context_, targetAet, publicIds, connection.GetRemoteAet(), originatorId); + return CreateIterator(context_, targetAet, publicIds, ResourceType_Series, connection.GetRemoteAet(), originatorId); + } + else if (LookupIdentifiers(publicIds, ResourceType_Study, input)) + { + return CreateIterator(context_, targetAet, publicIds, ResourceType_Study, connection.GetRemoteAet(), originatorId); + } + else if (LookupIdentifiers(publicIds, ResourceType_Patient, input)) + { + return CreateIterator(context_, targetAet, publicIds, ResourceType_Patient, connection.GetRemoteAet(), originatorId); } else { @@ -382,7 +403,7 @@ if (LookupIdentifiers(publicIds, level, input)) { - return CreateIterator(context_, targetAet, publicIds, connection.GetRemoteAet(), originatorId); + return CreateIterator(context_, targetAet, publicIds, level, connection.GetRemoteAet(), originatorId); } else {
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestArchive.cpp Wed Mar 25 08:26:53 2026 +0100 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestArchive.cpp Fri Apr 03 11:46:14 2026 +0200 @@ -52,7 +52,6 @@ static const char* const GET_FILENAME = "filename"; static const char* const GET_RESOURCES = "resources"; - static const char* const CONFIG_LOADER_THREADS = "ZipLoaderThreads"; static const char* const CONFIG_ALLOW_UTF8 = "ZipUseUtf8"; @@ -188,7 +187,7 @@ { OrthancConfiguration::ReaderLock lock; - loaderThreads = lock.GetConfiguration().GetUnsignedIntegerParameter(CONFIG_LOADER_THREADS, 0); // New in Orthanc 1.10.0 + loaderThreads = lock.GetConfiguration().GetLoaderThreads(); } // New in Orthanc 1.12.11 @@ -783,7 +782,7 @@ { OrthancConfiguration::ReaderLock lock; - unsigned int loaderThreads = lock.GetConfiguration().GetUnsignedIntegerParameter(CONFIG_LOADER_THREADS, 0); // New in Orthanc 1.10.0 + unsigned int loaderThreads = lock.GetConfiguration().GetLoaderThreads(); job->SetLoaderThreads(loaderThreads); }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp Wed Mar 25 08:26:53 2026 +0100 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp Fri Apr 03 11:46:14 2026 +0200 @@ -1360,7 +1360,7 @@ ***************************************************************************/ static void GetInstancesToExport(Json::Value& otherArguments, - SetOfInstancesJob& job, + StoreJob& job, const std::string& remote, RestApiPostCall& call) { @@ -1439,23 +1439,27 @@ "Resources to be exported must be specified as a JSON array of strings"); } - std::string stripped = Toolbox::StripSpaces((*resources) [i].asString()); - if (!Toolbox::IsSHA1(stripped)) + std::string resourceId = Toolbox::StripSpaces((*resources) [i].asString()); + if (!Toolbox::IsSHA1(resourceId)) { throw OrthancException(ErrorCode_BadFileFormat, - "This string is not a valid Orthanc identifier: " + stripped); + "This string is not a valid Orthanc identifier: " + resourceId); } ResourceType level; - context.GetIndex().LookupResourceType(level, stripped); - - job.AddParentResource(stripped, level); // New in Orthanc 1.5.7 - - context.AddChildInstances(job, stripped, level); + context.GetIndex().LookupResourceType(level, resourceId); + + job.AddParentResource(resourceId, level); // New in Orthanc 1.5.7 + + std::vector<std::string> instancesIds; + std::vector<FileInfo> filesInfo; + + context.GetOrderedChildInstances(instancesIds, filesInfo, resourceId, level); + job.AddInstances(instancesIds, filesInfo); if (logExportedResources) { - context.GetIndex().LogExportedResource(stripped, remote); + context.GetIndex().LogExportedResource(resourceId, remote); } } }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp Wed Mar 25 08:26:53 2026 +0100 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp Fri Apr 03 11:46:14 2026 +0200 @@ -75,7 +75,7 @@ static const char* const DATABASE_BACKEND_PLUGIN = "DatabaseBackendPlugin"; static const char* const DATABASE_VERSION = "DatabaseVersion"; static const char* const DATABASE_SERVER_IDENTIFIER = "DatabaseServerIdentifier"; - static const char* const DEFAULT_RETRIEVE_METHOD = "DefaultRetrieveMethod"; + static const char* const DICOM_DEFAULT_RETRIEVE_METHOD = "DicomDefaultRetrieveMethod"; static const char* const DICOM_AET = "DicomAet"; static const char* const DICOM_PORT = "DicomPort"; static const char* const HTTP_PORT = "HttpPort"; @@ -120,7 +120,7 @@ "Information about the installed storage area plugin (`null` if no such plugin is installed)") .SetAnswerField(DATABASE_BACKEND_PLUGIN, RestApiCallDocumentation::Type_String, "Information about the installed database index plugin (`null` if no such plugin is installed)") - .SetAnswerField(DEFAULT_RETRIEVE_METHOD, RestApiCallDocumentation::Type_String, "The DefaultRetrieveMethod configuration") + .SetAnswerField(DICOM_DEFAULT_RETRIEVE_METHOD, RestApiCallDocumentation::Type_String, "The DicomDefaultRetrieveMethod configuration") .SetAnswerField(DICOM_AET, RestApiCallDocumentation::Type_String, "The DICOM AET of Orthanc") .SetAnswerField(DICOM_PORT, RestApiCallDocumentation::Type_Number, "The port to the DICOM server of Orthanc") .SetAnswerField(HTTP_PORT, RestApiCallDocumentation::Type_Number, "The port to the HTTP server of Orthanc") @@ -183,7 +183,7 @@ result[MAXIMUM_STORAGE_SIZE] = lock.GetConfiguration().GetUnsignedIntegerParameter(MAXIMUM_STORAGE_SIZE, 0); // New in Orthanc 1.11.3 result[MAXIMUM_PATIENT_COUNT] = lock.GetConfiguration().GetUnsignedIntegerParameter(MAXIMUM_PATIENT_COUNT, 0); // New in Orthanc 1.12.4 result[MAXIMUM_STORAGE_MODE] = lock.GetConfiguration().GetStringParameter(MAXIMUM_STORAGE_MODE, "Recycle"); // New in Orthanc 1.11.3 - result[DEFAULT_RETRIEVE_METHOD] = lock.GetConfiguration().GetStringParameter(DEFAULT_RETRIEVE_METHOD, "C-MOVE"); + result[DICOM_DEFAULT_RETRIEVE_METHOD] = lock.GetConfiguration().GetStringParameter(DICOM_DEFAULT_RETRIEVE_METHOD, "C-MOVE"); } result[STORAGE_AREA_PLUGIN] = Json::nullValue;
--- a/OrthancServer/Sources/ServerContext.cpp Wed Mar 25 08:26:53 2026 +0100 +++ b/OrthancServer/Sources/ServerContext.cpp Fri Apr 03 11:46:14 2026 +0200 @@ -49,6 +49,7 @@ #include "ServerToolbox.h" #include "StorageCommitmentReports.h" #include "OutgoingDicomInstance.h" +#include "SimpleInstanceOrdering.h" #include <dcmtk/dcmdata/dcfilefo.h> #include <dcmtk/dcmnet/dimse.h> @@ -1809,20 +1810,53 @@ } - void ServerContext::AddChildInstances(SetOfInstancesJob& job, - const std::string& publicId, - ResourceType level) + void ServerContext::GetOrderedChildInstances(std::vector<std::string>& instancesIds, + std::vector<FileInfo>& filesInfo, + const std::string& publicId, + ResourceType level) { - std::list<std::string> instances; - GetIndex().GetChildInstances(instances, publicId, level); + // don't mix series and, inside a series, order the instances per instance number + switch (level) + { + case ResourceType_Patient: + case ResourceType_Study: + { + std::list<std::string> childrenIds; + GetIndex().GetChildren(childrenIds, level, publicId); + + for (std::list<std::string>::const_iterator it = childrenIds.begin(); it != childrenIds.end(); ++it) + { + GetOrderedChildInstances(instancesIds, filesInfo, *it, GetChildResourceType(level)); + } + }; break; + case ResourceType_Series: + { + SimpleInstanceOrdering orderedInstances(GetIndex(), publicId); - job.Reserve(job.GetInstancesCount() + instances.size()); + instancesIds.reserve(instancesIds.size() + orderedInstances.GetInstancesCount()); + filesInfo.reserve(filesInfo.size() + orderedInstances.GetInstancesCount()); + + for (size_t i = 0; i < orderedInstances.GetInstancesCount(); ++i) + { + instancesIds.push_back(orderedInstances.GetInstanceId(i)); + filesInfo.push_back(orderedInstances.GetInstanceFileInfo(i)); + } + }; break; + case ResourceType_Instance: + { + FileInfo fileInfo; + int64_t revisionNotUsed; - for (std::list<std::string>::const_iterator - it = instances.begin(); it != instances.end(); ++it) - { - job.AddInstance(*it); + if (GetIndex().LookupAttachment(fileInfo, revisionNotUsed, level, publicId, FileContentType_Dicom)) + { + instancesIds.push_back(publicId); + filesInfo.push_back(fileInfo); + } + }; break; + default: + throw OrthancException(ErrorCode_InternalError); } + }
--- a/OrthancServer/Sources/ServerContext.h Wed Mar 25 08:26:53 2026 +0100 +++ b/OrthancServer/Sources/ServerContext.h Fri Apr 03 11:46:14 2026 +0200 @@ -513,9 +513,10 @@ bool HasPlugins() const; - void AddChildInstances(SetOfInstancesJob& job, - const std::string& publicId, - ResourceType level); + void GetOrderedChildInstances(std::vector<std::string>& instancesIds, + std::vector<FileInfo>& filesInfo, + const std::string& publicId, + ResourceType level); void SignalUpdatedModalities();
--- a/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp Wed Mar 25 08:26:53 2026 +0100 +++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp Fri Apr 03 11:46:14 2026 +0200 @@ -36,6 +36,7 @@ #include "../OrthancConfiguration.h" #include "../ServerContext.h" #include "../SimpleInstanceOrdering.h" +#include "ThreadedInstancesLoader.h" #include <stdio.h> #include <boost/range/algorithm/count.hpp> @@ -57,8 +58,6 @@ static const char* const KEY_TRANSCODE = "Transcode"; static const char* const KEY_ALLOW_UTF8 = "Utf8"; -static boost::mutex loaderThreadsCounterMutex; -static uint32_t loaderThreadsCounter = 0; namespace Orthanc { @@ -86,263 +85,6 @@ } - class ArchiveJob::InstanceLoader : public boost::noncopyable - { - protected: - ServerContext& context_; - bool transcode_; - DicomTransferSyntax transferSyntax_; - unsigned int lossyQuality_; - public: - explicit InstanceLoader(ServerContext& context, bool transcode, DicomTransferSyntax transferSyntax, unsigned int lossyQuality) - : context_(context), - transcode_(transcode), - transferSyntax_(transferSyntax), - lossyQuality_(lossyQuality) - { - } - - virtual ~InstanceLoader() - { - } - - virtual void PrepareDicom(const std::string& instanceId, const FileInfo& fileInfo) - { - } - - bool TranscodeDicom(std::string& transcodedBuffer, const std::string& sourceBuffer, const std::string& instanceId) - { - if (transcode_) - { - std::set<DicomTransferSyntax> syntaxes; - syntaxes.insert(transferSyntax_); - - IDicomTranscoder::DicomImage source, transcoded; - source.SetExternalBuffer(sourceBuffer); - - if (context_.Transcode(transcoded, source, syntaxes, TranscodingSopInstanceUidMode_AllowNew, lossyQuality_)) - { - transcodedBuffer.assign(reinterpret_cast<const char*>(transcoded.GetBufferData()), transcoded.GetBufferSize()); - return true; - } - else - { - LOG(INFO) << "Cannot transcode instance " << instanceId - << " to transfer syntax: " << GetTransferSyntaxUid(transferSyntax_); - } - } - - return false; - } - - virtual void GetDicom(std::string& dicom, const std::string& instanceId, const FileInfo& fileInfo) = 0; - - virtual void Clear(bool isAbort) - { - } - }; - - class ArchiveJob::SynchronousInstanceLoader : public ArchiveJob::InstanceLoader - { - public: - explicit SynchronousInstanceLoader(ServerContext& context, bool transcode, DicomTransferSyntax transferSyntax, unsigned int lossyQuality) - : InstanceLoader(context, transcode, transferSyntax, lossyQuality) - { - } - - virtual void GetDicom(std::string& dicom, const std::string& instanceId, const FileInfo& fileInfo) ORTHANC_OVERRIDE - { - context_.ReadAttachment(dicom, fileInfo, true); - - if (transcode_) - { - std::string transcoded; - if (TranscodeDicom(transcoded, dicom, instanceId)) - { - dicom.swap(transcoded); - } - } - - } - }; - - class InstanceToPreload : public Orthanc::IDynamicObject - { - private: - std::string id_; - FileInfo fileInfo_; - - public: - explicit InstanceToPreload(const std::string& id, const FileInfo& fileInfo) : - id_(id), - fileInfo_(fileInfo) - { - } - - virtual ~InstanceToPreload() ORTHANC_OVERRIDE - { - } - - const std::string& GetId() const {return id_;}; - const FileInfo& GetFileInfo() const {return fileInfo_;}; - }; - - class ArchiveJob::ThreadedInstanceLoader : public ArchiveJob::InstanceLoader - { - boost::condition_variable condInstanceAvailable_; - std::map<std::string, boost::shared_ptr<std::string> > availableInstances_; - boost::mutex availableInstancesMutex_; - BlockingSharedMessageQueue instancesToPreload_; - std::vector<boost::thread*> threads_; - bool loadersShouldStop_; - - - public: - ThreadedInstanceLoader(ServerContext& context, size_t threadCount, bool transcode, DicomTransferSyntax transferSyntax, unsigned int lossyQuality) - : InstanceLoader(context, transcode, transferSyntax, lossyQuality), - instancesToPreload_ (3*threadCount), - loadersShouldStop_(false) - { - for (size_t i = 0; i < threadCount; i++) - { - threads_.push_back(new boost::thread(PreloaderWorkerThread, this)); - } - } - - virtual ~ThreadedInstanceLoader() ORTHANC_OVERRIDE - { - ThreadedInstanceLoader::Clear(false); - } - - virtual void Clear(bool isAbort) ORTHANC_OVERRIDE - { - if (threads_.size() > 0) - { - loadersShouldStop_ = true; // not need to protect this by a mutex. This is the only "writer" and all loaders are "readers" - - if (isAbort) - { - LOG(INFO) << "Cancelling the loader threads"; - instancesToPreload_.Clear(); - } - else - { - LOG(INFO) << "Waiting for loader threads to complete"; - } - - // unlock the loaders if they are waiting on this message queue (this happens when the job completes sucessfully) - for (size_t i = 0; i < threads_.size(); i++) - { - instancesToPreload_.Enqueue(NULL); - } - - for (size_t i = 0; i < threads_.size(); i++) - { - if (threads_[i]->joinable()) - { - threads_[i]->join(); - } - delete threads_[i]; - } - - threads_.clear(); - availableInstances_.clear(); - - LOG(INFO) << "Waiting for loader threads to complete - done"; - } - } - - static void PreloaderWorkerThread(ThreadedInstanceLoader* that) - { - { - boost::mutex::scoped_lock lock(loaderThreadsCounterMutex); - Logging::SetCurrentThreadName(std::string("ARCH-LOAD-") + boost::lexical_cast<std::string>(loaderThreadsCounter++)); - loaderThreadsCounter %= 1000000; - } - - LOG(INFO) << "Loader thread has started"; - - while (true) - { - std::unique_ptr<InstanceToPreload> instanceToPreload(dynamic_cast<InstanceToPreload*>(that->instancesToPreload_.Dequeue(0))); - if (instanceToPreload.get() == NULL || that->loadersShouldStop_) // that's the signal to exit the thread - { - LOG(INFO) << "Loader thread has completed"; - return; - } - - try - { - boost::shared_ptr<std::string> dicomContent(new std::string()); - that->context_.ReadAttachment(*dicomContent, instanceToPreload->GetFileInfo(), true); - - if (that->transcode_) - { - boost::shared_ptr<std::string> transcodedDicom(new std::string()); - if (that->TranscodeDicom(*transcodedDicom, *dicomContent, instanceToPreload->GetId())) - { - dicomContent = transcodedDicom; - } - } - - { - boost::mutex::scoped_lock lock(that->availableInstancesMutex_); - that->availableInstances_[instanceToPreload->GetId()] = dicomContent; - that->condInstanceAvailable_.notify_one(); - } - } - catch (OrthancException& e) - { - LOG(ERROR) << "Failed to load instance " << instanceToPreload->GetId() << " error: " << e.GetDetails(); - boost::mutex::scoped_lock lock(that->availableInstancesMutex_); - // store a NULL result to notify that we could not read the instance - that->availableInstances_[instanceToPreload->GetId()] = boost::shared_ptr<std::string>(); - that->condInstanceAvailable_.notify_one(); - } - catch (...) - { - LOG(ERROR) << "Failed to load instance " << instanceToPreload->GetId() << " unknown error"; - boost::mutex::scoped_lock lock(that->availableInstancesMutex_); - // store a NULL result to notify that we could not read the instance - that->availableInstances_[instanceToPreload->GetId()] = boost::shared_ptr<std::string>(); - that->condInstanceAvailable_.notify_one(); - } - } - } - - virtual void PrepareDicom(const std::string& instanceId, const FileInfo& fileInfo) ORTHANC_OVERRIDE - { - instancesToPreload_.Enqueue(new InstanceToPreload(instanceId, fileInfo)); - } - - virtual void GetDicom(std::string& dicom, const std::string& instanceId, const FileInfo& fileInfo) ORTHANC_OVERRIDE - { - boost::mutex::scoped_lock lock(availableInstancesMutex_); - - while (true) - { - // wait for this instance to be available but this might not be the one we are waiting for ! - while (availableInstances_.find(instanceId) == availableInstances_.end()) - { - condInstanceAvailable_.wait(lock); - } - - boost::shared_ptr<std::string> dicomContent; - - // this is the instance we were waiting for - dicomContent = availableInstances_[instanceId]; - availableInstances_.erase(instanceId); - - if (dicomContent.get() == NULL) // there has been an error while reading the file - { - throw OrthancException(ErrorCode_InexistentItem); - } - dicom.swap(*dicomContent); - - return; - } - } - }; // This enum defines specific resource types to be used when exporting the archive. // It defines if we should use the PatientInfo from the Patient or from the Study. @@ -774,7 +516,7 @@ } void Apply(HierarchicalZipWriter& writer, - InstanceLoader& instanceLoader, + ThreadedInstancesLoader& instancesLoader, DicomDirWriter* dicomDir, const std::string& dicomDirFolder, bool transcode, @@ -797,7 +539,7 @@ try { LOG(INFO) << "Adding instance " << instanceId_ << " in zip"; - instanceLoader.GetDicom(content, instanceId_, fileInfo_); + instancesLoader.GetDicom(content, instanceId_); } catch (OrthancException& e) { @@ -828,11 +570,11 @@ std::deque<Command*> commands_; uint64_t uncompressedSize_; unsigned int instancesCount_; - InstanceLoader& instanceLoader_; + ThreadedInstancesLoader& instancesLoader_; void ApplyInternal(HierarchicalZipWriter& writer, - InstanceLoader& instanceLoader, + ThreadedInstancesLoader& instancesLoader, size_t index, DicomDirWriter* dicomDir, const std::string& dicomDirFolder, @@ -844,14 +586,14 @@ throw OrthancException(ErrorCode_ParameterOutOfRange); } - commands_[index]->Apply(writer, instanceLoader, dicomDir, dicomDirFolder, transcode, transferSyntax); + commands_[index]->Apply(writer, instancesLoader, dicomDir, dicomDirFolder, transcode, transferSyntax); } public: - explicit ZipCommands(InstanceLoader& instanceLoader) : + explicit ZipCommands(ThreadedInstancesLoader& instancesLoader) : uncompressedSize_(0), instancesCount_(0), - instanceLoader_(instanceLoader) + instancesLoader_(instancesLoader) { } @@ -882,24 +624,24 @@ // "media" flavor (with DICOMDIR) void Apply(HierarchicalZipWriter& writer, - InstanceLoader& instanceLoader, + ThreadedInstancesLoader& instancesLoader, size_t index, DicomDirWriter& dicomDir, const std::string& dicomDirFolder, bool transcode, DicomTransferSyntax transferSyntax) const { - ApplyInternal(writer, instanceLoader, index, &dicomDir, dicomDirFolder, transcode, transferSyntax); + ApplyInternal(writer, instancesLoader, index, &dicomDir, dicomDirFolder, transcode, transferSyntax); } // "archive" flavor (without DICOMDIR) void Apply(HierarchicalZipWriter& writer, - InstanceLoader& instanceLoader, + ThreadedInstancesLoader& instancesLoader, size_t index, bool transcode, DicomTransferSyntax transferSyntax) const { - ApplyInternal(writer, instanceLoader, index, NULL, "", transcode, transferSyntax); + ApplyInternal(writer, instancesLoader, index, NULL, "", transcode, transferSyntax); } void AddOpenDirectory(const std::string& filename) @@ -916,7 +658,7 @@ const std::string& instanceId, const FileInfo& fileInfo) { - instanceLoader_.PrepareDicom(instanceId, fileInfo); + instancesLoader_.PrepareDicom(instanceId, fileInfo); commands_.push_back(new Command(Type_WriteInstance, filename, instanceId, fileInfo)); instancesCount_ ++; uncompressedSize_ += fileInfo.GetUncompressedSize(); @@ -1090,7 +832,7 @@ { private: ServerContext& context_; - InstanceLoader& instanceLoader_; + ThreadedInstancesLoader& instancesLoader_; ZipCommands commands_; std::unique_ptr<HierarchicalZipWriter> zip_; std::unique_ptr<DicomDirWriter> dicomDir_; @@ -1100,14 +842,14 @@ public: ZipWriterIterator(ServerContext& context, - InstanceLoader& instanceLoader, + ThreadedInstancesLoader& instancesLoader, ArchiveIndex& archive, bool isMedia, bool enableExtendedSopClass, bool allowUtf8) : context_(context), - instanceLoader_(instanceLoader), - commands_(instanceLoader), + instancesLoader_(instancesLoader), + commands_(instancesLoader), isMedia_(isMedia), isStream_(false), allowUtf8_(allowUtf8) @@ -1234,13 +976,13 @@ if (isMedia_) { assert(dicomDir_.get() != NULL); - commands_.Apply(*zip_, instanceLoader_, index, *dicomDir_, + commands_.Apply(*zip_, instancesLoader_, index, *dicomDir_, MEDIA_IMAGES_FOLDER, transcode, transferSyntax); } else { assert(dicomDir_.get() == NULL); - commands_.Apply(*zip_, instanceLoader_, index, transcode, transferSyntax); + commands_.Apply(*zip_, instancesLoader_, index, transcode, transferSyntax); } } } @@ -1273,7 +1015,7 @@ transcode_(false), transferSyntax_(DicomTransferSyntax_LittleEndianImplicit), lossyQuality_(100), - loaderThreads_(0), + loaderThreads_(1), allowUtf8_(false) { } @@ -1395,7 +1137,7 @@ } else { - loaderThreads_ = loaderThreads; + loaderThreads_ = std::max(1u, loaderThreads); } } @@ -1422,15 +1164,7 @@ void ArchiveJob::Start() { - if (loaderThreads_ == 0) - { - // default behaviour before loaderThreads was introducted in 1.10.0 - instanceLoader_.reset(new SynchronousInstanceLoader(context_, transcode_, transferSyntax_, lossyQuality_)); - } - else - { - instanceLoader_.reset(new ThreadedInstanceLoader(context_, loaderThreads_, transcode_, transferSyntax_, lossyQuality_)); - } + instancesLoader_.reset(new ThreadedInstancesLoader(context_, loaderThreads_, transcode_, transferSyntax_, lossyQuality_, "ARCH")); if (writer_.get() != NULL) { @@ -1453,7 +1187,7 @@ assert(asynchronousTarget_.get() != NULL); asynchronousTarget_->Touch(); // Make sure we can write to the temporary file - writer_.reset(new ZipWriterIterator(context_, *instanceLoader_, *archive_, isMedia_, enableExtendedSopClass_, allowUtf8_)); + writer_.reset(new ZipWriterIterator(context_, *instancesLoader_, *archive_, isMedia_, enableExtendedSopClass_, allowUtf8_)); writer_->SetOutputFile(asynchronousTarget_->GetPath()); } } @@ -1461,7 +1195,7 @@ { assert(synchronousTarget_.get() != NULL); - writer_.reset(new ZipWriterIterator(context_, *instanceLoader_, *archive_, isMedia_, enableExtendedSopClass_, allowUtf8_)); + writer_.reset(new ZipWriterIterator(context_, *instancesLoader_, *archive_, isMedia_, enableExtendedSopClass_, allowUtf8_)); writer_->AcquireOutputStream(synchronousTarget_.release()); } @@ -1506,9 +1240,9 @@ writer_.reset(); } - if (instanceLoader_.get() != NULL) + if (instancesLoader_.get() != NULL) { - instanceLoader_->Clear(false); + instancesLoader_->Clear(false); } if (asynchronousTarget_.get() != NULL) @@ -1578,9 +1312,9 @@ asynchronousTarget_.reset(); // clear the loader threads - if (instanceLoader_.get() != NULL) + if (instancesLoader_.get() != NULL) { - instanceLoader_->Clear(true); + instancesLoader_->Clear(true); } } }
--- a/OrthancServer/Sources/ServerJobs/ArchiveJob.h Wed Mar 25 08:26:53 2026 +0100 +++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.h Fri Apr 03 11:46:14 2026 +0200 @@ -34,6 +34,7 @@ namespace Orthanc { class ServerContext; + class ThreadedInstancesLoader; class ArchiveJob : public IJob { @@ -45,14 +46,11 @@ class ResourceIdentifiers; class ZipCommands; class ZipWriterIterator; - class InstanceLoader; - class SynchronousInstanceLoader; - class ThreadedInstanceLoader; std::unique_ptr<ZipWriter::IOutputStream> synchronousTarget_; // Only valid before "Start()" std::unique_ptr<TemporaryFile> asynchronousTarget_; ServerContext& context_; - std::unique_ptr<InstanceLoader> instanceLoader_; + std::unique_ptr<ThreadedInstancesLoader> instancesLoader_; boost::shared_ptr<ArchiveIndex> archive_; bool isMedia_; bool enableExtendedSopClass_;
--- a/OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp Wed Mar 25 08:26:53 2026 +0100 +++ b/OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp Fri Apr 03 11:46:14 2026 +0200 @@ -55,7 +55,7 @@ try { - context_.ReadDicom(dicom, instance); + instancesLoader_->GetDicom(dicom, instance); } catch (OrthancException& e) { @@ -112,7 +112,7 @@ DicomModalityStoreJob::DicomModalityStoreJob(ServerContext& context) : - context_(context), + StoreJob(context), moveOriginatorId_(0), // By default, not a C-MOVE storageCommitment_(false) // By default, no storage commitment { @@ -207,9 +207,10 @@ void DicomModalityStoreJob::Stop(JobStopReason reason) // For pausing jobs { connection_.reset(NULL); + + StoreJob::Stop(reason); } - void DicomModalityStoreJob::ResetStorageCommitment() { if (storageCommitment_) @@ -269,8 +270,7 @@ DicomModalityStoreJob::DicomModalityStoreJob(ServerContext& context, const Json::Value& serialized) : - SetOfInstancesJob(serialized), - context_(context) + StoreJob(context, serialized) { moveOriginatorAet_ = SerializationToolbox::ReadString(serialized, MOVE_ORIGINATOR_AET); moveOriginatorId_ = static_cast<uint16_t>
--- a/OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.h Wed Mar 25 08:26:53 2026 +0100 +++ b/OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.h Fri Apr 03 11:46:14 2026 +0200 @@ -24,7 +24,7 @@ #pragma once #include "../../../OrthancFramework/Sources/Compatibility.h" -#include "../../../OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.h" +#include "StoreJob.h" #include "../../../OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.h" #include <list> @@ -33,10 +33,9 @@ { class ServerContext; - class DicomModalityStoreJob : public SetOfInstancesJob + class DicomModalityStoreJob : public StoreJob { private: - ServerContext& context_; DicomAssociationParameters parameters_; std::string moveOriginatorAet_; uint16_t moveOriginatorId_; @@ -93,6 +92,11 @@ target = "DicomModalityStore"; } + virtual const char* GetLoaderPrefix() const + { + return "CSTO"; + } + virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE; virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE;
--- a/OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.cpp Wed Mar 25 08:26:53 2026 +0100 +++ b/OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.cpp Fri Apr 03 11:46:14 2026 +0200 @@ -61,7 +61,7 @@ if (transcode_) { std::string dicom; - context_.ReadDicom(dicom, instance); + instancesLoader_->GetDicom(dicom, instance); std::set<DicomTransferSyntax> syntaxes; syntaxes.insert(transferSyntax_); @@ -217,6 +217,8 @@ void OrthancPeerStoreJob::Stop(JobStopReason reason) // For pausing jobs { client_.reset(NULL); + + StoreJob::Stop(reason); } @@ -249,8 +251,7 @@ OrthancPeerStoreJob::OrthancPeerStoreJob(ServerContext& context, const Json::Value& serialized) : - SetOfInstancesJob(serialized), - context_(context) + StoreJob(context, serialized) { assert(serialized.type() == Json::objectValue); peer_ = WebServiceParameters(serialized[PEER]);
--- a/OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.h Wed Mar 25 08:26:53 2026 +0100 +++ b/OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.h Fri Apr 03 11:46:14 2026 +0200 @@ -24,7 +24,7 @@ #pragma once #include "../../../OrthancFramework/Sources/Compatibility.h" -#include "../../../OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.h" +#include "StoreJob.h" #include "../../../OrthancFramework/Sources/HttpClient.h" #include <stdint.h> @@ -34,10 +34,9 @@ { class ServerContext; - class OrthancPeerStoreJob : public SetOfInstancesJob + class OrthancPeerStoreJob : public StoreJob { private: - ServerContext& context_; WebServiceParameters peer_; std::unique_ptr<HttpClient> client_; bool transcode_; @@ -52,7 +51,7 @@ public: explicit OrthancPeerStoreJob(ServerContext& context) : - context_(context), + StoreJob(context), transcode_(false), transferSyntax_(DicomTransferSyntax_LittleEndianExplicit), // Dummy value compress_(false), @@ -97,6 +96,11 @@ target = "OrthancPeerStore"; } + virtual const char* GetLoaderPrefix() const + { + return "PSTO"; + } + virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE; virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/ServerJobs/StoreJob.cpp Fri Apr 03 11:46:14 2026 +0200 @@ -0,0 +1,118 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, 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. + * + * 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 <http://www.gnu.org/licenses/>. + **/ + + +#include "../PrecompiledHeadersServer.h" +#include "StoreJob.h" + +#include <algorithm> + +#include "../../../OrthancFramework/Sources/Compatibility.h" +#include "../../../OrthancFramework/Sources/Logging.h" +#include "../../../OrthancFramework/Sources/SerializationToolbox.h" +#include "../ServerContext.h" +#include "../OrthancConfiguration.h" + + +namespace Orthanc +{ + StoreJob::StoreJob(ServerContext& context) : + context_(context) + { + } + + StoreJob::StoreJob(ServerContext& context, + const Json::Value& serialized) : + SetOfInstancesJob(serialized), + context_(context) + { + // we need to rebuild the instancesIds_ and filesInfo_ from the SetOfInstancesJob + GetInstancesIds(instancesIds_); + + for (size_t i = 0; i < instancesIds_.size(); ++i) + { + FileInfo fileInfo; + int64_t revisionNotUsed; + if (!context.GetIndex().LookupAttachment(fileInfo, revisionNotUsed, ResourceType_Instance, instancesIds_[i], FileContentType_Dicom)) + { + throw OrthancException(ErrorCode_UnknownResource, std::string("Error while unserializing a job, unable to find DICOM attachment for instance ") + instancesIds_[i]); + } + + filesInfo_.push_back(fileInfo); + } + + } + + void StoreJob::AddInstances(const std::vector<std::string>& instancesIds, + const std::vector<FileInfo>& filesInfo) + { + assert(instancesIds.size() == filesInfo.size()); + + instancesIds_.reserve(instancesIds_.size() + instancesIds.size()); + filesInfo_.reserve(filesInfo.size() + filesInfo.size()); + + std::copy(instancesIds.begin(), instancesIds.end(), std::back_inserter(instancesIds_)); + std::copy(filesInfo.begin(), filesInfo.end(), std::back_inserter(filesInfo_)); + + // create the commands (they must be in the same order as the instancesIds_ such that the instancesLoader loads them in the right consume order) + for (size_t i = 0; i < instancesIds.size(); ++i) + { + AddInstance(instancesIds[i]); + } + } + + void StoreJob::Start() + { + size_t loaderThreads = 1; + { + OrthancConfiguration::ReaderLock lock; + loaderThreads = lock.GetConfiguration().GetLoaderThreads(); + } + + instancesLoader_.reset(new ThreadedInstancesLoader(context_, loaderThreads, false, DicomTransferSyntax_LittleEndianImplicit /* dummy value not used*/, 0, GetLoaderPrefix())); + + for (size_t i = 0; i < instancesIds_.size(); ++i) + { + instancesLoader_->PrepareDicom(instancesIds_[i], filesInfo_[i]); + } + + SetOfInstancesJob::Start(); + } + + void StoreJob::Stop(JobStopReason reason) // For pausing jobs + { + if (reason == JobStopReason_Canceled || + reason == JobStopReason_Failure || + reason == JobStopReason_Retry || + reason == JobStopReason_Success) + { + // clear the loader threads + if (instancesLoader_.get() != NULL) + { + instancesLoader_->Clear(true); + } + } + } + + + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/ServerJobs/StoreJob.h Fri Apr 03 11:46:14 2026 +0200 @@ -0,0 +1,60 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, 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. + * + * 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 <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "../../../OrthancFramework/Sources/Compatibility.h" +#include "../../../OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.h" +#include "../../../OrthancFramework/Sources/FileStorage/FileInfo.h" +#include "ThreadedInstancesLoader.h" + +#include <vector> + +namespace Orthanc +{ + class ServerContext; + + class ORTHANC_PUBLIC StoreJob : public SetOfInstancesJob + { + protected: + ServerContext& context_; + std::unique_ptr<ThreadedInstancesLoader> instancesLoader_; + std::vector<std::string> instancesIds_; + std::vector<FileInfo> filesInfo_; + + public: + explicit StoreJob(ServerContext& context); + + StoreJob(ServerContext& context, + const Json::Value& serialized); + + virtual void Start() ORTHANC_OVERRIDE; + + virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE; + + void AddInstances(const std::vector<std::string>& instancesIds, + const std::vector<FileInfo>& filesInfo); + + virtual const char* GetLoaderPrefix() const = 0; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/ServerJobs/ThreadedInstancesLoader.cpp Fri Apr 03 11:46:14 2026 +0200 @@ -0,0 +1,249 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, 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. + * + * 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 <http://www.gnu.org/licenses/>. + **/ + +#include "ThreadedInstancesLoader.h" + +#include "../ServerContext.h" +#include "../../../OrthancFramework/Sources/Logging.h" +#include "../../../OrthancFramework/Sources/DicomParsing/IDicomTranscoder.h" +#include "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" + +static boost::mutex loaderThreadsCounterMutex; +static uint32_t loaderThreadsCounter = 0; + + +namespace Orthanc +{ + class InstanceToPreload : public Orthanc::IDynamicObject + { + private: + std::string id_; + FileInfo fileInfo_; + + public: + explicit InstanceToPreload(const std::string& id, const FileInfo& fileInfo) : + id_(id), + fileInfo_(fileInfo) + { + } + + virtual ~InstanceToPreload() ORTHANC_OVERRIDE + { + } + + const std::string& GetId() const {return id_;}; + const FileInfo& GetFileInfo() const {return fileInfo_;}; + }; + + + ThreadedInstancesLoader::ThreadedInstancesLoader(ServerContext& context, size_t threadCount, bool transcode, DicomTransferSyntax transferSyntax, unsigned int lossyQuality, const std::string& nameForLogs4Char) + : availableInstancesSemaphore_(3*threadCount), + instancesToPreload_(0), // no limit on the message queue, the flow control is performed by the availableInstancesSemaphore_ + loadersShouldStop_(false), + nameForLogs4Char_(nameForLogs4Char), + context_(context), + transcode_(transcode), + transferSyntax_(transferSyntax), + lossyQuality_(lossyQuality) + + { + assert(nameForLogs4Char_.size() <= 4); + + if (threadCount < 1) + { + throw OrthancException(ErrorCode_InternalError); + } + + for (size_t i = 0; i < threadCount; i++) + { + threads_.push_back(new boost::thread(PreloaderWorkerThread, this)); + } + } + + ThreadedInstancesLoader::~ThreadedInstancesLoader() + { + ThreadedInstancesLoader::Clear(false); + } + + void ThreadedInstancesLoader::Clear(bool isAbort) + { + if (threads_.size() > 0) + { + loadersShouldStop_ = true; // not need to protect this by a mutex. This is the only "writer" and all loaders are "readers" + + if (isAbort) + { + LOG(INFO) << "Cancelling the loader threads"; + instancesToPreload_.Clear(); + } + else + { + LOG(INFO) << "Waiting for loader threads to complete"; + } + + // unlock the loaders if they are waiting on this message queue (this happens when the job completes sucessfully) + for (size_t i = 0; i < threads_.size(); i++) + { + instancesToPreload_.Enqueue(NULL); + } + + // unlock the loaders if they are waiting for room in the availableInstances (this happens when the job is interrupted) + availableInstancesSemaphore_.Release(threads_.size()); + + for (size_t i = 0; i < threads_.size(); i++) + { + if (threads_[i]->joinable()) + { + threads_[i]->join(); + } + delete threads_[i]; + } + + threads_.clear(); + availableInstances_.clear(); + + LOG(INFO) << "Waiting for loader threads to complete - done"; + } + } + + void ThreadedInstancesLoader::PreloaderWorkerThread(ThreadedInstancesLoader* that) + { + { + boost::mutex::scoped_lock lock(loaderThreadsCounterMutex); + Logging::SetCurrentThreadName(that->nameForLogs4Char_ + std::string("-LOAD-") + boost::lexical_cast<std::string>(loaderThreadsCounter++)); + loaderThreadsCounter %= 1000000; + } + + LOG(INFO) << "Loader thread has started"; + + while (true) + { + that->availableInstancesSemaphore_.Acquire(1); // reserve the slot early (since the instances are ordered, it is important that a single worker does not push dozens of small instances while we are waiting for a slot for a big instance) + + std::unique_ptr<InstanceToPreload> instanceToPreload(dynamic_cast<InstanceToPreload*>(that->instancesToPreload_.Dequeue(0))); + if (instanceToPreload.get() == NULL || that->loadersShouldStop_) // that's the signal to exit the thread + { + LOG(INFO) << "Loader thread has completed"; + return; + } + + const std::string& instanceId = instanceToPreload->GetId(); + + try + { + boost::shared_ptr<std::string> dicomContent(new std::string()); + that->context_.ReadAttachment(*dicomContent, instanceToPreload->GetFileInfo(), true); + + if (that->transcode_) + { + boost::shared_ptr<std::string> transcodedDicom(new std::string()); + if (that->TranscodeDicom(*transcodedDicom, *dicomContent, instanceId)) + { + dicomContent = transcodedDicom; + } + } + + { + boost::mutex::scoped_lock lock(that->availableInstancesMutex_); + that->availableInstances_[instanceId] = dicomContent; + that->condInstanceAvailable_.notify_one(); + } + } + catch (OrthancException& e) + { + LOG(ERROR) << "Failed to load instance " << instanceId << " error: " << e.GetDetails(); + boost::mutex::scoped_lock lock(that->availableInstancesMutex_); + // store a NULL result to notify that we could not read the instance + that->availableInstances_[instanceId] = boost::shared_ptr<std::string>(); + that->condInstanceAvailable_.notify_one(); + } + catch (...) + { + LOG(ERROR) << "Failed to load instance " << instanceId << " unknown error"; + boost::mutex::scoped_lock lock(that->availableInstancesMutex_); + // store a NULL result to notify that we could not read the instance + that->availableInstances_[instanceId] = boost::shared_ptr<std::string>(); + that->condInstanceAvailable_.notify_one(); + } + } + } + + void ThreadedInstancesLoader::PrepareDicom(const std::string& instanceId, const FileInfo& fileInfo) + { + instancesToPreload_.Enqueue(new InstanceToPreload(instanceId, fileInfo)); + } + + void ThreadedInstancesLoader::GetDicom(std::string& dicom, const std::string& instanceId) + { + boost::mutex::scoped_lock lock(availableInstancesMutex_); + + while (true) + { + // wait for this instance to be available but this might not be the one we are waiting for ! + while (availableInstances_.find(instanceId) == availableInstances_.end()) + { + condInstanceAvailable_.wait(lock); + } + + boost::shared_ptr<std::string> dicomContent; + + // this is the instance we were waiting for + dicomContent = availableInstances_[instanceId]; + availableInstances_.erase(instanceId); + availableInstancesSemaphore_.Release(1); + + if (dicomContent.get() == NULL) // there has been an error while reading the file + { + throw OrthancException(ErrorCode_InexistentItem); + } + dicom.swap(*dicomContent); + + return; + } + }; + + bool ThreadedInstancesLoader::TranscodeDicom(std::string& transcodedBuffer, const std::string& sourceBuffer, const std::string& instanceId) + { + if (transcode_) + { + std::set<DicomTransferSyntax> syntaxes; + syntaxes.insert(transferSyntax_); + + IDicomTranscoder::DicomImage source, transcoded; + source.SetExternalBuffer(sourceBuffer); + + if (context_.Transcode(transcoded, source, syntaxes, TranscodingSopInstanceUidMode_AllowNew, lossyQuality_)) + { + transcodedBuffer.assign(reinterpret_cast<const char*>(transcoded.GetBufferData()), transcoded.GetBufferSize()); + return true; + } + else + { + LOG(INFO) << "Cannot transcode instance " << instanceId + << " to transfer syntax: " << GetTransferSyntaxUid(transferSyntax_); + } + } + + return false; + } + +} \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/ServerJobs/ThreadedInstancesLoader.h Fri Apr 03 11:46:14 2026 +0200 @@ -0,0 +1,72 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, 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. + * + * 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 <http://www.gnu.org/licenses/>. + **/ + +#pragma once + +#include <string> +#include <boost/noncopyable.hpp> +#include "../../../OrthancFramework/Sources/Compatibility.h" +#include "../../../OrthancFramework/Sources/MultiThreading/BlockingSharedMessageQueue.h" +#include "../../../OrthancFramework/Sources/Enumerations.h" +#include "../../../OrthancFramework/Sources/MultiThreading/Semaphore.h" + + +namespace Orthanc +{ + class ServerContext; + class FileInfo; + + class ThreadedInstancesLoader : public boost::noncopyable + { + boost::condition_variable condInstanceAvailable_; + std::map<std::string, boost::shared_ptr<std::string> > availableInstances_; + boost::mutex availableInstancesMutex_; + Semaphore availableInstancesSemaphore_; + BlockingSharedMessageQueue instancesToPreload_; + std::vector<boost::thread*> threads_; + bool loadersShouldStop_; + std::string nameForLogs4Char_; + + protected: + ServerContext& context_; + bool transcode_; + DicomTransferSyntax transferSyntax_; + unsigned int lossyQuality_; + + public: + ThreadedInstancesLoader(ServerContext& context, size_t threadCount, bool transcode, DicomTransferSyntax transferSyntax, unsigned int lossyQuality, const std::string& nameForLogs4Char); + + ~ThreadedInstancesLoader(); + + void Clear(bool isAbort); + + static void PreloaderWorkerThread(ThreadedInstancesLoader* that); + + void PrepareDicom(const std::string& instanceId, const FileInfo& fileInfo); + + void GetDicom(std::string& dicom, const std::string& instanceId); + + protected: + bool TranscodeDicom(std::string& transcodedBuffer, const std::string& sourceBuffer, const std::string& instanceId); + }; +} + \ No newline at end of file
--- a/TODO Wed Mar 25 08:26:53 2026 +0100 +++ b/TODO Fri Apr 03 11:46:14 2026 +0200 @@ -71,8 +71,6 @@ "admin": { "Password": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8", "Hashing": "sha1"} } -* Reuse the ThreadedInstanceLoader from the ArchiveJob in C-Store, peer/store ... to optimize loading - time when working with an object storage. ============================
