# HG changeset patch # User Sebastien Jodogne # Date 1699456246 -3600 # Node ID 2a7a113d791d2dd8d9aba1800393c1a53cbdf1b8 # Parent 08177310e269ba61322c552ff2bdfb2f8dee1ea2# Parent d5c15e9a63dd79461583da6ff34d3ee7bd4090f3 merge diff -r 08177310e269 -r 2a7a113d791d NEWS --- a/NEWS Wed Nov 08 09:59:31 2023 +0100 +++ b/NEWS Wed Nov 08 16:10:46 2023 +0100 @@ -4,10 +4,31 @@ General ------- +* Performance: + - Allow multiple plugins to use the plugin SDK at the same time. In previous versions, + functions like instance transcoding or instance reading where mutually exclusive. + This can bring some significant improvements particularly in viewers. * Housekeeper plugin: - Update to rebuild the cache of the DicomWeb plugin when updating to DicomWeb 1.15. - New trigger configuration: "DicomWebCacheChange" - Fixed reading the triggers configuration. +* HTTP Compression: + - The default value of the "HttpCompressionEnabled" is now false by default. This reduces + the Orthanc overall CPU usage and latency. This is suitable for setups with large + bandwidth network like LAN. + - When "HttpCompressionEnabled" is true, only the content that is clearly identified as + compressible is compressed (JSON, XML, HTML, text, ...). DICOM files are never + compressed over HTTP. In prior versions, all content types were compressed. + This notably greatly improve loading time of large DICOM + files through WADO-RS e.g in StoneViewer when working on large bandwidth networks. + - When "HttpCompressionEnabled" is true, content < 2KB are never compressed. + +Bug Fixes +--------- + +* Solved a deadlock related to the Job Engine events and plugins. Job events are now pushed + into a queue to be handled asynchronously by plugins. + REST API -------- diff -r 08177310e269 -r 2a7a113d791d OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp --- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp Wed Nov 08 09:59:31 2023 +0100 +++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp Wed Nov 08 16:10:46 2023 +0100 @@ -1574,7 +1574,8 @@ static bool SaveToMemoryBufferInternal(std::string& buffer, DcmFileFormat& dicom, - E_TransferSyntax xfer) + E_TransferSyntax xfer, + std::string& errorMessage) { E_EncodingType encodingType = /*opt_sequenceType*/ EET_ExplicitLength; @@ -1613,14 +1614,24 @@ { // Error buffer.clear(); + errorMessage = std::string(c.text()); return false; } } - bool FromDcmtkBridge::SaveToMemoryBuffer(std::string& buffer, DcmDataset& dataSet) { + std::string errorMessageNotUsed; + return SaveToMemoryBuffer(buffer, dataSet, errorMessageNotUsed); + } + + + + bool FromDcmtkBridge::SaveToMemoryBuffer(std::string& buffer, + DcmDataset& dataSet, + std::string& errorMessage) + { // Determine the transfer syntax which shall be used to write the // information to the file. If not possible, switch to the Little // Endian syntax, with explicit length. @@ -1647,7 +1658,7 @@ ff.validateMetaInfo(xfer); ff.removeInvalidGroups(); - return SaveToMemoryBufferInternal(buffer, ff, xfer); + return SaveToMemoryBufferInternal(buffer, ff, xfer, errorMessage); } diff -r 08177310e269 -r 2a7a113d791d OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h --- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h Wed Nov 08 09:59:31 2023 +0100 +++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h Wed Nov 08 16:10:46 2023 +0100 @@ -203,6 +203,10 @@ static bool SaveToMemoryBuffer(std::string& buffer, DcmDataset& dataSet); + static bool SaveToMemoryBuffer(std::string& buffer, + DcmDataset& dataSet, + std::string& errorMessage); + static bool Transcode(DcmFileFormat& dicom, DicomTransferSyntax syntax, const DcmRepresentationParameter* representation); diff -r 08177310e269 -r 2a7a113d791d OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp --- a/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp Wed Nov 08 09:59:31 2023 +0100 +++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp Wed Nov 08 16:10:46 2023 +0100 @@ -939,9 +939,10 @@ void ParsedDicomFile::SaveToMemoryBuffer(std::string& buffer) { - if (!FromDcmtkBridge::SaveToMemoryBuffer(buffer, *GetDcmtkObject().getDataset())) + std::string errorMessage; + if (!FromDcmtkBridge::SaveToMemoryBuffer(buffer, *GetDcmtkObject().getDataset(), errorMessage) { - throw OrthancException(ErrorCode_InternalError, "Cannot write DICOM file to memory"); + throw OrthancException(ErrorCode_InternalError, "Cannot write DICOM file to memory, DCMTK error: " + errorMessage); } } diff -r 08177310e269 -r 2a7a113d791d OrthancFramework/Sources/Enumerations.cpp --- a/OrthancFramework/Sources/Enumerations.cpp Wed Nov 08 09:59:31 2023 +0100 +++ b/OrthancFramework/Sources/Enumerations.cpp Wed Nov 08 16:10:46 2023 +0100 @@ -35,26 +35,6 @@ namespace Orthanc { - static const char* const MIME_CSS = "text/css"; - static const char* const MIME_DICOM = "application/dicom"; - static const char* const MIME_GIF = "image/gif"; - static const char* const MIME_GZIP = "application/gzip"; - static const char* const MIME_HTML = "text/html"; - static const char* const MIME_JAVASCRIPT = "application/javascript"; - static const char* const MIME_JPEG2000 = "image/jp2"; - static const char* const MIME_NACL = "application/x-nacl"; - static const char* const MIME_PLAIN_TEXT = "text/plain"; - static const char* const MIME_PNACL = "application/x-pnacl"; - static const char* const MIME_SVG = "image/svg+xml"; - static const char* const MIME_WEB_ASSEMBLY = "application/wasm"; - static const char* const MIME_WOFF = "application/x-font-woff"; - static const char* const MIME_WOFF2 = "font/woff2"; - static const char* const MIME_XML_2 = "text/xml"; - static const char* const MIME_ZIP = "application/zip"; - static const char* const MIME_DICOM_WEB_JSON = "application/dicom+json"; - static const char* const MIME_DICOM_WEB_XML = "application/dicom+xml"; - static const char* const MIME_ICO = "image/x-icon"; - // This function is autogenerated by the script // "Resources/CodeGeneration/GenerateErrorCodes.py" const char* EnumerationToString(ErrorCode error) diff -r 08177310e269 -r 2a7a113d791d OrthancFramework/Sources/Enumerations.h --- a/OrthancFramework/Sources/Enumerations.h Wed Nov 08 09:59:31 2023 +0100 +++ b/OrthancFramework/Sources/Enumerations.h Wed Nov 08 16:10:46 2023 +0100 @@ -47,6 +47,27 @@ static const char* const MIME_MTL = "model/mtl"; static const char* const MIME_STL = "model/stl"; + static const char* const MIME_CSS = "text/css"; + static const char* const MIME_DICOM = "application/dicom"; + static const char* const MIME_GIF = "image/gif"; + static const char* const MIME_GZIP = "application/gzip"; + static const char* const MIME_HTML = "text/html"; + static const char* const MIME_JAVASCRIPT = "application/javascript"; + static const char* const MIME_JPEG2000 = "image/jp2"; + static const char* const MIME_NACL = "application/x-nacl"; + static const char* const MIME_PLAIN_TEXT = "text/plain"; + static const char* const MIME_PNACL = "application/x-pnacl"; + static const char* const MIME_SVG = "image/svg+xml"; + static const char* const MIME_WEB_ASSEMBLY = "application/wasm"; + static const char* const MIME_WOFF = "application/x-font-woff"; + static const char* const MIME_WOFF2 = "font/woff2"; + static const char* const MIME_XML_2 = "text/xml"; + static const char* const MIME_ZIP = "application/zip"; + static const char* const MIME_DICOM_WEB_JSON = "application/dicom+json"; + static const char* const MIME_DICOM_WEB_XML = "application/dicom+xml"; + static const char* const MIME_ICO = "image/x-icon"; + + /** * "No Internet Media Type (aka MIME type, content type) for PBM has * been registered with IANA, but the unofficial value diff -r 08177310e269 -r 2a7a113d791d OrthancFramework/Sources/HttpServer/FilesystemHttpHandler.cpp --- a/OrthancFramework/Sources/HttpServer/FilesystemHttpHandler.cpp Wed Nov 08 09:59:31 2023 +0100 +++ b/OrthancFramework/Sources/HttpServer/FilesystemHttpHandler.cpp Wed Nov 08 16:10:46 2023 +0100 @@ -153,7 +153,7 @@ { FilesystemHttpSender sender(p); sender.SetContentType(SystemToolbox::AutodetectMimeType(p.string())); - output.Answer(sender); // TODO COMPRESSION + output.Answer(sender); } else if (listDirectoryContent_ && fs::exists(p) && diff -r 08177310e269 -r 2a7a113d791d OrthancFramework/Sources/HttpServer/HttpFileSender.cpp --- a/OrthancFramework/Sources/HttpServer/HttpFileSender.cpp Wed Nov 08 09:59:31 2023 +0100 +++ b/OrthancFramework/Sources/HttpServer/HttpFileSender.cpp Wed Nov 08 16:10:46 2023 +0100 @@ -53,7 +53,8 @@ if (contentType_.empty()) { - contentType_ = SystemToolbox::AutodetectMimeType(filename); + MimeType mimeType = SystemToolbox::AutodetectMimeType(filename); + contentType_ = EnumerationToString(mimeType); } } diff -r 08177310e269 -r 2a7a113d791d OrthancFramework/Sources/HttpServer/HttpOutput.cpp --- a/OrthancFramework/Sources/HttpServer/HttpOutput.cpp Wed Nov 08 09:59:31 2023 +0100 +++ b/OrthancFramework/Sources/HttpServer/HttpOutput.cpp Wed Nov 08 16:10:46 2023 +0100 @@ -30,6 +30,7 @@ #include "../Logging.h" #include "../OrthancException.h" #include "../Toolbox.h" +#include "../SystemToolbox.h" #include #include @@ -51,6 +52,7 @@ unsigned int keepAliveTimeout) : stream_(stream), state_(State_WritingHeader), + isContentCompressible_(false), status_(HttpStatus_200_Ok), hasContentLength_(false), contentLength_(0), @@ -102,6 +104,17 @@ AddHeader("Content-Type", contentType); } + void HttpOutput::StateMachine::SetContentCompressible(bool isContentCompressible) + { + isContentCompressible_ = isContentCompressible; + } + + bool HttpOutput::StateMachine::IsContentCompressible() const + { + // We assume that all files that compress correctly (mainly JSON, XML) are clearly identified. + return isContentCompressible_; + } + void HttpOutput::StateMachine::SetContentFilename(const char* filename) { // TODO Escape double quotes @@ -275,13 +288,11 @@ HttpCompression HttpOutput::GetPreferredCompression(size_t bodySize) const { -#if 0 - // TODO Do not compress small files? - if (bodySize < 512) + // Do not compress small files since there is no real size benefit. + if (bodySize < 2048) { return HttpCompression_None; } -#endif // Prefer "gzip" over "deflate" if the choice is offered @@ -368,11 +379,13 @@ void HttpOutput::SetContentType(MimeType contentType) { stateMachine_.SetContentType(EnumerationToString(contentType)); + stateMachine_.SetContentCompressible(SystemToolbox::IsContentCompressible(contentType)); } void HttpOutput::SetContentType(const std::string &contentType) { stateMachine_.SetContentType(contentType.c_str()); + stateMachine_.SetContentCompressible(SystemToolbox::IsContentCompressible(contentType)); } void HttpOutput::SetContentFilename(const char *filename) @@ -442,7 +455,7 @@ HttpCompression compression = GetPreferredCompression(length); - if (compression == HttpCompression_None) + if (compression == HttpCompression_None || !IsContentCompressible()) { stateMachine_.SetContentLength(length); stateMachine_.SendBody(buffer, length); diff -r 08177310e269 -r 2a7a113d791d OrthancFramework/Sources/HttpServer/HttpOutput.h --- a/OrthancFramework/Sources/HttpServer/HttpOutput.h Wed Nov 08 09:59:31 2023 +0100 +++ b/OrthancFramework/Sources/HttpServer/HttpOutput.h Wed Nov 08 16:10:46 2023 +0100 @@ -56,6 +56,7 @@ IHttpOutputStream& stream_; State state_; + bool isContentCompressible_; HttpStatus status_; bool hasContentLength_; uint64_t contentLength_; @@ -82,6 +83,8 @@ void SetContentType(const char* contentType); + void SetContentCompressible(bool isCompressible); + void SetContentFilename(const char* filename); void SetCookie(const std::string& cookie, @@ -110,6 +113,8 @@ return state_; } + bool IsContentCompressible() const; + void CheckHeadersCompatibilityWithMultipart() const; void StartStream(const std::string& contentType); @@ -139,6 +144,11 @@ bool IsGzipAllowed() const; + bool IsContentCompressible() const + { + return stateMachine_.IsContentCompressible(); + } + void SendStatus(HttpStatus status, const char* message, size_t messageSize); diff -r 08177310e269 -r 2a7a113d791d OrthancFramework/Sources/SystemToolbox.cpp --- a/OrthancFramework/Sources/SystemToolbox.cpp Wed Nov 08 09:59:31 2023 +0100 +++ b/OrthancFramework/Sources/SystemToolbox.cpp Wed Nov 08 16:10:46 2023 +0100 @@ -725,6 +725,52 @@ } } + bool SystemToolbox::IsContentCompressible(MimeType mime) + { + switch (mime) + { + case MimeType_Css: + case MimeType_Html: + case MimeType_JavaScript: + case MimeType_Json: + case MimeType_Pam: + case MimeType_Pdf: + case MimeType_PlainText: + case MimeType_WebAssembly: + case MimeType_Xml: + case MimeType_PrometheusText: + case MimeType_DicomWebJson: + case MimeType_DicomWebXml: + return true; + default: // for all other (JPEG, DICOM, binary, ...) + return false; + } + } + + bool SystemToolbox::IsContentCompressible(const std::string& contentType) + { + if (contentType.empty()) + { + return false; + } + + if (contentType.find(MIME_JSON) != std::string::npos || + contentType.find(MIME_XML) != std::string::npos || + contentType.find(MIME_DICOM_WEB_JSON) != std::string::npos || + contentType.find(MIME_DICOM_WEB_XML) != std::string::npos || + contentType.find(MIME_PDF) != std::string::npos || + contentType.find(MIME_CSS) != std::string::npos || + contentType.find(MIME_HTML) != std::string::npos || + contentType.find(MIME_JAVASCRIPT) != std::string::npos || + contentType.find(MIME_PLAIN_TEXT) != std::string::npos || + contentType.find(MIME_WEB_ASSEMBLY) != std::string::npos || + contentType.find(MIME_XML_2) != std::string::npos) + { + return true; + } + + return false; + } MimeType SystemToolbox::AutodetectMimeType(const std::string& path) { diff -r 08177310e269 -r 2a7a113d791d OrthancFramework/Sources/SystemToolbox.h --- a/OrthancFramework/Sources/SystemToolbox.h Wed Nov 08 09:59:31 2023 +0100 +++ b/OrthancFramework/Sources/SystemToolbox.h Wed Nov 08 16:10:46 2023 +0100 @@ -108,6 +108,10 @@ static unsigned int GetHardwareConcurrency(); + static bool IsContentCompressible(MimeType mime); + + static bool IsContentCompressible(const std::string& contentType); + static MimeType AutodetectMimeType(const std::string& path); static void GetEnvironmentVariables(std::map& env); diff -r 08177310e269 -r 2a7a113d791d OrthancServer/Plugins/Engine/OrthancPlugins.cpp --- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp Wed Nov 08 09:59:31 2023 +0100 +++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp Wed Nov 08 16:10:46 2023 +0100 @@ -1090,7 +1090,10 @@ class OrthancPlugins::PImpl { private: - boost::mutex contextMutex_; + boost::mutex contextMutex_; + boost::condition_variable contextCond_; + size_t contextRefCount_; + ServerContext* context_; public: @@ -1458,21 +1461,38 @@ }; - class ServerContextLock + // This class ensures that the Context remains valid while being used. + // But it does not prevent multiple users to use the context at the same time. + // (new behavior in 1.12.2. In previous version, only one user could use the "locked" context) + class ServerContextReference { private: - boost::mutex::scoped_lock lock_; ServerContext* context_; + boost::mutex& mutex_; + boost::condition_variable& cond_; + size_t& refCount_; public: - explicit ServerContextLock(PImpl& that) : - lock_(that.contextMutex_), - context_(that.context_) + explicit ServerContextReference(PImpl& that) : + context_(that.context_), + mutex_(that.contextMutex_), + cond_(that.contextCond_), + refCount_(that.contextRefCount_) { if (context_ == NULL) { throw OrthancException(ErrorCode_DatabaseNotInitialized); } + + boost::mutex::scoped_lock lock(mutex_); + refCount_++; + } + + ~ServerContextReference() + { + boost::mutex::scoped_lock lock(mutex_); + refCount_--; + cond_.notify_one(); } ServerContext& GetContext() @@ -1485,7 +1505,13 @@ void SetServerContext(ServerContext* context) { + // update only the context while nobody is using it boost::mutex::scoped_lock lock(contextMutex_); + + while (contextRefCount_ > 0) + { + contextCond_.wait(lock); + } context_ = context; } @@ -1554,6 +1580,7 @@ unsigned int maxDatabaseRetries_; // New in Orthanc 1.9.2 explicit PImpl(const std::string& databaseServerIdentifier) : + contextRefCount_(0), context_(NULL), findCallback_(NULL), worklistCallback_(NULL), @@ -1600,7 +1627,7 @@ { static const char* LUA_CALLBACK = "IncomingWorklistRequestFilter"; - PImpl::ServerContextLock lock(*that_.pimpl_); + PImpl::ServerContextReference lock(*that_.pimpl_); LuaScripting::Lock lua(lock.GetContext().GetLuaScripting()); if (!lua.GetLua().IsExistingFunction(LUA_CALLBACK)) @@ -2730,6 +2757,25 @@ } + void OrthancPlugins::SignalJobEvent(const JobEvent& event) + { + // job events are actually considered as changes inside plugins -> translate + switch (event.GetEventType()) + { + case JobEventType_Submitted: + SignalChangeInternal(OrthancPluginChangeType_JobSubmitted, OrthancPluginResourceType_None, event.GetJobId().c_str()); + break; + case JobEventType_Success: + SignalChangeInternal(OrthancPluginChangeType_JobSuccess, OrthancPluginResourceType_None, event.GetJobId().c_str()); + break; + case JobEventType_Failure: + SignalChangeInternal(OrthancPluginChangeType_JobFailure, OrthancPluginResourceType_None, event.GetJobId().c_str()); + break; + default: + throw OrthancException(ErrorCode_InternalError); + } + } + void OrthancPlugins::RegisterRestCallback(const void* parameters, bool mutualExclusion) @@ -2776,6 +2822,8 @@ void OrthancPlugins::RegisterOnChangeCallback(const void* parameters) { + boost::recursive_mutex::scoped_lock lock(pimpl_->changeCallbackMutex_); + const _OrthancPluginOnChangeCallback& p = *reinterpret_cast(parameters); @@ -3135,7 +3183,7 @@ std::string dicom; { - PImpl::ServerContextLock lock(*pimpl_); + PImpl::ServerContextReference lock(*pimpl_); lock.GetContext().ReadDicom(dicom, p.instanceId); } @@ -3176,7 +3224,7 @@ IHttpHandler* handler; { - PImpl::ServerContextLock lock(*pimpl_); + PImpl::ServerContextReference lock(*pimpl_); handler = &lock.GetContext().GetHttpHandler().RestrictToOrthancRestApi(!afterPlugins); } @@ -3209,7 +3257,7 @@ IHttpHandler* handler; { - PImpl::ServerContextLock lock(*pimpl_); + PImpl::ServerContextReference lock(*pimpl_); handler = &lock.GetContext().GetHttpHandler().RestrictToOrthancRestApi(!p.afterPlugins); } @@ -3233,7 +3281,7 @@ IHttpHandler* handler; { - PImpl::ServerContextLock lock(*pimpl_); + PImpl::ServerContextReference lock(*pimpl_); handler = &lock.GetContext().GetHttpHandler().RestrictToOrthancRestApi(!afterPlugins); } @@ -3260,7 +3308,7 @@ IHttpHandler* handler; { - PImpl::ServerContextLock lock(*pimpl_); + PImpl::ServerContextReference lock(*pimpl_); handler = &lock.GetContext().GetHttpHandler().RestrictToOrthancRestApi(!afterPlugins); } @@ -3319,7 +3367,7 @@ std::vector result; { - PImpl::ServerContextLock lock(*pimpl_); + PImpl::ServerContextReference lock(*pimpl_); lock.GetContext().GetIndex().LookupIdentifierExact(result, level, tag, p.argument); } @@ -3614,7 +3662,7 @@ std::unique_ptr decoded; { - PImpl::ServerContextLock lock(*pimpl_); + PImpl::ServerContextReference lock(*pimpl_); decoded.reset(lock.GetContext().DecodeDicomFrame(instance, p.frameIndex)); } @@ -3697,7 +3745,7 @@ case OrthancPluginImageFormat_Dicom: { - PImpl::ServerContextLock lock(*pimpl_); + PImpl::ServerContextReference lock(*pimpl_); image.reset(lock.GetContext().DecodeDicomFrame(p.data, p.size, 0)); break; } @@ -4022,7 +4070,7 @@ IHttpHandler* handler; { - PImpl::ServerContextLock lock(*pimpl_); + PImpl::ServerContextReference lock(*pimpl_); handler = &lock.GetContext().GetHttpHandler().RestrictToOrthancRestApi(!p.afterPlugins); } @@ -4244,7 +4292,7 @@ std::string content; { - PImpl::ServerContextLock lock(*pimpl_); + PImpl::ServerContextReference lock(*pimpl_); lock.GetContext().ReadDicom(content, p.instanceId); } @@ -4373,7 +4421,7 @@ case _OrthancPluginService_DecodeDicomImage: { - PImpl::ServerContextLock lock(*pimpl_); + PImpl::ServerContextReference lock(*pimpl_); result.reset(lock.GetContext().DecodeDicomFrame(p.constBuffer, p.bufferSize, p.frameIndex)); break; } @@ -4426,7 +4474,7 @@ std::string buffer; { - PImpl::ServerContextLock lock(*pimpl_); + PImpl::ServerContextReference lock(*pimpl_); lock.GetContext().ReadDicom(buffer, params.instanceId); } @@ -4443,7 +4491,7 @@ std::string buffer; { - PImpl::ServerContextLock lock(*pimpl_); + PImpl::ServerContextReference lock(*pimpl_); if (!lock.GetContext().ReadDicomUntilPixelData(buffer, params.instanceId)) { lock.GetContext().ReadDicom(buffer, params.instanceId); @@ -4461,7 +4509,7 @@ ValueRepresentation pixelDataVR = parsed->GuessPixelDataValueRepresentation(); { - PImpl::ServerContextLock lock(*pimpl_); + PImpl::ServerContextReference lock(*pimpl_); std::string s; int64_t revision; // unused @@ -4792,7 +4840,7 @@ { // TODO - Plugins can only access global properties of their // own Orthanc server (no access to the shared global properties) - PImpl::ServerContextLock lock(*pimpl_); + PImpl::ServerContextReference lock(*pimpl_); lock.GetContext().GetIndex().SetGlobalProperty(static_cast(p.property), false /* not shared */, p.value); return true; @@ -4809,7 +4857,7 @@ { // TODO - Plugins can only access global properties of their // own Orthanc server (no access to the shared global properties) - PImpl::ServerContextLock lock(*pimpl_); + PImpl::ServerContextReference lock(*pimpl_); result = lock.GetContext().GetIndex().GetGlobalProperty(static_cast(p.property), false /* not shared */, p.value); } @@ -5276,7 +5324,7 @@ std::string uuid; - PImpl::ServerContextLock lock(*pimpl_); + PImpl::ServerContextReference lock(*pimpl_); lock.GetContext().GetJobsEngine().GetRegistry().Submit (uuid, reinterpret_cast(p.job), p.priority); @@ -5299,7 +5347,7 @@ *reinterpret_cast(parameters); { - PImpl::ServerContextLock lock(*pimpl_); + PImpl::ServerContextReference lock(*pimpl_); lock.GetContext().GetMetricsRegistry().SetFloatValue(p.name, p.value, Plugins::Convert(p.type)); } @@ -5312,7 +5360,7 @@ *reinterpret_cast(parameters); { - PImpl::ServerContextLock lock(*pimpl_); + PImpl::ServerContextReference lock(*pimpl_); lock.GetContext().GetMetricsRegistry().SetIntegerValue(p.name, p.value, Plugins::Convert(p.type)); } @@ -5404,7 +5452,7 @@ bool success; { - PImpl::ServerContextLock lock(*pimpl_); + PImpl::ServerContextReference lock(*pimpl_); success = lock.GetContext().Transcode( transcoded, source, syntaxes, true /* allow new sop */); } diff -r 08177310e269 -r 2a7a113d791d OrthancServer/Plugins/Engine/OrthancPlugins.h --- a/OrthancServer/Plugins/Engine/OrthancPlugins.h Wed Nov 08 09:59:31 2023 +0100 +++ b/OrthancServer/Plugins/Engine/OrthancPlugins.h Wed Nov 08 16:10:46 2023 +0100 @@ -269,7 +269,9 @@ const void* parameters) ORTHANC_OVERRIDE; virtual void SignalChange(const ServerIndexChange& change) ORTHANC_OVERRIDE; - + + virtual void SignalJobEvent(const JobEvent& event) ORTHANC_OVERRIDE; + virtual void SignalStoredInstance(const std::string& instanceId, const DicomInstanceToStore& instance, const Json::Value& simplifiedTags) ORTHANC_OVERRIDE; @@ -319,21 +321,6 @@ SignalChangeInternal(OrthancPluginChangeType_OrthancStopped, OrthancPluginResourceType_None, NULL); } - void SignalJobSubmitted(const std::string& jobId) - { - SignalChangeInternal(OrthancPluginChangeType_JobSubmitted, OrthancPluginResourceType_None, jobId.c_str()); - } - - void SignalJobSuccess(const std::string& jobId) - { - SignalChangeInternal(OrthancPluginChangeType_JobSuccess, OrthancPluginResourceType_None, jobId.c_str()); - } - - void SignalJobFailure(const std::string& jobId) - { - SignalChangeInternal(OrthancPluginChangeType_JobFailure, OrthancPluginResourceType_None, jobId.c_str()); - } - void SignalUpdatedPeers() { SignalChangeInternal(OrthancPluginChangeType_UpdatedPeers, OrthancPluginResourceType_None, NULL); diff -r 08177310e269 -r 2a7a113d791d OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp --- a/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp Wed Nov 08 09:59:31 2023 +0100 +++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp Wed Nov 08 16:10:46 2023 +0100 @@ -79,6 +79,10 @@ } } + void ResetGlobalContext() + { + globalContext_ = NULL; + } bool HasGlobalContext() { diff -r 08177310e269 -r 2a7a113d791d OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h --- a/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h Wed Nov 08 09:59:31 2023 +0100 +++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h Wed Nov 08 16:10:46 2023 +0100 @@ -137,6 +137,8 @@ void SetGlobalContext(OrthancPluginContext* context); + void ResetGlobalContext(); + bool HasGlobalContext(); OrthancPluginContext* GetGlobalContext(); diff -r 08177310e269 -r 2a7a113d791d OrthancServer/Resources/Configuration.json --- a/OrthancServer/Resources/Configuration.json Wed Nov 08 09:59:31 2023 +0100 +++ b/OrthancServer/Resources/Configuration.json Wed Nov 08 16:10:46 2023 +0100 @@ -125,7 +125,12 @@ // Enable HTTP compression to improve network bandwidth utilization, // at the expense of more computations on the server. Orthanc // supports the "gzip" and "deflate" HTTP encodings. - "HttpCompressionEnabled" : true, + // When working on a LAN or on localhost, you should typically set + // this configuration to false while when working on low-bandwidth, + // you should set it to true. + // Note in versions up to 1.12.1, the default value was "true" and is + // "false" since 1.12.2. + "HttpCompressionEnabled" : false, // Enable the publication of the content of the Orthanc server as a // WebDAV share (new in Orthanc 1.8.0). On the localhost, the WebDAV @@ -244,7 +249,7 @@ "SslVerifyPeers" : false, // Path to a file containing the concatenation of the client SSL - // certificate(s) that are trusted to verify the identify of remote + // certificate(s) that are trusted to verify the identity of remote // HTTP clients. The individual certificate(s) must be stored in the // PEM format. This option is only meaningful if "SslVerifyPeers" // is true. diff -r 08177310e269 -r 2a7a113d791d OrthancServer/Sources/IServerListener.h --- a/OrthancServer/Sources/IServerListener.h Wed Nov 08 09:59:31 2023 +0100 +++ b/OrthancServer/Sources/IServerListener.h Wed Nov 08 16:10:46 2023 +0100 @@ -24,6 +24,7 @@ #include "DicomInstanceToStore.h" #include "ServerIndexChange.h" +#include "JobEvent.h" #include @@ -42,6 +43,8 @@ virtual void SignalChange(const ServerIndexChange& change) = 0; + virtual void SignalJobEvent(const JobEvent& event) = 0; + virtual bool FilterIncomingInstance(const DicomInstanceToStore& instance, const Json::Value& simplified) = 0; diff -r 08177310e269 -r 2a7a113d791d OrthancServer/Sources/JobEvent.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/JobEvent.h Wed Nov 08 16:10:46 2023 +0100 @@ -0,0 +1,77 @@ +/** + * 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) 2021-2023 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 . + **/ + + +#pragma once + +#include "ServerEnumerations.h" +#include "../../OrthancFramework/Sources/IDynamicObject.h" +#include "../../OrthancFramework/Sources/SystemToolbox.h" + +#include +#include + +namespace Orthanc +{ + enum JobEventType + { + JobEventType_Failure, + JobEventType_Submitted, + JobEventType_Success + }; + + + struct JobEvent : public IDynamicObject + { + private: + JobEventType eventType_; + std::string jobId_; + + public: + JobEvent(JobEventType eventType, + const std::string& jobId) : + eventType_(eventType), + jobId_(jobId) + { + } + + JobEvent(const JobEvent& other) + : eventType_(other.eventType_), + jobId_(other.jobId_) + { + } + + // JobEvent* Clone() const + // { + // return new JobEvent(*this); + // } + + JobEventType GetEventType() const + { + return eventType_; + } + + const std::string& GetJobId() const + { + return jobId_; + } + }; +} diff -r 08177310e269 -r 2a7a113d791d OrthancServer/Sources/LuaScripting.cpp --- a/OrthancServer/Sources/LuaScripting.cpp Wed Nov 08 09:59:31 2023 +0100 +++ b/OrthancServer/Sources/LuaScripting.cpp Wed Nov 08 16:10:46 2023 +0100 @@ -239,25 +239,14 @@ }; - class LuaScripting::JobEvent : public LuaScripting::IEvent + class LuaScripting::LuaJobEvent : public LuaScripting::IEvent { - public: - enum Type - { - Type_Failure, - Type_Submitted, - Type_Success - }; - private: - Type type_; - std::string jobId_; + JobEvent event_; public: - JobEvent(Type type, - const std::string& jobId) : - type_(type), - jobId_(jobId) + LuaJobEvent(const JobEvent& event) : + event_(event) { } @@ -265,17 +254,17 @@ { std::string functionName; - switch (type_) + switch (event_.GetEventType()) { - case Type_Failure: + case JobEventType_Failure: functionName = "OnJobFailure"; break; - case Type_Submitted: + case JobEventType_Submitted: functionName = "OnJobSubmitted"; break; - case Type_Success: + case JobEventType_Success: functionName = "OnJobSuccess"; break; @@ -289,7 +278,7 @@ if (lock.GetLua().IsExistingFunction(functionName.c_str())) { LuaFunctionCall call(lock.GetLua(), functionName.c_str()); - call.PushString(jobId_); + call.PushString(event_.GetJobId()); call.Execute(); } } @@ -1056,20 +1045,9 @@ } - void LuaScripting::SignalJobSubmitted(const std::string& jobId) - { - pendingEvents_.Enqueue(new JobEvent(JobEvent::Type_Submitted, jobId)); - } - - - void LuaScripting::SignalJobSuccess(const std::string& jobId) + void LuaScripting::SignalJobEvent(const JobEvent& event) { - pendingEvents_.Enqueue(new JobEvent(JobEvent::Type_Success, jobId)); - } - - - void LuaScripting::SignalJobFailure(const std::string& jobId) - { - pendingEvents_.Enqueue(new JobEvent(JobEvent::Type_Failure, jobId)); + // Lua has its own event thread and queue to dissociate it completely from the main JobEventsThread + pendingEvents_.Enqueue(new LuaJobEvent(event)); } } diff -r 08177310e269 -r 2a7a113d791d OrthancServer/Sources/LuaScripting.h --- a/OrthancServer/Sources/LuaScripting.h Wed Nov 08 09:59:31 2023 +0100 +++ b/OrthancServer/Sources/LuaScripting.h Wed Nov 08 16:10:46 2023 +0100 @@ -24,6 +24,7 @@ #include "DicomInstanceToStore.h" #include "ServerIndexChange.h" +#include "JobEvent.h" #include "ServerJobs/LuaJobManager.h" #include "../../OrthancFramework/Sources/MultiThreading/SharedMessageQueue.h" @@ -47,7 +48,7 @@ class IEvent; class OnStoredInstanceEvent; class StableResourceEvent; - class JobEvent; + class LuaJobEvent; class DeleteEvent; class UpdateEvent; @@ -128,11 +129,7 @@ void Execute(const std::string& command); - void SignalJobSubmitted(const std::string& jobId); - - void SignalJobSuccess(const std::string& jobId); - - void SignalJobFailure(const std::string& jobId); + void SignalJobEvent(const JobEvent& event); TimeoutDicomConnectionManager& GetDicomConnectionManager() { diff -r 08177310e269 -r 2a7a113d791d OrthancServer/Sources/ServerContext.cpp --- a/OrthancServer/Sources/ServerContext.cpp Wed Nov 08 09:59:31 2023 +0100 +++ b/OrthancServer/Sources/ServerContext.cpp Wed Nov 08 16:10:46 2023 +0100 @@ -164,7 +164,7 @@ } catch (...) { - throw OrthancException(ErrorCode_InternalError); + throw OrthancException(ErrorCode_InternalError, "Error while signaling a change"); } } catch (OrthancException& e) @@ -179,6 +179,48 @@ } + void ServerContext::JobEventsThread(ServerContext* that, + unsigned int sleepDelay) + { + while (!that->done_) + { + std::unique_ptr obj(that->pendingJobEvents_.Dequeue(sleepDelay)); + + if (obj.get() != NULL) + { + const JobEvent& event = dynamic_cast(*obj.get()); + + boost::shared_lock lock(that->listenersMutex_); + for (ServerListeners::iterator it = that->listeners_.begin(); + it != that->listeners_.end(); ++it) + { + try + { + try + { + it->GetListener().SignalJobEvent(event); + } + catch (std::bad_alloc&) + { + LOG(ERROR) << "Not enough memory while signaling a job event"; + } + catch (...) + { + throw OrthancException(ErrorCode_InternalError, "Error while signaling a job event"); + } + } + catch (OrthancException& e) + { + LOG(ERROR) << "Error in the " << it->GetDescription() + << " callback while signaling a job event: " << e.What() + << " (code " << e.GetErrorCode() << ")"; + } + } + } + } + } + + void ServerContext::SaveJobsThread(ServerContext* that, unsigned int sleepDelay) { @@ -206,42 +248,21 @@ void ServerContext::SignalJobSubmitted(const std::string& jobId) { haveJobsChanged_ = true; - mainLua_.SignalJobSubmitted(jobId); - -#if ORTHANC_ENABLE_PLUGINS == 1 - if (HasPlugins()) - { - GetPlugins().SignalJobSubmitted(jobId); - } -#endif + pendingJobEvents_.Enqueue(new JobEvent(JobEventType_Submitted, jobId)); } void ServerContext::SignalJobSuccess(const std::string& jobId) { haveJobsChanged_ = true; - mainLua_.SignalJobSuccess(jobId); - -#if ORTHANC_ENABLE_PLUGINS == 1 - if (HasPlugins()) - { - GetPlugins().SignalJobSuccess(jobId); - } -#endif + pendingJobEvents_.Enqueue(new JobEvent(JobEventType_Success, jobId)); } void ServerContext::SignalJobFailure(const std::string& jobId) { haveJobsChanged_ = true; - mainLua_.SignalJobFailure(jobId); - -#if ORTHANC_ENABLE_PLUGINS == 1 - if (HasPlugins()) - { - GetPlugins().SignalJobFailure(jobId); - } -#endif + pendingJobEvents_.Enqueue(new JobEvent(JobEventType_Failure, jobId)); } @@ -449,6 +470,7 @@ listeners_.push_back(ServerListener(luaListener_, "Lua")); changeThread_ = boost::thread(ChangeThread, this, (unitTesting ? 20 : 100)); + jobEventsThread_ = boost::thread(JobEventsThread, this, (unitTesting ? 20 : 100)); #if HAVE_MALLOC_TRIM == 1 LOG(INFO) << "Starting memory trimming thread at 30 seconds interval"; @@ -494,6 +516,11 @@ changeThread_.join(); } + if (jobEventsThread_.joinable()) + { + jobEventsThread_.join(); + } + if (saveJobsThread_.joinable()) { saveJobsThread_.join(); diff -r 08177310e269 -r 2a7a113d791d OrthancServer/Sources/ServerContext.h --- a/OrthancServer/Sources/ServerContext.h Wed Nov 08 09:59:31 2023 +0100 +++ b/OrthancServer/Sources/ServerContext.h Wed Nov 08 16:10:46 2023 +0100 @@ -139,6 +139,11 @@ context_.mainLua_.SignalChange(change); } + virtual void SignalJobEvent(const JobEvent& event) ORTHANC_OVERRIDE + { + context_.mainLua_.SignalJobEvent(event); + } + virtual bool FilterIncomingInstance(const DicomInstanceToStore& instance, const Json::Value& simplified) ORTHANC_OVERRIDE { @@ -184,6 +189,9 @@ static void ChangeThread(ServerContext* that, unsigned int sleepDelay); + static void JobEventsThread(ServerContext* that, + unsigned int sleepDelay); + static void SaveJobsThread(ServerContext* that, unsigned int sleepDelay); @@ -233,7 +241,9 @@ bool haveJobsChanged_; bool isJobsEngineUnserialized_; SharedMessageQueue pendingChanges_; + SharedMessageQueue pendingJobEvents_; boost::thread changeThread_; + boost::thread jobEventsThread_; boost::thread saveJobsThread_; boost::thread memoryTrimmingThread_; diff -r 08177310e269 -r 2a7a113d791d OrthancServer/Sources/main.cpp --- a/OrthancServer/Sources/main.cpp Wed Nov 08 09:59:31 2023 +0100 +++ b/OrthancServer/Sources/main.cpp Wed Nov 08 16:10:46 2023 +0100 @@ -1031,7 +1031,7 @@ httpServer.SetRemoteAccessAllowed(lock.GetConfiguration().GetBooleanParameter("RemoteAccessAllowed", false)); httpServer.SetKeepAliveEnabled(lock.GetConfiguration().GetBooleanParameter("KeepAlive", defaultKeepAlive)); httpServer.SetKeepAliveTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("KeepAliveTimeout", 1)); - httpServer.SetHttpCompressionEnabled(lock.GetConfiguration().GetBooleanParameter("HttpCompressionEnabled", true)); + httpServer.SetHttpCompressionEnabled(lock.GetConfiguration().GetBooleanParameter("HttpCompressionEnabled", false)); httpServer.SetTcpNoDelay(lock.GetConfiguration().GetBooleanParameter("TcpNoDelay", true)); httpServer.SetRequestTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("HttpRequestTimeout", 30)); @@ -1606,6 +1606,7 @@ lock.GetConfiguration().LoadModalitiesAndPeers(); } + // this function exits only when Orthanc stops or resets return ConfigureHttpHandler(context, plugins, loadJobsFromDatabase); } } diff -r 08177310e269 -r 2a7a113d791d TODO --- a/TODO Wed Nov 08 09:59:31 2023 +0100 +++ b/TODO Wed Nov 08 16:10:46 2023 +0100 @@ -54,6 +54,8 @@ - for job durations (+ have one histogram for each job) - for HTTP request handling - ... +* Investigate if one could fix KeepAlive race conditions: + https://discourse.orthanc-server.org/t/socket-hangup-with-rest-api/4023/3 ============================ Documentation (Orthanc Book) @@ -264,6 +266,9 @@ * Add a primitive for user authentication (to generate 401 HTTP status, whereas the "RegisterIncomingHttpRequestFilter()" can only generate 403 HTTP status) https://groups.google.com/g/orthanc-users/c/ymtaAmgSs6Q/m/PqVBactQAQAJ +* Add an index on the UUID column in the DelayedDeletion plugin: + https://discourse.orthanc-server.org/t/delayeddeletion-improvement-unique-index-on-pending-uuid-column/4032 + ---------------- Ideas of plugins