Mercurial > hg > orthanc
changeset 6612:81f91290f91e limited-memory
wip: limit the memory used by the HTTP server when multiple HTTP clients upload large files
| author | Alain Mazy <am@orthanc.team> |
|---|---|
| date | Wed, 04 Mar 2026 16:01:39 +0100 |
| parents | 2c557f38484a |
| children | e19301aba089 |
| files | NEWS OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake OrthancFramework/Sources/FileBuffer.cpp OrthancFramework/Sources/FileBuffer.h OrthancFramework/Sources/HttpServer/HttpServer.cpp OrthancFramework/Sources/HttpServer/HttpServer.h OrthancFramework/Sources/MemoryManagedString.cpp OrthancFramework/Sources/MemoryManagedString.h OrthancFramework/Sources/SystemToolbox.cpp OrthancFramework/Sources/SystemToolbox.h OrthancFramework/Sources/TemporaryFile.cpp OrthancFramework/Sources/TemporaryFile.h OrthancServer/Sources/main.cpp |
| diffstat | 13 files changed, 326 insertions(+), 11 deletions(-) [+] |
line wrap: on
line diff
--- a/NEWS Wed Mar 04 14:12:36 2026 +0100 +++ b/NEWS Wed Mar 04 16:01:39 2026 +0100 @@ -4,6 +4,8 @@ General ------- +* Experimental WIP: try to limit the memory used by the HTTP server when multiple HTTP clients upload + large DICOM files at the same time. Right now, the limit is hardcoded to 8GB. Check the TODO-MEM in the code. REST API --------
--- a/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake Wed Mar 04 14:12:36 2026 +0100 +++ b/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake Wed Mar 04 16:01:39 2026 +0100 @@ -178,6 +178,7 @@ ${CMAKE_CURRENT_LIST_DIR}/../../Sources/HttpServer/StringMatcher.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/Logging.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/MallocMemoryBuffer.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../Sources/MemoryManagedString.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/OrthancException.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/OrthancFramework.cpp ${CMAKE_CURRENT_LIST_DIR}/../../Sources/RestApi/RestApiHierarchy.cpp
--- a/OrthancFramework/Sources/FileBuffer.cpp Wed Mar 04 14:12:36 2026 +0100 +++ b/OrthancFramework/Sources/FileBuffer.cpp Wed Mar 04 16:01:39 2026 +0100 @@ -88,6 +88,17 @@ file_.Read(target); } + + void Read(MemoryManagedString& target) + { + if (isWriting_) + { + stream_.close(); + isWriting_ = false; + } + + file_.Read(target); + } }; @@ -110,4 +121,11 @@ assert(pimpl_.get() != NULL); pimpl_->Read(target); } + + + void FileBuffer::Read(MemoryManagedString& target) + { + assert(pimpl_.get() != NULL); + pimpl_->Read(target); + } }
--- a/OrthancFramework/Sources/FileBuffer.h Wed Mar 04 14:12:36 2026 +0100 +++ b/OrthancFramework/Sources/FileBuffer.h Wed Mar 04 16:01:39 2026 +0100 @@ -36,7 +36,7 @@ #include <boost/noncopyable.hpp> #include <boost/shared_ptr.hpp> - +#include "MemoryManagedString.h" namespace Orthanc { @@ -53,5 +53,7 @@ size_t size); void Read(std::string& target); + + void Read(MemoryManagedString& target); }; }
--- a/OrthancFramework/Sources/HttpServer/HttpServer.cpp Wed Mar 04 14:12:36 2026 +0100 +++ b/OrthancFramework/Sources/HttpServer/HttpServer.cpp Wed Mar 04 16:01:39 2026 +0100 @@ -30,6 +30,7 @@ #include "../ChunkedBuffer.h" #include "../FileBuffer.h" #include "../Logging.h" +#include "../MemoryManagedString.h" #include "../OrthancException.h" #include "../TemporaryFile.h" #include "../SystemToolbox.h" @@ -480,7 +481,7 @@ const std::string& username, const UriComponents& uri, const std::map<std::string, std::string>& headers, - const std::string& body, + const MemoryManagedString& body, const std::string& boundary, const std::string& authenticationPayload) { @@ -488,12 +489,12 @@ MultipartStreamReader reader(boundary); reader.SetHandler(handler); - reader.AddChunk(body); + reader.AddChunk(body.c_str(), body.size()); reader.CloseStream(); } - static PostDataStatus ReadBodyWithContentLength(std::string& body, + static PostDataStatus ReadBodyWithContentLength(MemoryManagedString& body, struct mg_connection *connection, const std::string& contentLength) { @@ -533,7 +534,7 @@ } - static PostDataStatus ReadBodyWithoutContentLength(std::string& body, + static PostDataStatus ReadBodyWithoutContentLength(MemoryManagedString& body, struct mg_connection *connection) { // Store the individual chunks in a temporary file, then read it @@ -565,7 +566,7 @@ } - static PostDataStatus ReadBodyToString(std::string& body, + static PostDataStatus ReadBodyToString(MemoryManagedString& body, struct mg_connection *connection, const HttpToolbox::Arguments& headers) { @@ -593,7 +594,7 @@ if (contentLength != headers.end()) { // "Content-Length" is available - std::string body; + MemoryManagedString body; PostDataStatus status = ReadBodyWithContentLength(body, connection, contentLength->second); if (status == PostDataStatus_Success && @@ -1039,10 +1040,11 @@ else if (method == "PUT") { #if CIVETWEB_HAS_WEBDAV_WRITING == 1 - std::string body; + MemoryManagedString body; if (ReadBodyToString(body, connection, headers) == PostDataStatus_Success) { - if (bucket->second->StoreFile(body, path)) + std::string bodyStr(body.begin(), body.end()); // TODO-MEM: avoid this copy and use a MemoryManagedString in StoreFile too + if (bucket->second->StoreFile(bodyStr, path)) { //output.SendStatus(HttpStatus_200_Ok); output.SendStatus(HttpStatus_201_Created); @@ -1425,8 +1427,8 @@ // Extract the body of the request for PUT and POST, or process // the body as a stream + MemoryManagedString body; - std::string body; if (method == HttpMethod_Post || method == HttpMethod_Put) {
--- a/OrthancFramework/Sources/HttpServer/HttpServer.h Wed Mar 04 14:12:36 2026 +0100 +++ b/OrthancFramework/Sources/HttpServer/HttpServer.h Wed Mar 04 16:01:39 2026 +0100 @@ -51,6 +51,7 @@ #include "IIncomingHttpRequestFilter.h" #include "../MetricsRegistry.h" +#include "../MemoryManagedString.h" #include <list> #include <map> @@ -234,7 +235,7 @@ const std::string& username, const UriComponents& uri, const std::map<std::string, std::string>& headers, - const std::string& body, + const MemoryManagedString& body, const std::string& boundary, const std::string& authenticationPayload);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancFramework/Sources/MemoryManagedString.cpp Wed Mar 04 16:01:39 2026 +0100 @@ -0,0 +1,85 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/>. + **/ + + +#include "MemoryManagedString.h" + +#include "OrthancException.h" +#include "MultiThreading/Semaphore.h" +#include <boost/lexical_cast.hpp> +#include "Logging.h" + + +namespace Orthanc +{ + static std::unique_ptr<Semaphore> availableMemorySemaphore_; + static size_t maximumMemorySize_ = 0; + + void LimitedMemoryAllocator::Initialize(size_t maximumMemorySize) + { + if (availableMemorySemaphore_.get() != NULL) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + maximumMemorySize_ = maximumMemorySize; + availableMemorySemaphore_.reset(new Semaphore(maximumMemorySize)); + } + + void* LimitedMemoryAllocator::Allocate(size_t elemSize, size_t numElem) + { + if (availableMemorySemaphore_.get() == NULL) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + size_t requestedSize = numElem * elemSize; + + if (requestedSize > maximumMemorySize_) + { + throw OrthancException(ErrorCode_NotEnoughMemory, "Trying to allocate a buffer ( " + boost::lexical_cast<std::string>(requestedSize) + " bytes) that is larger than the maximum memory size ( " + boost::lexical_cast<std::string>(maximumMemorySize_) + " bytes)"); + } + + // wait until there is enough memory available + availableMemorySemaphore_->Acquire(requestedSize); + + // LOG(TRACE) << "Reserved " << requestedSize << " bytes in memory. Remaining size: " << availableMemorySemaphore_->GetAvailableResourcesCount() << " bytes"; + + void* p = malloc(numElem * elemSize); + if (!p) + { + throw std::bad_alloc(); // the allocation might still fail since we don't track the whole memory + } + return p; + } + + void LimitedMemoryAllocator::Deallocate(void* p, size_t elemSize, size_t numElem) + { + free(p); + availableMemorySemaphore_->Release(elemSize * numElem); + + // LOG(TRACE) << "Released " << (elemSize * numElem) << " bytes in memory. Remaining size: " << availableMemorySemaphore_->GetAvailableResourcesCount() << " bytes"; + } + + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancFramework/Sources/MemoryManagedString.h Wed Mar 04 16:01:39 2026 +0100 @@ -0,0 +1,127 @@ +/** + * 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 Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include <memory> +#include <cstdlib> + + +namespace Orthanc +{ + template<typename T> class LimitedStdAllocator; + + // TODO-MEM: move this to a dedicated file + class LimitedMemoryAllocator + { + public: + static void Initialize(size_t maximumMemorySize); + protected: + static void* Allocate(size_t elemSize, size_t numElem); + + static void Deallocate(void* p, size_t elemSize, size_t numElem); + + template<typename T>friend class LimitedStdAllocator; + }; + + + template<typename T> + class LimitedStdAllocator + { + public: + typedef T value_type; + typedef T* pointer; + typedef const T* const_pointer; + typedef T& reference; + typedef const T& const_reference; + typedef std::size_t size_type; + typedef std::ptrdiff_t difference_type; + + // Rebind to allow allocator for other types + template<typename U> + struct rebind + { + typedef LimitedStdAllocator<U> other; + }; + + LimitedStdAllocator() + { + } + + template<typename U> + LimitedStdAllocator(const LimitedStdAllocator<U>&) + { + } + + pointer allocate(size_type num, const void* hint = 0) + { + return static_cast<pointer>(LimitedMemoryAllocator::Allocate(sizeof(T), num)); + } + + void deallocate(pointer p, size_type num) + { + LimitedMemoryAllocator::Deallocate(p, sizeof(T), num); + } + + void construct(pointer p, const T& val) + { + new (p) T(val); + } + + void destroy(pointer p) + { + p->~T(); + } + + pointer address(reference x) const + { + return &x; + } + + const_pointer address(const_reference x) const + { + return &x; + } + + size_type max_size() const + { + return size_type(-1) / sizeof(T); + } + }; + + + // // Equality check + // template<typename T1, typename T2> + // bool operator==(const LimitedStdAllocator<T1>&, const LimitedStdAllocator<T2>&) + // { + // return true; + // } + + // template<typename T1, typename T2> + // bool operator!=(const LimitedStdAllocator<T1>&, const LimitedStdAllocator<T2>&) throw() { + // return false; + // } + + typedef std::basic_string<char, std::char_traits<char>, LimitedStdAllocator<char> > MemoryManagedString; +}
--- a/OrthancFramework/Sources/SystemToolbox.cpp Wed Mar 04 14:12:36 2026 +0100 +++ b/OrthancFramework/Sources/SystemToolbox.cpp Wed Mar 04 16:01:39 2026 +0100 @@ -267,6 +267,52 @@ } + void SystemToolbox::ReadFile(MemoryManagedString& content, + const boost::filesystem::path& path, + bool log) + { + if (!IsRegularFile(path)) + { + throw OrthancException(ErrorCode_RegularFileExpected, + "The path does not point to a regular file: " + PathToUtf8(path), log); + } + + try + { + boost::filesystem::ifstream f; + f.open(path, std::ifstream::in | std::ifstream::binary); + if (!f.good()) + { + throw OrthancException(ErrorCode_InexistentFile, "File not found: " + PathToUtf8(path), log); + } + + std::streamsize size = GetStreamSize(f); + content.resize(static_cast<size_t>(size)); + + if (static_cast<std::streamsize>(content.size()) != size) + { + throw OrthancException(ErrorCode_InternalError, + "Reading a file that is too large for a 32bit architecture"); + } + + if (size != 0) + { + f.read(&content[0], size); + } + + f.close(); + } + catch (boost::filesystem::filesystem_error&) + { + throw OrthancException(ErrorCode_InexistentFile); + } + catch (...) // To catch "std::system_error&" in C++11 + { + throw OrthancException(ErrorCode_InexistentFile); + } + } + + bool SystemToolbox::ReadHeader(std::string& header, const boost::filesystem::path& path, size_t headerSize)
--- a/OrthancFramework/Sources/SystemToolbox.h Wed Mar 04 14:12:36 2026 +0100 +++ b/OrthancFramework/Sources/SystemToolbox.h Wed Mar 04 16:01:39 2026 +0100 @@ -45,6 +45,7 @@ #include <string> #include <stdint.h> #include <boost/filesystem.hpp> +#include "MemoryManagedString.h" // Note: The use of "boost::filesystem::path" is mandatory to handle // non ASCII-only path on Windows @@ -70,6 +71,16 @@ ReadFile(content, path, true /* log */); } + static void ReadFile(MemoryManagedString& content, + const boost::filesystem::path& path, + bool log); + + static inline void ReadFile(MemoryManagedString& content, + const boost::filesystem::path& path) + { + ReadFile(content, path, true /* log */); + } + static bool ReadHeader(std::string& header, const boost::filesystem::path& path, size_t headerSize);
--- a/OrthancFramework/Sources/TemporaryFile.cpp Wed Mar 04 14:12:36 2026 +0100 +++ b/OrthancFramework/Sources/TemporaryFile.cpp Wed Mar 04 16:01:39 2026 +0100 @@ -129,6 +129,21 @@ } + void TemporaryFile::Read(MemoryManagedString& content) const + { + try + { + SystemToolbox::ReadFile(content, path_); + } + catch (OrthancException& e) + { + throw OrthancException(e.GetErrorCode(), + "Can't read temporary file \"" + SystemToolbox::PathToUtf8(path_) + + "\": Another process has corrupted the temporary directory"); + } + } + + void TemporaryFile::Touch() { std::string empty;
--- a/OrthancFramework/Sources/TemporaryFile.h Wed Mar 04 14:12:36 2026 +0100 +++ b/OrthancFramework/Sources/TemporaryFile.h Wed Mar 04 16:01:39 2026 +0100 @@ -38,6 +38,7 @@ #include <boost/filesystem.hpp> #include <stdint.h> #include <string> +#include "MemoryManagedString.h" namespace Orthanc { @@ -60,6 +61,8 @@ void Read(std::string& content) const; + void Read(MemoryManagedString& content) const; + void Touch(); uint64_t GetFileSize() const;
--- a/OrthancServer/Sources/main.cpp Wed Mar 04 14:12:36 2026 +0100 +++ b/OrthancServer/Sources/main.cpp Wed Mar 04 16:01:39 2026 +0100 @@ -1949,6 +1949,8 @@ Logging::SetCurrentThreadName("MAIN"); SetGlobalVerbosity(Verbosity_Default); + LimitedMemoryAllocator::Initialize(8ul * 1024ul * 1024ul * 1024ul); // TODO-MEM: get this value from the configuration file + bool upgradeDatabase = false; bool loadJobsFromDatabase = true; boost::filesystem::path configurationFile;
