changeset 5061:e95fadefeb72

new MaximumStorageMode configuration
author Alain Mazy <am@osimis.io>
date Tue, 09 Aug 2022 17:57:38 +0200
parents e69a3ff39bc5
children caed453042db
files NEWS OrthancServer/Resources/Configuration.json OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp OrthancServer/Sources/Database/StatelessDatabaseOperations.h OrthancServer/Sources/ServerEnumerations.cpp OrthancServer/Sources/ServerEnumerations.h OrthancServer/Sources/ServerIndex.cpp OrthancServer/Sources/ServerIndex.h OrthancServer/Sources/main.cpp
diffstat 9 files changed, 150 insertions(+), 32 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Mon Aug 08 12:42:48 2022 +0200
+++ b/NEWS	Tue Aug 09 17:57:38 2022 +0200
@@ -5,6 +5,9 @@
 -------
 
 * Added support for RGBA64 images in tools/create-dicom and /preview
+* New configuration "MaximumStorageMode" to choose between recyling of
+  old patients (default behavior) and rejection of new incoming data when
+  the MaximumStorageSize has been reached.
 
 Bug Fixes
 ---------
--- a/OrthancServer/Resources/Configuration.json	Mon Aug 08 12:42:48 2022 +0200
+++ b/OrthancServer/Resources/Configuration.json	Tue Aug 09 17:57:38 2022 +0200
@@ -41,6 +41,16 @@
   // of patients)
   "MaximumPatientCount" : 0,
 
+  // Action to take when the maximum storage is reached.
+  // By default, the patients are recycled ("Recycle" mode).
+  // In "Reject" mode, the sender will receive a 0xA700 DIMSE status code
+  // if the instance was sent through C-Store, a 507 HTTP status code
+  // if using the Rest API and a 0xA700 Failure reason when using
+  // DicomWeb Stow-RS 
+  // Allowed values: "Recycle", "Reject"
+  // (new in Orthanc 1.11.2)
+  "MaximumStorageMode" : "Recycle",
+
   // Maximum size of the storage cache in MB.  The storage cache
   // is stored in RAM and contains a copy of recently accessed
   // files (written or read).  A value of "0" indicates the cache
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Mon Aug 08 12:42:48 2022 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Tue Aug 09 17:57:38 2022 +0200
@@ -2773,10 +2773,8 @@
   }
 
 
-  static bool IsRecyclingNeeded(IDatabaseWrapper::ITransaction& transaction,
-                                uint64_t maximumStorageSize,
-                                unsigned int maximumPatients,
-                                uint64_t addedInstanceSize)
+  bool StatelessDatabaseOperations::ReadWriteTransaction::HasReachedMaxStorageSize(uint64_t maximumStorageSize,
+                                                                                   uint64_t addedInstanceSize)
   {
     if (maximumStorageSize != 0)
     {
@@ -2788,24 +2786,35 @@
                                boost::lexical_cast<std::string>(maximumStorageSize));
       }
       
-      if (transaction.IsDiskSizeAbove(maximumStorageSize - addedInstanceSize))
-      {
-        return true;
-      }
-    }
-
-    if (maximumPatients != 0)
-    {
-      uint64_t patientCount = transaction.GetResourcesCount(ResourceType_Patient);
-      if (patientCount > maximumPatients)
+      if (transaction_.IsDiskSizeAbove(maximumStorageSize - addedInstanceSize))
       {
         return true;
       }
     }
 
     return false;
+  }                                                                           
+
+  bool StatelessDatabaseOperations::ReadWriteTransaction::HasReachedMaxPatientCount(unsigned int maximumPatientCount,
+                                                                                   const std::string& patientId)
+  {
+    if (maximumPatientCount != 0)
+    {
+      uint64_t patientCount = transaction_.GetResourcesCount(ResourceType_Patient);  // at this time, the new patient has already been added (as part of the transaction)
+      return patientCount > maximumPatientCount;
+    }
+
+    return false;
   }
   
+  bool StatelessDatabaseOperations::ReadWriteTransaction::IsRecyclingNeeded(uint64_t maximumStorageSize,
+                                                                            unsigned int maximumPatients,
+                                                                            uint64_t addedInstanceSize,
+                                                                            const std::string& newPatientId)
+  {
+    return HasReachedMaxStorageSize(maximumStorageSize, addedInstanceSize)
+      || HasReachedMaxPatientCount(maximumPatients, newPatientId);
+  }
 
   void StatelessDatabaseOperations::ReadWriteTransaction::Recycle(uint64_t maximumStorageSize,
                                                                   unsigned int maximumPatients,
@@ -2814,7 +2823,7 @@
   {
     // TODO - Performance: Avoid calls to "IsRecyclingNeeded()"
     
-    if (IsRecyclingNeeded(transaction_, maximumStorageSize, maximumPatients, addedInstanceSize))
+    if (IsRecyclingNeeded(maximumStorageSize, maximumPatients, addedInstanceSize, newPatientId))
     {
       // Check whether other DICOM instances from this patient are
       // already stored
@@ -2854,7 +2863,7 @@
         LOG(TRACE) << "Recycling one patient";
         transaction_.DeleteResource(patientToRecycle);
 
-        if (!IsRecyclingNeeded(transaction_, maximumStorageSize, maximumPatients, addedInstanceSize))
+        if (!IsRecyclingNeeded(maximumStorageSize, maximumPatients, addedInstanceSize, newPatientId))
         {
           // OK, we're done
           return;
@@ -2864,14 +2873,15 @@
   }
 
 
-  void StatelessDatabaseOperations::StandaloneRecycling(uint64_t maximumStorageSize,
+  void StatelessDatabaseOperations::StandaloneRecycling(MaxStorageMode maximumStorageMode,
+                                                        uint64_t maximumStorageSize,
                                                         unsigned int maximumPatientCount)
   {
     class Operations : public IReadWriteOperations
     {
     private:
-      uint64_t      maximumStorageSize_;
-      unsigned int  maximumPatientCount_;
+      uint64_t        maximumStorageSize_;
+      unsigned int    maximumPatientCount_;
       
     public:
       Operations(uint64_t maximumStorageSize,
@@ -2887,8 +2897,8 @@
       }
     };
 
-    if (maximumStorageSize != 0 ||
-        maximumPatientCount != 0)
+    if (maximumStorageMode == MaxStorageMode_Recycle 
+      && (maximumStorageSize != 0 || maximumPatientCount != 0))
     {
       Operations operations(maximumStorageSize, maximumPatientCount);
       Apply(operations);
@@ -2906,6 +2916,7 @@
                                                  DicomTransferSyntax transferSyntax,
                                                  bool hasPixelDataOffset,
                                                  uint64_t pixelDataOffset,
+                                                 MaxStorageMode maximumStorageMode,
                                                  uint64_t maximumStorageSize,
                                                  unsigned int maximumPatients,
                                                  bool isReconstruct)
@@ -2924,6 +2935,7 @@
       DicomTransferSyntax                  transferSyntax_;
       bool                                 hasPixelDataOffset_;
       uint64_t                             pixelDataOffset_;
+      MaxStorageMode                       maximumStorageMode_;
       uint64_t                             maximumStorageSize_;
       unsigned int                         maximumPatientCount_;
       bool                                 isReconstruct_;
@@ -3026,6 +3038,7 @@
                  DicomTransferSyntax transferSyntax,
                  bool hasPixelDataOffset,
                  uint64_t pixelDataOffset,
+                 MaxStorageMode maximumStorageMode,
                  uint64_t maximumStorageSize,
                  unsigned int maximumPatientCount,
                  bool isReconstruct) :
@@ -3040,6 +3053,7 @@
         transferSyntax_(transferSyntax),
         hasPixelDataOffset_(hasPixelDataOffset),
         pixelDataOffset_(pixelDataOffset),
+        maximumStorageMode_(maximumStorageMode),
         maximumStorageSize_(maximumStorageSize),
         maximumPatientCount_(maximumPatientCount),
         isReconstruct_(isReconstruct)
@@ -3134,8 +3148,24 @@
 
           if (!isReconstruct_)  // reconstruction should not affect recycling
           {
-            transaction.Recycle(maximumStorageSize_, maximumPatientCount_,
-                                instanceSize, hashPatient_ /* don't consider the current patient for recycling */);
+            if (maximumStorageMode_ == MaxStorageMode_Reject)
+            {
+              if (transaction.HasReachedMaxStorageSize(maximumStorageSize_, instanceSize))
+              {
+                storeStatus_ = StoreStatus_StorageFull;
+                throw OrthancException(ErrorCode_FullStorage, HttpStatus_507_InsufficientStorage, "Maximum storage size reached"); // throw to cancel the transaction
+              }
+              if (transaction.HasReachedMaxPatientCount(maximumPatientCount_, hashPatient_))
+              {
+                storeStatus_ = StoreStatus_StorageFull;
+                throw OrthancException(ErrorCode_FullStorage, HttpStatus_507_InsufficientStorage, "Maximum patient count reached");  // throw to cancel the transaction
+              }
+            }
+            else
+            {
+              transaction.Recycle(maximumStorageSize_, maximumPatientCount_,
+                                  instanceSize, hashPatient_ /* don't consider the current patient for recycling */);
+            }
           }  
      
           // Attach the files to the newly created instance
@@ -3349,7 +3379,7 @@
 
     Operations operations(instanceMetadata, dicomSummary, attachments, metadata, origin,
                           overwrite, hasTransferSyntax, transferSyntax, hasPixelDataOffset,
-                          pixelDataOffset, maximumStorageSize, maximumPatients, isReconstruct);
+                          pixelDataOffset, maximumStorageMode, maximumStorageSize, maximumPatients, isReconstruct);
     Apply(operations);
     return operations.GetStoreStatus();
   }
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Mon Aug 08 12:42:48 2022 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Tue Aug 09 17:57:38 2022 +0200
@@ -411,6 +411,17 @@
                    unsigned int maximumPatients,
                    uint64_t addedInstanceSize,
                    const std::string& newPatientId);
+
+      bool HasReachedMaxStorageSize(uint64_t maximumStorageSize,
+                                    uint64_t addedInstanceSize);
+
+      bool HasReachedMaxPatientCount(unsigned int maximumPatientCount,
+                                     const std::string& patientId);
+      
+      bool IsRecyclingNeeded(uint64_t maximumStorageSize,
+                             unsigned int maximumPatients,
+                             uint64_t addedInstanceSize,
+                             const std::string& newPatientId);
     };
 
 
@@ -457,7 +468,8 @@
                        IReadWriteOperations* writeOperations);
 
   protected:
-    void StandaloneRecycling(uint64_t maximumStorageSize,
+    void StandaloneRecycling(MaxStorageMode maximumStorageMode,
+                             uint64_t maximumStorageSize,
                              unsigned int maximumPatientCount);
 
   public:
@@ -658,6 +670,7 @@
                       DicomTransferSyntax transferSyntax,
                       bool hasPixelDataOffset,
                       uint64_t pixelDataOffset,
+                      MaxStorageMode maximumStorageMode,
                       uint64_t maximumStorageSize,
                       unsigned int maximumPatients,
                       bool isReconstruct);
--- a/OrthancServer/Sources/ServerEnumerations.cpp	Mon Aug 08 12:42:48 2022 +0200
+++ b/OrthancServer/Sources/ServerEnumerations.cpp	Tue Aug 09 17:57:38 2022 +0200
@@ -218,6 +218,24 @@
     return mode == FindStorageAccessMode_DiskOnLookupAndAnswer;
   }
 
+  MaxStorageMode StringToMaxStorageMode(const std::string& value)
+  {
+    if (value == "Recycle")
+    {
+      return MaxStorageMode_Recycle;
+    }
+    else if (value == "Reject")
+    {
+      return MaxStorageMode_Reject;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "Configuration option \"MaxStorageMode\" "
+                             "should be \"Recycle\" or \"Reject\": " + value);
+    }    
+  }
+
   BuiltinDecoderTranscoderOrder StringToBuiltinDecoderTranscoderOrder(const std::string& value)
   {
     if (value == "Before")
--- a/OrthancServer/Sources/ServerEnumerations.h	Mon Aug 08 12:42:48 2022 +0200
+++ b/OrthancServer/Sources/ServerEnumerations.h	Tue Aug 09 17:57:38 2022 +0200
@@ -49,7 +49,14 @@
     StoreStatus_Success,
     StoreStatus_AlreadyStored,
     StoreStatus_Failure,
-    StoreStatus_FilteredOut     // Removed by NewInstanceFilter or ReceivedInstanceCallback
+    StoreStatus_FilteredOut,     // Removed by NewInstanceFilter or ReceivedInstanceCallback
+    StoreStatus_StorageFull      // new in Orthanc 1.11.2
+  };
+
+  enum MaxStorageMode
+  {
+    MaxStorageMode_Recycle,
+    MaxStorageMode_Reject
   };
 
   enum DicomTagType
@@ -217,6 +224,8 @@
 
   FindStorageAccessMode StringToFindStorageAccessMode(const std::string& str);
 
+  MaxStorageMode StringToMaxStorageMode(const std::string& str);
+  
   bool IsStorageAccessAllowedForAnswers(FindStorageAccessMode mode);
 
   bool IsStorageAccessAllowedForLookup(FindStorageAccessMode mode);
--- a/OrthancServer/Sources/ServerIndex.cpp	Mon Aug 08 12:42:48 2022 +0200
+++ b/OrthancServer/Sources/ServerIndex.cpp	Tue Aug 09 17:57:38 2022 +0200
@@ -321,6 +321,7 @@
                            unsigned int threadSleepGranularityMilliseconds) :
     StatelessDatabaseOperations(db),
     done_(false),
+    maximumStorageMode_(MaxStorageMode_Recycle),
     maximumStorageSize_(0),
     maximumPatients_(0)
   {
@@ -328,7 +329,7 @@
 
     // Initial recycling if the parameters have changed since the last
     // execution of Orthanc
-    StandaloneRecycling(maximumStorageSize_, maximumPatients_);
+    StandaloneRecycling(maximumStorageMode_, maximumStorageSize_, maximumPatients_);
 
     if (HasFlushToDisk())
     {
@@ -385,7 +386,7 @@
       }
     }
 
-    StandaloneRecycling(maximumStorageSize_, maximumPatients_);
+    StandaloneRecycling(maximumStorageMode_, maximumStorageSize_, maximumPatients_);
   }
 
   
@@ -405,9 +406,27 @@
       }
     }
 
-    StandaloneRecycling(maximumStorageSize_, maximumPatients_);
+    StandaloneRecycling(maximumStorageMode_, maximumStorageSize_, maximumPatients_);
   }
 
+  void ServerIndex::SetMaximumStorageMode(MaxStorageMode mode) 
+  {
+    {
+      boost::mutex::scoped_lock lock(monitoringMutex_);
+      maximumStorageMode_ = mode;
+      
+      if (mode == MaxStorageMode_Recycle)
+      {
+        LOG(WARNING) << "Maximum Storage mode: Recycle";
+      }
+      else
+      {
+        LOG(WARNING) << "Maximum Storage mode: Reject";
+      }
+    }
+
+    StandaloneRecycling(maximumStorageMode_, maximumStorageSize_, maximumPatients_);
+  }
 
   void ServerIndex::UnstableResourcesMonitorThread(ServerIndex* that,
                                                    unsigned int threadSleepGranularityMilliseconds)
@@ -523,16 +542,18 @@
   {
     uint64_t maximumStorageSize;
     unsigned int maximumPatients;
+    MaxStorageMode maximumStorageMode;
     
     {
       boost::mutex::scoped_lock lock(monitoringMutex_);
       maximumStorageSize = maximumStorageSize_;
       maximumPatients = maximumPatients_;
+      maximumStorageMode = maximumStorageMode_;
     }
 
     return StatelessDatabaseOperations::Store(
       instanceMetadata, dicomSummary, attachments, metadata, origin, overwrite, hasTransferSyntax,
-      transferSyntax, hasPixelDataOffset, pixelDataOffset, maximumStorageSize, maximumPatients, isReconstruct);
+      transferSyntax, hasPixelDataOffset, pixelDataOffset, maximumStorageMode, maximumStorageSize, maximumPatients, isReconstruct);
   }
 
   
--- a/OrthancServer/Sources/ServerIndex.h	Mon Aug 08 12:42:48 2022 +0200
+++ b/OrthancServer/Sources/ServerIndex.h	Tue Aug 09 17:57:38 2022 +0200
@@ -45,8 +45,9 @@
 
     LeastRecentlyUsedIndex<int64_t, UnstableResourcePayload>  unstableResources_;
 
-    uint64_t     maximumStorageSize_;
-    unsigned int maximumPatients_;
+    MaxStorageMode  maximumStorageMode_;
+    uint64_t        maximumStorageSize_;
+    unsigned int    maximumPatients_;
 
     static void FlushThread(ServerIndex* that,
                             unsigned int threadSleep);
@@ -75,6 +76,8 @@
     // "count == 0" means no limit on the number of patients
     void SetMaximumPatientCount(unsigned int count);
 
+    void SetMaximumStorageMode(MaxStorageMode mode);
+
     StoreStatus Store(std::map<MetadataType, std::string>& instanceMetadata,
                       const DicomMap& dicomSummary,
                       const Attachments& attachments,
--- a/OrthancServer/Sources/main.cpp	Mon Aug 08 12:42:48 2022 +0200
+++ b/OrthancServer/Sources/main.cpp	Tue Aug 09 17:57:38 2022 +0200
@@ -43,6 +43,7 @@
 #include "OrthancMoveRequestHandler.h"
 #include "OrthancWebDav.h"
 #include "ServerContext.h"
+#include "ServerEnumerations.h"
 #include "ServerJobs/StorageCommitmentScpJob.h"
 #include "ServerToolbox.h"
 #include "StorageCommitmentReports.h"
@@ -1564,6 +1565,16 @@
 
     try
     {
+      std::string mode = lock.GetConfiguration().GetStringParameter("MaximumStorageMode", "Recycle");
+      context.GetIndex().SetMaximumStorageMode(StringToMaxStorageMode(mode));
+    }
+    catch (...)
+    {
+      context.GetIndex().SetMaximumStorageMode(MaxStorageMode_Recycle);
+    }
+
+    try
+    {
       uint64_t size = lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumStorageCacheSize", 128);
       context.SetMaximumStorageCacheSize(size * 1024 * 1024);
     }