Mercurial > hg > orthanc
changeset 5524:e998ca3e20be
integration pg-transactions->mainline
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Wed, 31 Jan 2024 09:27:34 +0100 |
parents | 0a74634073c0 (current diff) d82cc7c9720a (diff) |
children | 9a431368801b |
files | |
diffstat | 23 files changed, 542 insertions(+), 158 deletions(-) [+] |
line wrap: on
line diff
--- a/NEWS Wed Jan 24 21:58:14 2024 +0100 +++ b/NEWS Wed Jan 31 09:27:34 2024 +0100 @@ -1,6 +1,14 @@ Pending changes in the mainline =============================== +General +------- + +* Performance of databases: + - At startup, if using a database plugin, displays the latency to access the DB. + - Added support for new DB primitives to enable the "READ COMMITTED" + transaction mode in the PostgreSQL plugin. + REST API --------
--- a/OrthancFramework/Resources/CodeGeneration/ErrorCodes.json Wed Jan 24 21:58:14 2024 +0100 +++ b/OrthancFramework/Resources/CodeGeneration/ErrorCodes.json Wed Jan 31 09:27:34 2024 +0100 @@ -256,6 +256,12 @@ "Name": "ForbiddenAccess", "Description": "Access to a resource is forbidden" }, + { + "Code": 46, + "HttpStatus": 409, + "Name": "DuplicateResource", + "Description": "Duplicate resource" + },
--- a/OrthancFramework/Sources/Enumerations.cpp Wed Jan 24 21:58:14 2024 +0100 +++ b/OrthancFramework/Sources/Enumerations.cpp Wed Jan 31 09:27:34 2024 +0100 @@ -182,6 +182,9 @@ case ErrorCode_ForbiddenAccess: return "Access to a resource is forbidden"; + case ErrorCode_DuplicateResource: + return "Duplicate resource"; + case ErrorCode_SQLiteNotOpened: return "SQLite: The database is not opened"; @@ -2264,6 +2267,9 @@ case ErrorCode_ForbiddenAccess: return HttpStatus_403_Forbidden; + case ErrorCode_DuplicateResource: + return HttpStatus_409_Conflict; + case ErrorCode_CreateDicomNotString: return HttpStatus_400_BadRequest;
--- a/OrthancFramework/Sources/Enumerations.h Wed Jan 24 21:58:14 2024 +0100 +++ b/OrthancFramework/Sources/Enumerations.h Wed Jan 31 09:27:34 2024 +0100 @@ -170,6 +170,7 @@ ErrorCode_Revision = 43 /*!< A bad revision number was provided, which might indicate conflict between multiple writers */, ErrorCode_MainDicomTagsMultiplyDefined = 44 /*!< A main DICOM Tag has been defined multiple times for the same resource level */, ErrorCode_ForbiddenAccess = 45 /*!< Access to a resource is forbidden */, + ErrorCode_DuplicateResource = 46 /*!< Duplicate resource */, ErrorCode_SQLiteNotOpened = 1000 /*!< SQLite: The database is not opened */, ErrorCode_SQLiteAlreadyOpened = 1001 /*!< SQLite: Connection is already open */, ErrorCode_SQLiteCannotOpen = 1002 /*!< SQLite: Unable to open the database */,
--- a/OrthancServer/CMakeLists.txt Wed Jan 24 21:58:14 2024 +0100 +++ b/OrthancServer/CMakeLists.txt Wed Jan 31 09:27:34 2024 +0100 @@ -87,6 +87,7 @@ ##################################################################### set(ORTHANC_SERVER_SOURCES + ${CMAKE_SOURCE_DIR}/Sources/Database/BaseDatabaseWrapper.cpp ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/DatabaseLookup.cpp ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/ICreateInstance.cpp ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/IGetChildrenMetadata.cpp
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp Wed Jan 24 21:58:14 2024 +0100 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp Wed Jan 31 09:27:34 2024 +0100 @@ -43,7 +43,7 @@ namespace Orthanc { class OrthancPluginDatabase::Transaction : - public IDatabaseWrapper::ITransaction, + public BaseDatabaseWrapper::BaseTransaction, public Compatibility::ICreateInstance, public Compatibility::IGetChildrenMetadata, public Compatibility::ILookupResources,
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.h Wed Jan 24 21:58:14 2024 +0100 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.h Wed Jan 31 09:27:34 2024 +0100 @@ -25,7 +25,7 @@ #if ORTHANC_ENABLE_PLUGINS == 1 #include "../../../OrthancFramework/Sources/SharedLibrary.h" -#include "../../Sources/Database/IDatabaseWrapper.h" +#include "../../Sources/Database/BaseDatabaseWrapper.h" #include "../Include/orthanc/OrthancCDatabasePlugin.h" #include "PluginsErrorDictionary.h" @@ -45,7 +45,7 @@ * able to rollback the modifications. Read-only accesses didn't * start a transaction, as they were protected by the global mutex. **/ - class OrthancPluginDatabase : public IDatabaseWrapper + class OrthancPluginDatabase : public BaseDatabaseWrapper { private: class Transaction; @@ -65,6 +65,7 @@ Transaction* activeTransaction_; bool fastGetTotalSize_; uint64_t currentDiskSize_; + IDatabaseWrapper::Capabilities dbCapabilities_; OrthancPluginDatabaseContext* GetContext() { @@ -94,11 +95,6 @@ { } - virtual bool HasFlushToDisk() const ORTHANC_OVERRIDE - { - return false; - } - virtual IDatabaseWrapper::ITransaction* StartTransaction(TransactionType type, IDatabaseListener& listener) ORTHANC_OVERRIDE; @@ -108,14 +104,9 @@ virtual void Upgrade(unsigned int targetVersion, IStorageArea& storageArea) ORTHANC_OVERRIDE; - virtual bool HasRevisionsSupport() const ORTHANC_OVERRIDE + virtual const Capabilities GetDatabaseCapabilities() const ORTHANC_OVERRIDE { - return false; // No support for revisions in old API - } - - virtual bool HasLabelsSupport() const ORTHANC_OVERRIDE - { - return false; + return dbCapabilities_; } void AnswerReceived(const _OrthancPluginDatabaseAnswer& answer);
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp Wed Jan 24 21:58:14 2024 +0100 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp Wed Jan 31 09:27:34 2024 +0100 @@ -45,7 +45,7 @@ namespace Orthanc { - class OrthancPluginDatabaseV3::Transaction : public IDatabaseWrapper::ITransaction + class OrthancPluginDatabaseV3::Transaction : public BaseDatabaseWrapper::BaseTransaction { private: OrthancPluginDatabaseV3& that_; @@ -278,7 +278,6 @@ } } - virtual void Rollback() ORTHANC_OVERRIDE { CheckSuccess(that_.backend_.rollback(transaction_)); @@ -1084,6 +1083,7 @@ errorDictionary_(errorDictionary), database_(database), serverIdentifier_(serverIdentifier) + { CLOG(INFO, PLUGINS) << "Identifier of this Orthanc server for the global properties " << "of the custom database: \"" << serverIdentifier << "\""; @@ -1190,6 +1190,11 @@ void OrthancPluginDatabaseV3::Open() { CheckSuccess(backend_.open(database_)); + + // update the db capabilities + uint8_t hasRevisions; + CheckSuccess(backend_.hasRevisionsSupport(database_, &hasRevisions)); + dbCapabilities_.SetRevisionsSupport(hasRevisions != 0); } @@ -1250,12 +1255,4 @@ } } - - bool OrthancPluginDatabaseV3::HasRevisionsSupport() const - { - // WARNING: This method requires "Open()" to have been called - uint8_t hasRevisions; - CheckSuccess(backend_.hasRevisionsSupport(database_, &hasRevisions)); - return (hasRevisions != 0); - } }
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.h Wed Jan 24 21:58:14 2024 +0100 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.h Wed Jan 31 09:27:34 2024 +0100 @@ -25,13 +25,13 @@ #if ORTHANC_ENABLE_PLUGINS == 1 #include "../../../OrthancFramework/Sources/SharedLibrary.h" -#include "../../Sources/Database/IDatabaseWrapper.h" +#include "../../Sources/Database/BaseDatabaseWrapper.h" #include "../Include/orthanc/OrthancCDatabasePlugin.h" #include "PluginsErrorDictionary.h" namespace Orthanc { - class OrthancPluginDatabaseV3 : public IDatabaseWrapper + class OrthancPluginDatabaseV3 : public BaseDatabaseWrapper { private: class Transaction; @@ -41,6 +41,7 @@ OrthancPluginDatabaseBackendV3 backend_; void* database_; std::string serverIdentifier_; + IDatabaseWrapper::Capabilities dbCapabilities_; void CheckSuccess(OrthancPluginErrorCode code) const; @@ -67,11 +68,6 @@ { } - virtual bool HasFlushToDisk() const ORTHANC_OVERRIDE - { - return false; - } - virtual IDatabaseWrapper::ITransaction* StartTransaction(TransactionType type, IDatabaseListener& listener) ORTHANC_OVERRIDE; @@ -81,11 +77,9 @@ virtual void Upgrade(unsigned int targetVersion, IStorageArea& storageArea) ORTHANC_OVERRIDE; - virtual bool HasRevisionsSupport() const ORTHANC_OVERRIDE; - - virtual bool HasLabelsSupport() const ORTHANC_OVERRIDE + virtual const Capabilities GetDatabaseCapabilities() const ORTHANC_OVERRIDE { - return false; + return dbCapabilities_; } }; }
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp Wed Jan 24 21:58:14 2024 +0100 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp Wed Jan 31 09:27:34 2024 +0100 @@ -229,7 +229,7 @@ bool isSingleResource, int64_t resource) { - if (database_.HasLabelsSupport()) + if (database_.GetDatabaseCapabilities().HasLabelsSupport()) { DatabasePluginMessages::TransactionRequest request; request.mutable_list_labels()->set_single_resource(isSingleResource); @@ -305,7 +305,6 @@ } } - void* GetTransactionObject() { return transaction_; @@ -772,7 +771,40 @@ } } - + + virtual int64_t IncrementGlobalProperty(GlobalProperty property, + int64_t increment, + bool shared) ORTHANC_OVERRIDE + { + DatabasePluginMessages::TransactionRequest request; + request.mutable_increment_global_property()->set_server_id(shared ? "" : database_.GetServerIdentifier()); + request.mutable_increment_global_property()->set_property(property); + request.mutable_increment_global_property()->set_increment(increment); + + DatabasePluginMessages::TransactionResponse response; + ExecuteTransaction(response, DatabasePluginMessages::OPERATION_INCREMENT_GLOBAL_PROPERTY, request); + + return response.increment_global_property().new_value(); + } + + virtual void UpdateAndGetStatistics(int64_t& patientsCount, + int64_t& studiesCount, + int64_t& seriesCount, + int64_t& instancesCount, + int64_t& compressedSize, + int64_t& uncompressedSize) ORTHANC_OVERRIDE + { + DatabasePluginMessages::TransactionResponse response; + ExecuteTransaction(response, DatabasePluginMessages::OPERATION_UPDATE_AND_GET_STATISTICS); + + patientsCount = response.update_and_get_statistics().patients_count(); + studiesCount = response.update_and_get_statistics().studies_count(); + seriesCount = response.update_and_get_statistics().series_count(); + instancesCount = response.update_and_get_statistics().instances_count(); + compressedSize = response.update_and_get_statistics().total_compressed_size(); + uncompressedSize = response.update_and_get_statistics().total_uncompressed_size(); + } + virtual bool LookupMetadata(std::string& target, int64_t& revision, int64_t id, @@ -948,7 +980,7 @@ LabelsConstraint labelsConstraint, uint32_t limit) ORTHANC_OVERRIDE { - if (!database_.HasLabelsSupport() && + if (!database_.GetDatabaseCapabilities().HasLabelsSupport() && !labels.empty()) { throw OrthancException(ErrorCode_InternalError); @@ -1197,7 +1229,7 @@ virtual void AddLabel(int64_t resource, const std::string& label) ORTHANC_OVERRIDE { - if (database_.HasLabelsSupport()) + if (database_.GetDatabaseCapabilities().HasLabelsSupport()) { DatabasePluginMessages::TransactionRequest request; request.mutable_add_label()->set_id(resource); @@ -1216,7 +1248,7 @@ virtual void RemoveLabel(int64_t resource, const std::string& label) ORTHANC_OVERRIDE { - if (database_.HasLabelsSupport()) + if (database_.GetDatabaseCapabilities().HasLabelsSupport()) { DatabasePluginMessages::TransactionRequest request; request.mutable_remove_label()->set_id(resource); @@ -1255,10 +1287,7 @@ definition_(database), serverIdentifier_(serverIdentifier), open_(false), - databaseVersion_(0), - hasFlushToDisk_(false), - hasRevisionsSupport_(false), - hasLabelsSupport_(false) + databaseVersion_(0) { CLOG(INFO, PLUGINS) << "Identifier of this Orthanc server for the global properties " << "of the custom database: \"" << serverIdentifier << "\""; @@ -1325,10 +1354,15 @@ DatabasePluginMessages::DatabaseRequest request; DatabasePluginMessages::DatabaseResponse response; ExecuteDatabase(response, *this, DatabasePluginMessages::OPERATION_GET_SYSTEM_INFORMATION, request); - databaseVersion_ = response.get_system_information().database_version(); - hasFlushToDisk_ = response.get_system_information().supports_flush_to_disk(); - hasRevisionsSupport_ = response.get_system_information().supports_revisions(); - hasLabelsSupport_ = response.get_system_information().supports_labels(); + + const ::Orthanc::DatabasePluginMessages::GetSystemInformation_Response& systemInfo = response.get_system_information(); + databaseVersion_ = systemInfo.database_version(); + dbCapabilities_.SetFlushToDisk(systemInfo.supports_flush_to_disk()); + dbCapabilities_.SetRevisionsSupport(systemInfo.supports_revisions()); + dbCapabilities_.SetLabelsSupport(systemInfo.supports_labels()); + dbCapabilities_.SetAtomicIncrementGlobalProperty(systemInfo.supports_increment_global_property()); + dbCapabilities_.SetUpdateAndGetStatistics(systemInfo.has_update_and_get_statistics()); + dbCapabilities_.SetMeasureLatency(systemInfo.has_measure_latency()); } open_ = true; @@ -1350,23 +1384,11 @@ } - bool OrthancPluginDatabaseV4::HasFlushToDisk() const - { - if (!open_) - { - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } - else - { - return hasFlushToDisk_; - } - } - void OrthancPluginDatabaseV4::FlushToDisk() { if (!open_ || - !hasFlushToDisk_) + !GetDatabaseCapabilities().HasFlushToDisk()) { throw OrthancException(ErrorCode_BadSequenceOfCalls); } @@ -1438,8 +1460,8 @@ } } - - bool OrthancPluginDatabaseV4::HasRevisionsSupport() const + + uint64_t OrthancPluginDatabaseV4::MeasureLatency() { if (!open_) { @@ -1447,12 +1469,16 @@ } else { - return hasRevisionsSupport_; + DatabasePluginMessages::DatabaseRequest request; + DatabasePluginMessages::DatabaseResponse response; + + ExecuteDatabase(response, *this, DatabasePluginMessages::OPERATION_MEASURE_LATENCY, request); + return response.measure_latency().latency_us(); } } - - bool OrthancPluginDatabaseV4::HasLabelsSupport() const + + const IDatabaseWrapper::Capabilities OrthancPluginDatabaseV4::GetDatabaseCapabilities() const { if (!open_) { @@ -1460,7 +1486,7 @@ } else { - return hasLabelsSupport_; + return dbCapabilities_; } } }
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h Wed Jan 24 21:58:14 2024 +0100 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h Wed Jan 31 09:27:34 2024 +0100 @@ -42,9 +42,7 @@ std::string serverIdentifier_; bool open_; unsigned int databaseVersion_; - bool hasFlushToDisk_; - bool hasRevisionsSupport_; - bool hasLabelsSupport_; + IDatabaseWrapper::Capabilities dbCapabilities_; void CheckSuccess(OrthancPluginErrorCode code) const; @@ -82,8 +80,6 @@ virtual void FlushToDisk() ORTHANC_OVERRIDE; - virtual bool HasFlushToDisk() const ORTHANC_OVERRIDE; - virtual IDatabaseWrapper::ITransaction* StartTransaction(TransactionType type, IDatabaseListener& listener) ORTHANC_OVERRIDE; @@ -93,9 +89,9 @@ virtual void Upgrade(unsigned int targetVersion, IStorageArea& storageArea) ORTHANC_OVERRIDE; - virtual bool HasRevisionsSupport() const ORTHANC_OVERRIDE; + virtual uint64_t MeasureLatency() ORTHANC_OVERRIDE; - virtual bool HasLabelsSupport() const ORTHANC_OVERRIDE; + virtual const Capabilities GetDatabaseCapabilities() const ORTHANC_OVERRIDE; }; }
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Wed Jan 24 21:58:14 2024 +0100 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Wed Jan 31 09:27:34 2024 +0100 @@ -120,7 +120,7 @@ #define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER 1 #define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER 12 -#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER 2 +#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER 3 #if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE) @@ -246,6 +246,7 @@ OrthancPluginErrorCode_Revision = 43 /*!< A bad revision number was provided, which might indicate conflict between multiple writers */, OrthancPluginErrorCode_MainDicomTagsMultiplyDefined = 44 /*!< A main DICOM Tag has been defined multiple times for the same resource level */, OrthancPluginErrorCode_ForbiddenAccess = 45 /*!< Access to a resource is forbidden */, + OrthancPluginErrorCode_DuplicateResource = 46 /*!< Duplicate resource */, OrthancPluginErrorCode_SQLiteNotOpened = 1000 /*!< SQLite: The database is not opened */, OrthancPluginErrorCode_SQLiteAlreadyOpened = 1001 /*!< SQLite: Connection is already open */, OrthancPluginErrorCode_SQLiteCannotOpen = 1002 /*!< SQLite: Unable to open the database */,
--- a/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto Wed Jan 24 21:58:14 2024 +0100 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto Wed Jan 31 09:27:34 2024 +0100 @@ -121,6 +121,7 @@ OPERATION_START_TRANSACTION = 4; OPERATION_UPGRADE = 5; OPERATION_FINALIZE_TRANSACTION = 6; + OPERATION_MEASURE_LATENCY = 7; // New in Orthanc 1.12.X } enum TransactionType { @@ -136,6 +137,9 @@ bool supports_flush_to_disk = 2; bool supports_revisions = 3; bool supports_labels = 4; + bool supports_increment_global_property = 5; + bool has_update_and_get_statistics = 6; + bool has_measure_latency = 7; } } @@ -198,6 +202,15 @@ } } +message MeasureLatency { + message Request { + } + message Response { + int64 latency_us = 1; + } +} + + message DatabaseRequest { sfixed64 database = 1; DatabaseOperation operation = 2; @@ -208,7 +221,8 @@ FlushToDisk.Request flush_to_disk = 103; StartTransaction.Request start_transaction = 104; Upgrade.Request upgrade = 105; - FinalizeTransaction.Request finalize_transaction = 106; + FinalizeTransaction.Request finalize_transaction = 106; + MeasureLatency.Request measure_latency = 107; } message DatabaseResponse { @@ -219,6 +233,7 @@ StartTransaction.Response start_transaction = 104; Upgrade.Response upgrade = 105; FinalizeTransaction.Response finalize_transaction = 106; + MeasureLatency.Response measure_latency = 107; } @@ -275,6 +290,8 @@ OPERATION_ADD_LABEL = 45; // New in Orthanc 1.12.0 OPERATION_REMOVE_LABEL = 46; // New in Orthanc 1.12.0 OPERATION_LIST_LABELS = 47; // New in Orthanc 1.12.0 + OPERATION_INCREMENT_GLOBAL_PROPERTY = 48; // New in Orthanc 1.12.X + OPERATION_UPDATE_AND_GET_STATISTICS = 49; // New in Orthanc 1.12.X } message Rollback { @@ -628,6 +645,30 @@ } } +message IncrementGlobalProperty { + message Request { + string server_id = 1; + int32 property = 2; + int64 increment = 3; + } + message Response { + int64 new_value = 1; + } +} + +message UpdateAndGetStatistics { + message Request { + } + message Response { + int64 patients_count = 1; + int64 studies_count = 2; + int64 series_count = 3; + int64 instances_count = 4; + int64 total_compressed_size = 5; + int64 total_uncompressed_size = 6; + } +} + message ClearMainDicomTags { message Request { int64 id = 1; @@ -834,6 +875,8 @@ AddLabel.Request add_label = 145; RemoveLabel.Request remove_label = 146; ListLabels.Request list_labels = 147; + IncrementGlobalProperty.Request increment_global_property = 148; + UpdateAndGetStatistics.Request update_and_get_statistics = 149; } message TransactionResponse { @@ -885,6 +928,8 @@ AddLabel.Response add_label = 145; RemoveLabel.Response remove_label = 146; ListLabels.Response list_labels = 147; + IncrementGlobalProperty.Response increment_global_property = 148; + UpdateAndGetStatistics.Response update_and_get_statistics = 149; } enum RequestType {
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/BaseDatabaseWrapper.cpp Wed Jan 31 09:27:34 2024 +0100 @@ -0,0 +1,52 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2024 Osimis S.A., Belgium + * Copyright (C) 2021-2024 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 "BaseDatabaseWrapper.h" + +#include "../../../OrthancFramework/Sources/OrthancException.h" + +namespace Orthanc +{ + int64_t BaseDatabaseWrapper::BaseTransaction::IncrementGlobalProperty(GlobalProperty property, + int64_t increment, + bool shared) + { + throw OrthancException(ErrorCode_NotImplemented); // Not supported + } + + + void BaseDatabaseWrapper::BaseTransaction::UpdateAndGetStatistics(int64_t& patientsCount, + int64_t& studiesCount, + int64_t& seriesCount, + int64_t& instancesCount, + int64_t& compressedSize, + int64_t& uncompressedSize) + { + throw OrthancException(ErrorCode_NotImplemented); // Not supported + } + + + uint64_t BaseDatabaseWrapper::MeasureLatency() + { + throw OrthancException(ErrorCode_NotImplemented); // only implemented in V4 + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/BaseDatabaseWrapper.h Wed Jan 31 09:27:34 2024 +0100 @@ -0,0 +1,53 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2024 Osimis S.A., Belgium + * Copyright (C) 2021-2024 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 "IDatabaseWrapper.h" + +namespace Orthanc +{ + /** + * This class provides a default "not implemented" implementation + * for all recent methods (1.12.X) + **/ + class BaseDatabaseWrapper : public IDatabaseWrapper + { + public: + class BaseTransaction : public IDatabaseWrapper::ITransaction + { + public: + virtual int64_t IncrementGlobalProperty(GlobalProperty property, + int64_t increment, + bool shared) ORTHANC_OVERRIDE; + + virtual void UpdateAndGetStatistics(int64_t& patientsCount, + int64_t& studiesCount, + int64_t& seriesCount, + int64_t& instancesCount, + int64_t& compressedSize, + int64_t& uncompressedSize) ORTHANC_OVERRIDE; + }; + + virtual uint64_t MeasureLatency() ORTHANC_OVERRIDE; + }; +}
--- a/OrthancServer/Sources/Database/IDatabaseWrapper.h Wed Jan 24 21:58:14 2024 +0100 +++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h Wed Jan 31 09:27:34 2024 +0100 @@ -39,10 +39,92 @@ class DatabaseConstraint; class ResourcesContent; - class IDatabaseWrapper : public boost::noncopyable { public: + class Capabilities + { + private: + bool hasFlushToDisk_; + bool hasRevisionsSupport_; + bool hasLabelsSupport_; + bool hasAtomicIncrementGlobalProperty_; + bool hasUpdateAndGetStatistics_; + bool hasMeasureLatency_; + + public: + Capabilities() : + hasFlushToDisk_(false), + hasRevisionsSupport_(false), + hasLabelsSupport_(false), + hasAtomicIncrementGlobalProperty_(false), + hasUpdateAndGetStatistics_(false), + hasMeasureLatency_(false) + { + } + + void SetFlushToDisk(bool value) + { + hasFlushToDisk_ = value; + } + + bool HasFlushToDisk() const + { + return hasFlushToDisk_; + } + + void SetRevisionsSupport(bool value) + { + hasRevisionsSupport_ = value; + } + + bool HasRevisionsSupport() const + { + return hasRevisionsSupport_; + } + + void SetLabelsSupport(bool value) + { + hasLabelsSupport_ = value; + } + + bool HasLabelsSupport() const + { + return hasLabelsSupport_; + } + + void SetAtomicIncrementGlobalProperty(bool value) + { + hasAtomicIncrementGlobalProperty_ = value; + } + + bool HasAtomicIncrementGlobalProperty() const + { + return hasAtomicIncrementGlobalProperty_; + } + + void SetUpdateAndGetStatistics(bool value) + { + hasUpdateAndGetStatistics_ = value; + } + + bool HasUpdateAndGetStatistics() const + { + return hasUpdateAndGetStatistics_; + } + + void SetMeasureLatency(bool value) + { + hasMeasureLatency_ = value; + } + + bool HasMeasureLatency() const + { + return hasMeasureLatency_; + } + }; + + struct CreateInstanceResult : public boost::noncopyable { bool isNewPatient_; @@ -257,6 +339,17 @@ // List all the labels that are present in any resource virtual void ListAllLabels(std::set<std::string>& target) = 0; + + virtual int64_t IncrementGlobalProperty(GlobalProperty property, + int64_t increment, + bool shared) = 0; + + virtual void UpdateAndGetStatistics(int64_t& patientsCount, + int64_t& studiesCount, + int64_t& seriesCount, + int64_t& instancesCount, + int64_t& compressedSize, + int64_t& uncompressedSize) = 0; }; @@ -270,8 +363,6 @@ virtual void FlushToDisk() = 0; - virtual bool HasFlushToDisk() const = 0; - virtual ITransaction* StartTransaction(TransactionType type, IDatabaseListener& listener) = 0; @@ -280,8 +371,8 @@ virtual void Upgrade(unsigned int targetVersion, IStorageArea& storageArea) = 0; - virtual bool HasRevisionsSupport() const = 0; + virtual const Capabilities GetDatabaseCapabilities() const = 0; - virtual bool HasLabelsSupport() const = 0; + virtual uint64_t MeasureLatency() = 0; }; }
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Wed Jan 24 21:58:14 2024 +0100 +++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Wed Jan 31 09:27:34 2024 +0100 @@ -1324,6 +1324,9 @@ signalRemainingAncestor_(NULL), version_(0) { + // TODO: implement revisions in SQLite + dbCapabilities_.SetFlushToDisk(true); + dbCapabilities_.SetLabelsSupport(true); db_.Open(path); } @@ -1333,6 +1336,9 @@ signalRemainingAncestor_(NULL), version_(0) { + // TODO: implement revisions in SQLite + dbCapabilities_.SetFlushToDisk(true); + dbCapabilities_.SetLabelsSupport(true); db_.OpenInMemory(); }
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h Wed Jan 24 21:58:14 2024 +0100 +++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h Wed Jan 31 09:27:34 2024 +0100 @@ -22,7 +22,7 @@ #pragma once -#include "IDatabaseWrapper.h" +#include "BaseDatabaseWrapper.h" #include "../../../OrthancFramework/Sources/SQLite/Connection.h" @@ -35,7 +35,7 @@ * translates low-level requests into SQL statements. Mutual * exclusion MUST be implemented at a higher level. **/ - class SQLiteDatabaseWrapper : public IDatabaseWrapper + class SQLiteDatabaseWrapper : public BaseDatabaseWrapper { private: class TransactionBase; @@ -51,6 +51,7 @@ TransactionBase* activeTransaction_; SignalRemainingAncestor* signalRemainingAncestor_; unsigned int version_; + IDatabaseWrapper::Capabilities dbCapabilities_; void GetChangesInternal(std::list<ServerIndexChange>& target, bool& done, @@ -79,11 +80,6 @@ virtual void FlushToDisk() ORTHANC_OVERRIDE; - virtual bool HasFlushToDisk() const ORTHANC_OVERRIDE - { - return true; - } - virtual unsigned int GetDatabaseVersion() ORTHANC_OVERRIDE { return version_; @@ -92,24 +88,23 @@ virtual void Upgrade(unsigned int targetVersion, IStorageArea& storageArea) ORTHANC_OVERRIDE; - virtual bool HasRevisionsSupport() const ORTHANC_OVERRIDE + virtual const Capabilities GetDatabaseCapabilities() const ORTHANC_OVERRIDE { - return false; // TODO - REVISIONS + return dbCapabilities_; } - virtual bool HasLabelsSupport() const ORTHANC_OVERRIDE + virtual uint64_t MeasureLatency() ORTHANC_OVERRIDE { - return true; + throw OrthancException(ErrorCode_NotImplemented); } - /** * The "StartTransaction()" method is guaranteed to return a class * derived from "UnitTestsTransaction". The methods of * "UnitTestsTransaction" give access to additional information * about the underlying SQLite database to be used in unit tests. **/ - class UnitTestsTransaction : public ITransaction + class UnitTestsTransaction : public BaseDatabaseWrapper::BaseTransaction { protected: SQLite::Connection& db_;
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Wed Jan 24 21:58:14 2024 +0100 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Wed Jan 31 09:27:34 2024 +0100 @@ -607,7 +607,7 @@ Transaction transaction(db_, *factory_, TransactionType_ReadOnly); // TODO - Only if not "TransactionType_Implicit" { - ReadOnlyTransaction t(transaction.GetDatabaseTransaction(), transaction.GetContext(), db_.HasLabelsSupport()); + ReadOnlyTransaction t(transaction.GetDatabaseTransaction(), transaction.GetContext()); readOperations->Apply(t); } transaction.Commit(); @@ -618,7 +618,7 @@ Transaction transaction(db_, *factory_, TransactionType_ReadWrite); { - ReadWriteTransaction t(transaction.GetDatabaseTransaction(), transaction.GetContext(), db_.HasLabelsSupport()); + ReadWriteTransaction t(transaction.GetDatabaseTransaction(), transaction.GetContext()); writeOperations->Apply(t); } transaction.Commit(); @@ -632,6 +632,7 @@ { if (attempt >= maxRetries_) { + LOG(ERROR) << "Maximum transactions retries reached " << e.GetDetails(); throw; } else @@ -654,7 +655,6 @@ StatelessDatabaseOperations::StatelessDatabaseOperations(IDatabaseWrapper& db) : db_(db), mainDicomTagsRegistry_(new MainDicomTagsRegistry), - hasFlushToDisk_(db.HasFlushToDisk()), maxRetries_(0) { } @@ -721,7 +721,8 @@ bool&, ExpandedResource&, const std::string&, ResourceType, const std::set<DicomTag>&, ExpandResourceFlags> { private: - + bool hasLabelsSupport_; + static bool LookupStringMetadata(std::string& result, const std::map<MetadataType, std::string>& metadata, MetadataType type) @@ -763,6 +764,11 @@ public: + Operations(bool hasLabelsSupport) : + hasLabelsSupport_(hasLabelsSupport) + { + } + virtual void ApplyTuple(ReadOnlyTransaction& transaction, const Tuple& tuple) ORTHANC_OVERRIDE { @@ -939,7 +945,7 @@ } if ((expandFlags & ExpandResourceFlags_IncludeLabels) && - transaction.HasLabelsSupport()) + hasLabelsSupport_) { transaction.ListLabels(target.labels_, internalId); } @@ -978,7 +984,7 @@ }; bool found; - Operations operations; + Operations operations(db_.GetDatabaseCapabilities().HasLabelsSupport()); operations.Apply(*this, found, target, publicId, level, requestedTags, expandFlags); return found; } @@ -1103,7 +1109,54 @@ /* out */ uint64_t& countSeries, /* out */ uint64_t& countInstances) { - class Operations : public ReadOnlyOperationsT6<uint64_t&, uint64_t&, uint64_t&, uint64_t&, uint64_t&, uint64_t&> + // Code introduced in Orthanc 1.12.3 that updates and gets all statistics. + // I.e, PostgreSQL now store "changes" to apply to the statistics to prevent row locking + // of the GlobalIntegers table while multiple clients are inserting/deleting new resources. + // Then, the statistics are updated when requested to make sure they are correct. + class Operations : public IReadWriteOperations + { + private: + int64_t diskSize_; + int64_t uncompressedSize_; + int64_t countPatients_; + int64_t countStudies_; + int64_t countSeries_; + int64_t countInstances_; + + public: + Operations() : + diskSize_(0), + uncompressedSize_(0), + countPatients_(0), + countStudies_(0), + countSeries_(0), + countInstances_(0) + { + } + + void GetValues(uint64_t& diskSize, + uint64_t& uncompressedSize, + uint64_t& countPatients, + uint64_t& countStudies, + uint64_t& countSeries, + uint64_t& countInstances) const + { + diskSize = static_cast<uint64_t>(diskSize_); + uncompressedSize = static_cast<uint64_t>(uncompressedSize_); + countPatients = static_cast<uint64_t>(countPatients_); + countStudies = static_cast<uint64_t>(countStudies_); + countSeries = static_cast<uint64_t>(countSeries_); + countInstances = static_cast<uint64_t>(countInstances_); + } + + virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE + { + transaction.UpdateAndGetStatistics(countPatients_, countStudies_, countSeries_, countInstances_, diskSize_, uncompressedSize_); + } + }; + + // Compatibility with Orthanc SDK <= 1.12.2 that reads each entry individualy + class LegacyOperations : public ReadOnlyOperationsT6<uint64_t&, uint64_t&, uint64_t&, uint64_t&, uint64_t&, uint64_t&> { public: virtual void ApplyTuple(ReadOnlyTransaction& transaction, @@ -1117,10 +1170,20 @@ tuple.get<5>() = transaction.GetResourcesCount(ResourceType_Instance); } }; - - Operations operations; - operations.Apply(*this, diskSize, uncompressedSize, countPatients, - countStudies, countSeries, countInstances); + + if (GetDatabaseCapabilities().HasUpdateAndGetStatistics()) + { + Operations operations; + Apply(operations); + + operations.GetValues(diskSize, uncompressedSize, countPatients, countStudies, countSeries, countInstances); + } + else + { + LegacyOperations operations; + operations.Apply(*this, diskSize, uncompressedSize, countPatients, + countStudies, countSeries, countInstances); + } } @@ -1961,7 +2024,7 @@ }; if (!labels.empty() && - !db_.HasLabelsSupport()) + !db_.GetDatabaseCapabilities().HasLabelsSupport()) { throw OrthancException(ErrorCode_NotImplemented, "The database backend doesn't support labels"); } @@ -2414,13 +2477,16 @@ uint64_t newValue_; GlobalProperty sequence_; bool shared_; + bool hasAtomicIncrementGlobalProperty_; public: Operations(GlobalProperty sequence, - bool shared) : + bool shared, + bool hasAtomicIncrementGlobalProperty) : newValue_(0), // Dummy initialization sequence_(sequence), - shared_(shared) + shared_(shared), + hasAtomicIncrementGlobalProperty_(hasAtomicIncrementGlobalProperty) { } @@ -2431,36 +2497,43 @@ virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE { - std::string oldString; - - if (transaction.LookupGlobalProperty(oldString, sequence_, shared_)) + if (hasAtomicIncrementGlobalProperty_) { - uint64_t oldValue; - - try - { - oldValue = boost::lexical_cast<uint64_t>(oldString); - } - catch (boost::bad_lexical_cast&) - { - LOG(ERROR) << "Cannot read the global sequence " - << boost::lexical_cast<std::string>(sequence_) << ", resetting it"; - oldValue = 0; - } - - newValue_ = oldValue + 1; + newValue_ = static_cast<uint64_t>(transaction.IncrementGlobalProperty(sequence_, shared_, 1)); } else { - // Initialize the sequence at "1" - newValue_ = 1; + std::string oldString; + + if (transaction.LookupGlobalProperty(oldString, sequence_, shared_)) + { + uint64_t oldValue; + + try + { + oldValue = boost::lexical_cast<uint64_t>(oldString); + } + catch (boost::bad_lexical_cast&) + { + LOG(ERROR) << "Cannot read the global sequence " + << boost::lexical_cast<std::string>(sequence_) << ", resetting it"; + oldValue = 0; + } + + newValue_ = oldValue + 1; + } + else + { + // Initialize the sequence at "1" + newValue_ = 1; + } + + transaction.SetGlobalProperty(sequence_, shared_, boost::lexical_cast<std::string>(newValue_)); } - - transaction.SetGlobalProperty(sequence_, shared_, boost::lexical_cast<std::string>(newValue_)); } }; - Operations operations(sequence, shared); + Operations operations(sequence, shared, GetDatabaseCapabilities().HasAtomicIncrementGlobalProperty()); Apply(operations); assert(operations.GetNewValue() != 0); return operations.GetNewValue(); @@ -3147,7 +3220,13 @@ if (!transaction.CreateInstance(status, instanceId, hashPatient_, hashStudy_, hashSeries_, hashInstance_)) { - throw OrthancException(ErrorCode_InternalError, "No new instance while overwriting; this should not happen."); + // Note that, sometime, it does not create a new instance, + // in very rare occasions in READ COMMITTED mode when multiple clients are pushing the same instance at the same time, + // this thread will not create the instance because another thread has created it in the meantime. + // At the end, there is always a thread that creates the instance and this is what we expect. + + // Note, we must delete the attachments that have already been stored from this failed insertion (they have not yet been added into the DB) + throw OrthancException(ErrorCode_DuplicateResource, "No new instance while overwriting; this might happen if another client has pushed the same instance at the same time."); } } else @@ -3694,6 +3773,6 @@ bool StatelessDatabaseOperations::HasLabelsSupport() { boost::shared_lock<boost::shared_mutex> lock(mutex_); - return db_.HasLabelsSupport(); + return db_.GetDatabaseCapabilities().HasLabelsSupport(); } }
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Wed Jan 24 21:58:14 2024 +0100 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Wed Jan 31 09:27:34 2024 +0100 @@ -176,17 +176,14 @@ { private: ITransactionContext& context_; - bool hasLabelsSupport_; - + protected: IDatabaseWrapper::ITransaction& transaction_; public: explicit ReadOnlyTransaction(IDatabaseWrapper::ITransaction& transaction, - ITransactionContext& context, - bool hasLabelsSupport) : + ITransactionContext& context) : context_(context), - hasLabelsSupport_(hasLabelsSupport), transaction_(transaction) { } @@ -196,11 +193,6 @@ return context_; } - bool HasLabelsSupport() const - { - return hasLabelsSupport_; - } - /** * Higher-level constructions **/ @@ -391,9 +383,8 @@ { public: ReadWriteTransaction(IDatabaseWrapper::ITransaction& transaction, - ITransactionContext& context, - bool hasLabelsSupport) : - ReadOnlyTransaction(transaction, context, hasLabelsSupport) + ITransactionContext& context) : + ReadOnlyTransaction(transaction, context) { } @@ -463,6 +454,23 @@ transaction_.SetGlobalProperty(property, shared, value); } + int64_t IncrementGlobalProperty(GlobalProperty sequence, + bool shared, + int64_t increment) + { + return transaction_.IncrementGlobalProperty(sequence, shared, increment); + } + + void UpdateAndGetStatistics(int64_t& patientsCount, + int64_t& studiesCount, + int64_t& seriesCount, + int64_t& instancesCount, + int64_t& compressedSize, + int64_t& uncompressedSize) + { + return transaction_.UpdateAndGetStatistics(patientsCount, studiesCount, seriesCount, instancesCount, compressedSize, uncompressedSize); + } + void SetMetadata(int64_t id, MetadataType type, const std::string& value, @@ -540,7 +548,6 @@ IDatabaseWrapper& db_; boost::shared_ptr<MainDicomTagsRegistry> mainDicomTagsRegistry_; // "shared_ptr" because of PImpl - bool hasFlushToDisk_; // Mutex to protect the configuration options boost::shared_mutex mutex_; @@ -575,12 +582,13 @@ return db_.GetDatabaseVersion(); } + const IDatabaseWrapper::Capabilities GetDatabaseCapabilities() const + { + return db_.GetDatabaseCapabilities(); + } + void FlushToDisk(); - bool HasFlushToDisk() const - { - return hasFlushToDisk_; - } void Apply(IReadOnlyOperations& operations);
--- a/OrthancServer/Sources/ServerContext.cpp Wed Jan 24 21:58:14 2024 +0100 +++ b/OrthancServer/Sources/ServerContext.cpp Wed Jan 31 09:27:34 2024 +0100 @@ -710,9 +710,29 @@ typedef std::map<MetadataType, std::string> InstanceMetadata; InstanceMetadata instanceMetadata; - result.SetStatus(index_.Store( - instanceMetadata, summary, attachments, dicom.GetMetadata(), dicom.GetOrigin(), overwrite, - hasTransferSyntax, transferSyntax, hasPixelDataOffset, pixelDataOffset, pixelDataVR, isReconstruct)); + + try + { + result.SetStatus(index_.Store( + instanceMetadata, summary, attachments, dicom.GetMetadata(), dicom.GetOrigin(), overwrite, + hasTransferSyntax, transferSyntax, hasPixelDataOffset, pixelDataOffset, pixelDataVR, isReconstruct)); + } + catch (OrthancException& ex) + { + if (ex.GetErrorCode() == ErrorCode_DuplicateResource) + { + LOG(WARNING) << "Duplicate instance, deleting the attachments"; + + accessor.Remove(dicomInfo); + + if (dicomUntilPixelData.IsValid()) + { + accessor.Remove(dicomUntilPixelData); + } + } + + throw; + } // Only keep the metadata for the "instance" level dicom.ClearMetadata();
--- a/OrthancServer/Sources/ServerIndex.cpp Wed Jan 24 21:58:14 2024 +0100 +++ b/OrthancServer/Sources/ServerIndex.cpp Wed Jan 31 09:27:34 2024 +0100 @@ -331,7 +331,7 @@ // execution of Orthanc StandaloneRecycling(maximumStorageMode_, maximumStorageSize_, maximumPatients_); - if (HasFlushToDisk()) + if (GetDatabaseCapabilities().HasFlushToDisk()) { flushThread_ = boost::thread(FlushThread, this, threadSleepGranularityMilliseconds); }
--- a/OrthancServer/Sources/main.cpp Wed Jan 24 21:58:14 2024 +0100 +++ b/OrthancServer/Sources/main.cpp Wed Jan 31 09:27:34 2024 +0100 @@ -820,6 +820,7 @@ PrintErrorCode(ErrorCode_Revision, "A bad revision number was provided, which might indicate conflict between multiple writers"); PrintErrorCode(ErrorCode_MainDicomTagsMultiplyDefined, "A main DICOM Tag has been defined multiple times for the same resource level"); PrintErrorCode(ErrorCode_ForbiddenAccess, "Access to a resource is forbidden"); + PrintErrorCode(ErrorCode_DuplicateResource, "Duplicate resource"); PrintErrorCode(ErrorCode_SQLiteNotOpened, "SQLite: The database is not opened"); PrintErrorCode(ErrorCode_SQLiteAlreadyOpened, "SQLite: Connection is already open"); PrintErrorCode(ErrorCode_SQLiteCannotOpen, "SQLite: Unable to open the database"); @@ -1643,7 +1644,7 @@ if (lock.GetConfiguration().GetBooleanParameter(CHECK_REVISIONS, false)) { - if (database.HasRevisionsSupport()) + if (database.GetDatabaseCapabilities().HasRevisionsSupport()) { LOG(INFO) << "Handling of revisions is enabled, and the custom database back-end *has* " << "support for revisions of metadata and attachments"; @@ -1666,11 +1667,18 @@ } } - if (!database.HasLabelsSupport()) + if (!database.GetDatabaseCapabilities().HasLabelsSupport()) { LOG(WARNING) << "The custom database back-end has *no* support for labels"; } + if (database.GetDatabaseCapabilities().HasMeasureLatency()) + { + uint64_t latency = database.MeasureLatency(); + LOG(WARNING) << "The DB latency is " << latency << " µs"; + } + + bool success = ConfigureServerContext(database, storageArea, plugins, loadJobsFromDatabase); database.Close();