comparison OrthancServer/Sources/ServerIndex.cpp @ 4577:a114a5db2afe db-changes

end of refactoring read-write transactions
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 09 Mar 2021 11:52:07 +0100
parents f6cd49af7526
children 710748828b6a
comparison
equal deleted inserted replaced
4576:f6cd49af7526 4577:a114a5db2afe
110 std::string remainingPublicId_; 110 std::string remainingPublicId_;
111 std::list<FileToRemove> pendingFilesToRemove_; 111 std::list<FileToRemove> pendingFilesToRemove_;
112 std::list<ServerIndexChange> pendingChanges_; 112 std::list<ServerIndexChange> pendingChanges_;
113 uint64_t sizeOfFilesToRemove_; 113 uint64_t sizeOfFilesToRemove_;
114 bool insideTransaction_; 114 bool insideTransaction_;
115 uint64_t sizeOfAddedAttachments_;
115 116
116 void Reset() 117 void Reset()
117 { 118 {
118 sizeOfFilesToRemove_ = 0; 119 sizeOfFilesToRemove_ = 0;
119 hasRemainingLevel_ = false; 120 hasRemainingLevel_ = false;
120 pendingFilesToRemove_.clear(); 121 pendingFilesToRemove_.clear();
121 pendingChanges_.clear(); 122 pendingChanges_.clear();
123 sizeOfAddedAttachments_ = 0;
122 } 124 }
123 125
124 public: 126 public:
125 explicit Listener(ServerContext& context) : 127 explicit Listener(ServerContext& context) :
126 context_(context), 128 context_(context),
127 insideTransaction_(false) 129 insideTransaction_(false),
130 sizeOfAddedAttachments_(0)
128 { 131 {
129 Reset(); 132 Reset();
130 assert(ResourceType_Patient < ResourceType_Study && 133 assert(ResourceType_Patient < ResourceType_Study &&
131 ResourceType_Study < ResourceType_Series && 134 ResourceType_Study < ResourceType_Series &&
132 ResourceType_Series < ResourceType_Instance); 135 ResourceType_Series < ResourceType_Instance);
224 { 227 {
225 context_.SignalChange(change); 228 context_.SignalChange(change);
226 } 229 }
227 } 230 }
228 231
232 void SignalAttachmentsAdded(uint64_t compressedSize)
233 {
234 sizeOfAddedAttachments_ += compressedSize;
235 }
236
229 bool HasRemainingLevel() const 237 bool HasRemainingLevel() const
230 { 238 {
231 return hasRemainingLevel_; 239 return hasRemainingLevel_;
232 } 240 }
233 241
239 247
240 const std::string& GetRemainingPublicId() const 248 const std::string& GetRemainingPublicId() const
241 { 249 {
242 assert(HasRemainingLevel()); 250 assert(HasRemainingLevel());
243 return remainingPublicId_; 251 return remainingPublicId_;
244 } 252 }
253
254 uint64_t GetSizeOfAddedAttachments() const
255 {
256 return sizeOfAddedAttachments_;
257 }
245 }; 258 };
246 259
247 260
248 class ServerIndex::Transaction 261 class ServerIndex::Transaction
249 { 262 {
250 private: 263 private:
251 ServerIndex& index_; 264 ServerIndex& index_;
252 std::unique_ptr<IDatabaseWrapper::ITransaction> transaction_; 265 std::unique_ptr<IDatabaseWrapper::ITransaction> transaction_;
253 bool isCommitted_; 266 bool isCommitted_;
254 uint64_t sizeOfAddedAttachments_;
255 267
256 public: 268 public:
257 explicit Transaction(ServerIndex& index, 269 explicit Transaction(ServerIndex& index,
258 TransactionType type) : 270 TransactionType type) :
259 index_(index), 271 index_(index),
260 isCommitted_(false), 272 isCommitted_(false)
261 sizeOfAddedAttachments_(0)
262 { 273 {
263 transaction_.reset(index_.db_.StartTransaction(type)); 274 transaction_.reset(index_.db_.StartTransaction(type));
264 index_.listener_->StartTransaction(); 275 index_.listener_->StartTransaction();
265 } 276 }
266 277
272 { 283 {
273 transaction_->Rollback(); 284 transaction_->Rollback();
274 } 285 }
275 } 286 }
276 287
277 void SignalAttachmentsAdded(uint64_t compressedSize)
278 {
279 sizeOfAddedAttachments_ += compressedSize;
280 }
281
282 void Commit() 288 void Commit()
283 { 289 {
284 if (!isCommitted_) 290 if (!isCommitted_)
285 { 291 {
286 int64_t delta = (static_cast<int64_t>(sizeOfAddedAttachments_) - 292 int64_t delta = (static_cast<int64_t>(index_.listener_->GetSizeOfAddedAttachments()) -
287 static_cast<int64_t>(index_.listener_->GetSizeOfFilesToRemove())); 293 static_cast<int64_t>(index_.listener_->GetSizeOfFilesToRemove()));
288 294
289 transaction_->Commit(delta); 295 transaction_->Commit(delta);
290 296
291 // We can remove the files once the SQLite transaction has 297 // We can remove the files once the SQLite transaction has
450 unsigned int threadSleep) 456 unsigned int threadSleep)
451 { 457 {
452 // By default, wait for 10 seconds before flushing 458 // By default, wait for 10 seconds before flushing
453 unsigned int sleep = 10; 459 unsigned int sleep = 10;
454 460
461 // TODO - REMOVE THIS
455 try 462 try
456 { 463 {
457 boost::mutex::scoped_lock lock(that->mutex_); 464 boost::mutex::scoped_lock lock(that->monitoringMutex_);
458 std::string sleepString; 465 std::string sleepString;
459 466
460 if (that->db_.LookupGlobalProperty(sleepString, GlobalProperty_FlushSleep) && 467 if (that->db_.LookupGlobalProperty(sleepString, GlobalProperty_FlushSleep) &&
461 Toolbox::IsInteger(sleepString)) 468 Toolbox::IsInteger(sleepString))
462 { 469 {
480 continue; 487 continue;
481 } 488 }
482 489
483 Logging::Flush(); 490 Logging::Flush();
484 491
485 boost::mutex::scoped_lock lock(that->mutex_); 492 boost::mutex::scoped_lock lock(that->monitoringMutex_);
486 493
487 try 494 try
488 { 495 {
489 that->db_.FlushToDisk(); 496 that->db_.FlushToDisk();
490 } 497 }
633 listener_.reset(new Listener(context)); 640 listener_.reset(new Listener(context));
634 db_.SetListener(*listener_); 641 db_.SetListener(*listener_);
635 642
636 // Initial recycling if the parameters have changed since the last 643 // Initial recycling if the parameters have changed since the last
637 // execution of Orthanc 644 // execution of Orthanc
638 StandaloneRecycling(); 645 StandaloneRecycling(maximumStorageSize_, maximumPatients_);
639 646
640 if (db.HasFlushToDisk()) 647 if (db.HasFlushToDisk())
641 { 648 {
642 flushThread_ = boost::thread(FlushThread, this, threadSleep); 649 flushThread_ = boost::thread(FlushThread, this, threadSleep);
643 } 650 }
677 } 684 }
678 } 685 }
679 } 686 }
680 687
681 688
682 static void SetInstanceMetadata(ResourcesContent& content, 689
683 std::map<MetadataType, std::string>& instanceMetadata, 690 SeriesStatus ServerIndex::ReadOnlyTransaction::GetSeriesStatus(int64_t id,
684 int64_t instance, 691 int64_t expectedNumberOfInstances)
685 MetadataType metadata,
686 const std::string& value)
687 {
688 content.AddMetadata(instance, metadata, value);
689 instanceMetadata[metadata] = value;
690 }
691
692
693 StoreStatus ServerIndex::Store(std::map<MetadataType, std::string>& instanceMetadata,
694 const DicomMap& dicomSummary,
695 const Attachments& attachments,
696 const MetadataMap& metadata,
697 const DicomInstanceOrigin& origin,
698 bool overwrite,
699 bool hasTransferSyntax,
700 DicomTransferSyntax transferSyntax,
701 bool hasPixelDataOffset,
702 uint64_t pixelDataOffset)
703 {
704 boost::mutex::scoped_lock lock(mutex_);
705
706 int64_t expectedInstances;
707 const bool hasExpectedInstances =
708 ComputeExpectedNumberOfInstances(expectedInstances, dicomSummary);
709
710 instanceMetadata.clear();
711
712 DicomInstanceHasher hasher(dicomSummary);
713 const std::string hashPatient = hasher.HashPatient();
714 const std::string hashStudy = hasher.HashStudy();
715 const std::string hashSeries = hasher.HashSeries();
716 const std::string hashInstance = hasher.HashInstance();
717
718 try
719 {
720 Transaction t(*this, TransactionType_ReadWrite);
721
722 IDatabaseWrapper::CreateInstanceResult status;
723 int64_t instanceId;
724
725 // Check whether this instance is already stored
726 if (!db_.CreateInstance(status, instanceId, hashPatient,
727 hashStudy, hashSeries, hashInstance))
728 {
729 // The instance already exists
730
731 if (overwrite)
732 {
733 // Overwrite the old instance
734 LOG(INFO) << "Overwriting instance: " << hashInstance;
735 db_.DeleteResource(instanceId);
736
737 // Re-create the instance, now that the old one is removed
738 if (!db_.CreateInstance(status, instanceId, hashPatient,
739 hashStudy, hashSeries, hashInstance))
740 {
741 throw OrthancException(ErrorCode_InternalError);
742 }
743 }
744 else
745 {
746 // Do nothing if the instance already exists and overwriting is disabled
747 db_.GetAllMetadata(instanceMetadata, instanceId);
748 return StoreStatus_AlreadyStored;
749 }
750 }
751
752
753 // Warn about the creation of new resources. The order must be
754 // from instance to patient.
755
756 // NB: In theory, could be sped up by grouping the underlying
757 // calls to "db_.LogChange()". However, this would only have an
758 // impact when new patient/study/series get created, which
759 // occurs far less often that creating new instances. The
760 // positive impact looks marginal in practice.
761 LogChange(instanceId, ChangeType_NewInstance, ResourceType_Instance, hashInstance);
762
763 if (status.isNewSeries_)
764 {
765 LogChange(status.seriesId_, ChangeType_NewSeries, ResourceType_Series, hashSeries);
766 }
767
768 if (status.isNewStudy_)
769 {
770 LogChange(status.studyId_, ChangeType_NewStudy, ResourceType_Study, hashStudy);
771 }
772
773 if (status.isNewPatient_)
774 {
775 LogChange(status.patientId_, ChangeType_NewPatient, ResourceType_Patient, hashPatient);
776 }
777
778
779 // Ensure there is enough room in the storage for the new instance
780 uint64_t instanceSize = 0;
781 for (Attachments::const_iterator it = attachments.begin();
782 it != attachments.end(); ++it)
783 {
784 instanceSize += it->GetCompressedSize();
785 }
786
787 Recycle(instanceSize, hashPatient /* don't consider the current patient for recycling */);
788
789
790 // Attach the files to the newly created instance
791 for (Attachments::const_iterator it = attachments.begin();
792 it != attachments.end(); ++it)
793 {
794 db_.AddAttachment(instanceId, *it);
795 }
796
797
798 {
799 ResourcesContent content;
800
801 // Populate the tags of the newly-created resources
802
803 content.AddResource(instanceId, ResourceType_Instance, dicomSummary);
804
805 if (status.isNewSeries_)
806 {
807 content.AddResource(status.seriesId_, ResourceType_Series, dicomSummary);
808 }
809
810 if (status.isNewStudy_)
811 {
812 content.AddResource(status.studyId_, ResourceType_Study, dicomSummary);
813 }
814
815 if (status.isNewPatient_)
816 {
817 content.AddResource(status.patientId_, ResourceType_Patient, dicomSummary);
818 }
819
820
821 // Attach the user-specified metadata
822
823 for (MetadataMap::const_iterator
824 it = metadata.begin(); it != metadata.end(); ++it)
825 {
826 switch (it->first.first)
827 {
828 case ResourceType_Patient:
829 content.AddMetadata(status.patientId_, it->first.second, it->second);
830 break;
831
832 case ResourceType_Study:
833 content.AddMetadata(status.studyId_, it->first.second, it->second);
834 break;
835
836 case ResourceType_Series:
837 content.AddMetadata(status.seriesId_, it->first.second, it->second);
838 break;
839
840 case ResourceType_Instance:
841 SetInstanceMetadata(content, instanceMetadata, instanceId,
842 it->first.second, it->second);
843 break;
844
845 default:
846 throw OrthancException(ErrorCode_ParameterOutOfRange);
847 }
848 }
849
850
851 // Attach the auto-computed metadata for the patient/study/series levels
852 std::string now = SystemToolbox::GetNowIsoString(true /* use UTC time (not local time) */);
853 content.AddMetadata(status.seriesId_, MetadataType_LastUpdate, now);
854 content.AddMetadata(status.studyId_, MetadataType_LastUpdate, now);
855 content.AddMetadata(status.patientId_, MetadataType_LastUpdate, now);
856
857 if (status.isNewSeries_)
858 {
859 if (hasExpectedInstances)
860 {
861 content.AddMetadata(status.seriesId_, MetadataType_Series_ExpectedNumberOfInstances,
862 boost::lexical_cast<std::string>(expectedInstances));
863 }
864
865 // New in Orthanc 1.9.0
866 content.AddMetadata(status.seriesId_, MetadataType_RemoteAet,
867 origin.GetRemoteAetC());
868 }
869
870
871 // Attach the auto-computed metadata for the instance level,
872 // reflecting these additions into the input metadata map
873 SetInstanceMetadata(content, instanceMetadata, instanceId,
874 MetadataType_Instance_ReceptionDate, now);
875 SetInstanceMetadata(content, instanceMetadata, instanceId, MetadataType_RemoteAet,
876 origin.GetRemoteAetC());
877 SetInstanceMetadata(content, instanceMetadata, instanceId, MetadataType_Instance_Origin,
878 EnumerationToString(origin.GetRequestOrigin()));
879
880
881 if (hasTransferSyntax)
882 {
883 // New in Orthanc 1.2.0
884 SetInstanceMetadata(content, instanceMetadata, instanceId,
885 MetadataType_Instance_TransferSyntax,
886 GetTransferSyntaxUid(transferSyntax));
887 }
888
889 {
890 std::string s;
891
892 if (origin.LookupRemoteIp(s))
893 {
894 // New in Orthanc 1.4.0
895 SetInstanceMetadata(content, instanceMetadata, instanceId,
896 MetadataType_Instance_RemoteIp, s);
897 }
898
899 if (origin.LookupCalledAet(s))
900 {
901 // New in Orthanc 1.4.0
902 SetInstanceMetadata(content, instanceMetadata, instanceId,
903 MetadataType_Instance_CalledAet, s);
904 }
905
906 if (origin.LookupHttpUsername(s))
907 {
908 // New in Orthanc 1.4.0
909 SetInstanceMetadata(content, instanceMetadata, instanceId,
910 MetadataType_Instance_HttpUsername, s);
911 }
912 }
913
914 if (hasPixelDataOffset)
915 {
916 // New in Orthanc 1.9.1
917 SetInstanceMetadata(content, instanceMetadata, instanceId,
918 MetadataType_Instance_PixelDataOffset,
919 boost::lexical_cast<std::string>(pixelDataOffset));
920 }
921
922 const DicomValue* value;
923 if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_SOP_CLASS_UID)) != NULL &&
924 !value->IsNull() &&
925 !value->IsBinary())
926 {
927 SetInstanceMetadata(content, instanceMetadata, instanceId,
928 MetadataType_Instance_SopClassUid, value->GetContent());
929 }
930
931
932 if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_INSTANCE_NUMBER)) != NULL ||
933 (value = dicomSummary.TestAndGetValue(DICOM_TAG_IMAGE_INDEX)) != NULL)
934 {
935 if (!value->IsNull() &&
936 !value->IsBinary())
937 {
938 SetInstanceMetadata(content, instanceMetadata, instanceId,
939 MetadataType_Instance_IndexInSeries, Toolbox::StripSpaces(value->GetContent()));
940 }
941 }
942
943
944 db_.SetResourcesContent(content);
945 }
946
947
948 // Check whether the series of this new instance is now completed
949 int64_t expectedNumberOfInstances;
950 if (ComputeExpectedNumberOfInstances(expectedNumberOfInstances, dicomSummary))
951 {
952 SeriesStatus seriesStatus = GetSeriesStatus(db_, status.seriesId_, expectedNumberOfInstances);
953 if (seriesStatus == SeriesStatus_Complete)
954 {
955 LogChange(status.seriesId_, ChangeType_CompletedSeries, ResourceType_Series, hashSeries);
956 }
957 }
958
959
960 // Mark the parent resources of this instance as unstable
961 MarkAsUnstable(status.seriesId_, ResourceType_Series, hashSeries);
962 MarkAsUnstable(status.studyId_, ResourceType_Study, hashStudy);
963 MarkAsUnstable(status.patientId_, ResourceType_Patient, hashPatient);
964
965 t.SignalAttachmentsAdded(instanceSize);
966 t.Commit();
967
968 return StoreStatus_Success;
969 }
970 catch (OrthancException& e)
971 {
972 LOG(ERROR) << "EXCEPTION [" << e.What() << "]";
973 }
974
975 return StoreStatus_Failure;
976 }
977
978
979 SeriesStatus ServerIndex::GetSeriesStatus(IDatabaseWrapper& db,
980 int64_t id,
981 int64_t expectedNumberOfInstances)
982 { 692 {
983 std::list<std::string> values; 693 std::list<std::string> values;
984 db.GetChildrenMetadata(values, id, MetadataType_Instance_IndexInSeries); 694 db_.GetChildrenMetadata(values, id, MetadataType_Instance_IndexInSeries);
985 695
986 std::set<int64_t> instances; 696 std::set<int64_t> instances;
987 697
988 for (std::list<std::string>::const_iterator 698 for (std::list<std::string>::const_iterator
989 it = values.begin(); it != values.end(); ++it) 699 it = values.begin(); it != values.end(); ++it)
1090 800
1091 target["Last"] = static_cast<int>(last); 801 target["Last"] = static_cast<int>(last);
1092 } 802 }
1093 803
1094 804
1095 bool ServerIndex::IsRecyclingNeeded(uint64_t instanceSize) 805 void ServerIndex::SetMaximumPatientCount(unsigned int count)
1096 { 806 {
1097 if (maximumStorageSize_ != 0) 807 {
1098 { 808 boost::mutex::scoped_lock lock(monitoringMutex_);
1099 assert(maximumStorageSize_ >= instanceSize); 809 maximumPatients_ = count;
1100 810
1101 if (db_.IsDiskSizeAbove(maximumStorageSize_ - instanceSize)) 811 if (count == 0)
1102 { 812 {
1103 return true; 813 LOG(WARNING) << "No limit on the number of stored patients";
1104 }
1105 }
1106
1107 if (maximumPatients_ != 0)
1108 {
1109 uint64_t patientCount = db_.GetResourceCount(ResourceType_Patient);
1110 if (patientCount > maximumPatients_)
1111 {
1112 return true;
1113 }
1114 }
1115
1116 return false;
1117 }
1118
1119
1120 void ServerIndex::Recycle(uint64_t instanceSize,
1121 const std::string& newPatientId)
1122 {
1123 if (IsRecyclingNeeded(instanceSize))
1124 {
1125 // Check whether other DICOM instances from this patient are
1126 // already stored
1127 int64_t patientToAvoid;
1128 bool hasPatientToAvoid;
1129
1130 if (newPatientId.empty())
1131 {
1132 hasPatientToAvoid = false;
1133 } 814 }
1134 else 815 else
1135 { 816 {
1136 ResourceType type; 817 LOG(WARNING) << "At most " << count << " patients will be stored";
1137 hasPatientToAvoid = db_.LookupResource(patientToAvoid, type, newPatientId); 818 }
1138 if (type != ResourceType_Patient) 819 }
1139 { 820
1140 throw OrthancException(ErrorCode_InternalError); 821 StandaloneRecycling(maximumStorageSize_, maximumPatients_);
1141 } 822 }
1142 } 823
1143 824 void ServerIndex::SetMaximumStorageSize(uint64_t size)
1144 // Iteratively select patient to remove until there is enough 825 {
1145 // space in the DICOM store 826 {
1146 int64_t patientToRecycle; 827 boost::mutex::scoped_lock lock(monitoringMutex_);
1147 while (true) 828 maximumStorageSize_ = size;
1148 {
1149 // If other instances of this patient are already in the store,
1150 // we must avoid to recycle them
1151 bool ok = (hasPatientToAvoid ?
1152 db_.SelectPatientToRecycle(patientToRecycle, patientToAvoid) :
1153 db_.SelectPatientToRecycle(patientToRecycle));
1154
1155 if (!ok)
1156 {
1157 throw OrthancException(ErrorCode_FullStorage);
1158 }
1159 829
1160 LOG(TRACE) << "Recycling one patient"; 830 if (size == 0)
1161 db_.DeleteResource(patientToRecycle); 831 {
1162 832 LOG(WARNING) << "No limit on the size of the storage area";
1163 if (!IsRecyclingNeeded(instanceSize)) 833 }
1164 { 834 else
1165 // OK, we're done 835 {
1166 break; 836 LOG(WARNING) << "At most " << (size / MEGA_BYTES) << "MB will be used for the storage area";
1167 } 837 }
1168 } 838 }
1169 } 839
1170 } 840 StandaloneRecycling(maximumStorageSize_, maximumPatients_);
1171
1172
1173 void ServerIndex::SetMaximumPatientCount(unsigned int count)
1174 {
1175 boost::mutex::scoped_lock lock(mutex_);
1176 maximumPatients_ = count;
1177
1178 if (count == 0)
1179 {
1180 LOG(WARNING) << "No limit on the number of stored patients";
1181 }
1182 else
1183 {
1184 LOG(WARNING) << "At most " << count << " patients will be stored";
1185 }
1186
1187 StandaloneRecycling();
1188 }
1189
1190 void ServerIndex::SetMaximumStorageSize(uint64_t size)
1191 {
1192 boost::mutex::scoped_lock lock(mutex_);
1193 maximumStorageSize_ = size;
1194
1195 if (size == 0)
1196 {
1197 LOG(WARNING) << "No limit on the size of the storage area";
1198 }
1199 else
1200 {
1201 LOG(WARNING) << "At most " << (size / MEGA_BYTES) << "MB will be used for the storage area";
1202 }
1203
1204 StandaloneRecycling();
1205 }
1206
1207
1208 void ServerIndex::StandaloneRecycling()
1209 {
1210 // WARNING: No mutex here, do not include this as a public method
1211 Transaction t(*this, TransactionType_ReadWrite);
1212 Recycle(0, "");
1213 t.Commit();
1214 } 841 }
1215 842
1216 843
1217 void ServerIndex::UnstableResourcesMonitorThread(ServerIndex* that, 844 void ServerIndex::UnstableResourcesMonitorThread(ServerIndex* that,
1218 unsigned int threadSleep) 845 unsigned int threadSleep)
1234 while (!that->done_) 861 while (!that->done_)
1235 { 862 {
1236 // Check for stable resources each few seconds 863 // Check for stable resources each few seconds
1237 boost::this_thread::sleep(boost::posix_time::milliseconds(threadSleep)); 864 boost::this_thread::sleep(boost::posix_time::milliseconds(threadSleep));
1238 865
1239 boost::mutex::scoped_lock lock(that->mutex_); 866 boost::mutex::scoped_lock lock(that->monitoringMutex_);
1240 867
1241 while (!that->unstableResources_.IsEmpty() && 868 while (!that->unstableResources_.IsEmpty() &&
1242 that->unstableResources_.GetOldestPayload().GetAge() > static_cast<unsigned int>(stableAge)) 869 that->unstableResources_.GetOldestPayload().GetAge() > static_cast<unsigned int>(stableAge))
1243 { 870 {
1244 // This DICOM resource has not received any new instance for 871 // This DICOM resource has not received any new instance for
1279 906
1280 void ServerIndex::MarkAsUnstable(int64_t id, 907 void ServerIndex::MarkAsUnstable(int64_t id,
1281 Orthanc::ResourceType type, 908 Orthanc::ResourceType type,
1282 const std::string& publicId) 909 const std::string& publicId)
1283 { 910 {
1284 // WARNING: Before calling this method, "mutex_" must be locked. 911 // WARNING: Before calling this method, "monitoringMutex_" must be locked.
1285 912
1286 assert(type == Orthanc::ResourceType_Patient || 913 assert(type == Orthanc::ResourceType_Patient ||
1287 type == Orthanc::ResourceType_Study || 914 type == Orthanc::ResourceType_Study ||
1288 type == Orthanc::ResourceType_Series); 915 type == Orthanc::ResourceType_Series);
1289 916
1292 //LOG(INFO) << "Unstable resource: " << EnumerationToString(type) << " " << id; 919 //LOG(INFO) << "Unstable resource: " << EnumerationToString(type) << " " << id;
1293 920
1294 LogChange(id, ChangeType_NewChildInstance, type, publicId); 921 LogChange(id, ChangeType_NewChildInstance, type, publicId);
1295 } 922 }
1296 923
1297
1298
1299 StoreStatus ServerIndex::AddAttachment(const FileInfo& attachment,
1300 const std::string& publicId)
1301 {
1302 boost::mutex::scoped_lock lock(mutex_);
1303
1304 Transaction t(*this, TransactionType_ReadWrite);
1305
1306 ResourceType resourceType;
1307 int64_t resourceId;
1308 if (!db_.LookupResource(resourceId, resourceType, publicId))
1309 {
1310 return StoreStatus_Failure; // Inexistent resource
1311 }
1312
1313 // Remove possible previous attachment
1314 db_.DeleteAttachment(resourceId, attachment.GetContentType());
1315
1316 // Locate the patient of the target resource
1317 int64_t patientId = resourceId;
1318 for (;;)
1319 {
1320 int64_t parent;
1321 if (db_.LookupParent(parent, patientId))
1322 {
1323 // We have not reached the patient level yet
1324 patientId = parent;
1325 }
1326 else
1327 {
1328 // We have reached the patient level
1329 break;
1330 }
1331 }
1332
1333 // Possibly apply the recycling mechanism while preserving this patient
1334 assert(db_.GetResourceType(patientId) == ResourceType_Patient);
1335 Recycle(attachment.GetCompressedSize(), db_.GetPublicId(patientId));
1336
1337 db_.AddAttachment(resourceId, attachment);
1338
1339 if (IsUserContentType(attachment.GetContentType()))
1340 {
1341 LogChange(resourceId, ChangeType_UpdatedAttachment, resourceType, publicId);
1342 }
1343
1344 t.SignalAttachmentsAdded(attachment.GetCompressedSize());
1345 t.Commit();
1346
1347 return StoreStatus_Success;
1348 }
1349 924
1350 925
1351 void ServerIndex::NormalizeLookup(std::vector<DatabaseConstraint>& target, 926 void ServerIndex::NormalizeLookup(std::vector<DatabaseConstraint>& target,
1352 const DatabaseLookup& source, 927 const DatabaseLookup& source,
1353 ResourceType queryLevel) const 928 ResourceType queryLevel) const
1599 1174
1600 for (;;) 1175 for (;;)
1601 { 1176 {
1602 try 1177 try
1603 { 1178 {
1604 boost::mutex::scoped_lock lock(mutex_); // TODO - REMOVE 1179 boost::mutex::scoped_lock lock(monitoringMutex_); // TODO - REMOVE
1605 1180
1606 if (readOperations != NULL) 1181 if (readOperations != NULL)
1607 { 1182 {
1608 /** 1183 /**
1609 * IMPORTANT: In Orthanc <= 1.9.1, there was no transaction 1184 * IMPORTANT: In Orthanc <= 1.9.1, there was no transaction
3459 }; 3034 };
3460 3035
3461 Operations operations(dicom); 3036 Operations operations(dicom);
3462 Apply(operations); 3037 Apply(operations);
3463 } 3038 }
3039
3040
3041 static bool IsRecyclingNeeded(IDatabaseWrapper& db,
3042 uint64_t maximumStorageSize,
3043 unsigned int maximumPatients,
3044 uint64_t addedInstanceSize)
3045 {
3046 if (maximumStorageSize != 0)
3047 {
3048 if (maximumStorageSize < addedInstanceSize)
3049 {
3050 throw OrthancException(ErrorCode_FullStorage, "Cannot store an instance of size " +
3051 boost::lexical_cast<std::string>(addedInstanceSize) +
3052 " bytes in a storage area limited to " +
3053 boost::lexical_cast<std::string>(maximumStorageSize));
3054 }
3055
3056 if (db.IsDiskSizeAbove(maximumStorageSize - addedInstanceSize))
3057 {
3058 return true;
3059 }
3060 }
3061
3062 if (maximumPatients != 0)
3063 {
3064 uint64_t patientCount = db.GetResourceCount(ResourceType_Patient);
3065 if (patientCount > maximumPatients)
3066 {
3067 return true;
3068 }
3069 }
3070
3071 return false;
3072 }
3073
3074
3075 void ServerIndex::ReadWriteTransaction::Recycle(uint64_t maximumStorageSize,
3076 unsigned int maximumPatients,
3077 uint64_t addedInstanceSize,
3078 const std::string& newPatientId)
3079 {
3080 // TODO - Performance: Avoid calls to "IsRecyclingNeeded()"
3081
3082 if (IsRecyclingNeeded(db_, maximumStorageSize, maximumPatients, addedInstanceSize))
3083 {
3084 // Check whether other DICOM instances from this patient are
3085 // already stored
3086 int64_t patientToAvoid;
3087 bool hasPatientToAvoid;
3088
3089 if (newPatientId.empty())
3090 {
3091 hasPatientToAvoid = false;
3092 }
3093 else
3094 {
3095 ResourceType type;
3096 hasPatientToAvoid = db_.LookupResource(patientToAvoid, type, newPatientId);
3097 if (type != ResourceType_Patient)
3098 {
3099 throw OrthancException(ErrorCode_InternalError);
3100 }
3101 }
3102
3103 // Iteratively select patient to remove until there is enough
3104 // space in the DICOM store
3105 int64_t patientToRecycle;
3106 while (true)
3107 {
3108 // If other instances of this patient are already in the store,
3109 // we must avoid to recycle them
3110 bool ok = (hasPatientToAvoid ?
3111 db_.SelectPatientToRecycle(patientToRecycle, patientToAvoid) :
3112 db_.SelectPatientToRecycle(patientToRecycle));
3113
3114 if (!ok)
3115 {
3116 throw OrthancException(ErrorCode_FullStorage);
3117 }
3118
3119 LOG(TRACE) << "Recycling one patient";
3120 db_.DeleteResource(patientToRecycle);
3121
3122 if (!IsRecyclingNeeded(db_, maximumStorageSize, maximumPatients, addedInstanceSize))
3123 {
3124 // OK, we're done
3125 return;
3126 }
3127 }
3128 }
3129 }
3130
3131
3132 void ServerIndex::StandaloneRecycling(uint64_t maximumStorageSize,
3133 unsigned int maximumPatientCount)
3134 {
3135 class Operations : public IReadWriteOperations
3136 {
3137 private:
3138 uint64_t maximumStorageSize_;
3139 unsigned int maximumPatientCount_;
3140
3141 public:
3142 Operations(uint64_t maximumStorageSize,
3143 unsigned int maximumPatientCount) :
3144 maximumStorageSize_(maximumStorageSize),
3145 maximumPatientCount_(maximumPatientCount)
3146 {
3147 }
3148
3149 virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
3150 {
3151 transaction.Recycle(maximumStorageSize_, maximumPatientCount_, 0, "");
3152 }
3153 };
3154
3155 Operations operations(maximumStorageSize, maximumPatientCount);
3156 Apply(operations);
3157 }
3158
3159
3160 static void SetInstanceMetadata(ResourcesContent& content,
3161 std::map<MetadataType, std::string>& instanceMetadata,
3162 int64_t instance,
3163 MetadataType metadata,
3164 const std::string& value)
3165 {
3166 content.AddMetadata(instance, metadata, value);
3167 instanceMetadata[metadata] = value;
3168 }
3169
3170
3171 StoreStatus ServerIndex::Store(std::map<MetadataType, std::string>& instanceMetadata,
3172 const DicomMap& dicomSummary,
3173 const Attachments& attachments,
3174 const MetadataMap& metadata,
3175 const DicomInstanceOrigin& origin,
3176 bool overwrite,
3177 bool hasTransferSyntax,
3178 DicomTransferSyntax transferSyntax,
3179 bool hasPixelDataOffset,
3180 uint64_t pixelDataOffset)
3181 {
3182 class Operations : public IReadWriteOperations
3183 {
3184 private:
3185 StoreStatus storeStatus_;
3186 std::map<MetadataType, std::string>& instanceMetadata_;
3187 ServerIndex& index_;
3188 const DicomMap& dicomSummary_;
3189 const Attachments& attachments_;
3190 const MetadataMap& metadata_;
3191 const DicomInstanceOrigin& origin_;
3192 bool overwrite_;
3193 bool hasTransferSyntax_;
3194 DicomTransferSyntax transferSyntax_;
3195 bool hasPixelDataOffset_;
3196 uint64_t pixelDataOffset_;
3197 uint64_t maximumStorageSize_;
3198 unsigned int maximumPatientCount_;
3199
3200 // Auto-computed fields
3201 bool hasExpectedInstances_;
3202 int64_t expectedInstances_;
3203 std::string hashPatient_;
3204 std::string hashStudy_;
3205 std::string hashSeries_;
3206 std::string hashInstance_;
3207
3208 public:
3209 Operations(std::map<MetadataType, std::string>& instanceMetadata,
3210 ServerIndex& index,
3211 const DicomMap& dicomSummary,
3212 const Attachments& attachments,
3213 const MetadataMap& metadata,
3214 const DicomInstanceOrigin& origin,
3215 bool overwrite,
3216 bool hasTransferSyntax,
3217 DicomTransferSyntax transferSyntax,
3218 bool hasPixelDataOffset,
3219 uint64_t pixelDataOffset,
3220 uint64_t maximumStorageSize,
3221 unsigned int maximumPatientCount) :
3222 storeStatus_(StoreStatus_Failure),
3223 instanceMetadata_(instanceMetadata),
3224 index_(index),
3225 dicomSummary_(dicomSummary),
3226 attachments_(attachments),
3227 metadata_(metadata),
3228 origin_(origin),
3229 overwrite_(overwrite),
3230 hasTransferSyntax_(hasTransferSyntax),
3231 transferSyntax_(transferSyntax),
3232 hasPixelDataOffset_(hasPixelDataOffset),
3233 pixelDataOffset_(pixelDataOffset),
3234 maximumStorageSize_(maximumStorageSize),
3235 maximumPatientCount_(maximumPatientCount)
3236 {
3237 hasExpectedInstances_ = ComputeExpectedNumberOfInstances(expectedInstances_, dicomSummary);
3238
3239 instanceMetadata_.clear();
3240
3241 DicomInstanceHasher hasher(dicomSummary);
3242 hashPatient_ = hasher.HashPatient();
3243 hashStudy_ = hasher.HashStudy();
3244 hashSeries_ = hasher.HashSeries();
3245 hashInstance_ = hasher.HashInstance();
3246 }
3247
3248 StoreStatus GetStoreStatus() const
3249 {
3250 return storeStatus_;
3251 }
3252
3253 virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
3254 {
3255 try
3256 {
3257 IDatabaseWrapper::CreateInstanceResult status;
3258 int64_t instanceId;
3259
3260 // Check whether this instance is already stored
3261 if (!transaction.CreateInstance(status, instanceId, hashPatient_,
3262 hashStudy_, hashSeries_, hashInstance_))
3263 {
3264 // The instance already exists
3265
3266 if (overwrite_)
3267 {
3268 // Overwrite the old instance
3269 LOG(INFO) << "Overwriting instance: " << hashInstance_;
3270 transaction.DeleteResource(instanceId);
3271
3272 // Re-create the instance, now that the old one is removed
3273 if (!transaction.CreateInstance(status, instanceId, hashPatient_,
3274 hashStudy_, hashSeries_, hashInstance_))
3275 {
3276 throw OrthancException(ErrorCode_InternalError);
3277 }
3278 }
3279 else
3280 {
3281 // Do nothing if the instance already exists and overwriting is disabled
3282 transaction.GetAllMetadata(instanceMetadata_, instanceId);
3283 storeStatus_ = StoreStatus_AlreadyStored;
3284 return;
3285 }
3286 }
3287
3288
3289 // Warn about the creation of new resources. The order must be
3290 // from instance to patient.
3291
3292 // NB: In theory, could be sped up by grouping the underlying
3293 // calls to "transaction.LogChange()". However, this would only have an
3294 // impact when new patient/study/series get created, which
3295 // occurs far less often that creating new instances. The
3296 // positive impact looks marginal in practice.
3297 transaction.LogChange(instanceId, ChangeType_NewInstance, ResourceType_Instance, hashInstance_);
3298
3299 if (status.isNewSeries_)
3300 {
3301 transaction.LogChange(status.seriesId_, ChangeType_NewSeries, ResourceType_Series, hashSeries_);
3302 }
3303
3304 if (status.isNewStudy_)
3305 {
3306 transaction.LogChange(status.studyId_, ChangeType_NewStudy, ResourceType_Study, hashStudy_);
3307 }
3308
3309 if (status.isNewPatient_)
3310 {
3311 transaction.LogChange(status.patientId_, ChangeType_NewPatient, ResourceType_Patient, hashPatient_);
3312 }
3313
3314
3315 // Ensure there is enough room in the storage for the new instance
3316 uint64_t instanceSize = 0;
3317 for (Attachments::const_iterator it = attachments_.begin();
3318 it != attachments_.end(); ++it)
3319 {
3320 instanceSize += it->GetCompressedSize();
3321 }
3322
3323 transaction.Recycle(maximumStorageSize_, maximumPatientCount_,
3324 instanceSize, hashPatient_ /* don't consider the current patient for recycling */);
3325
3326
3327 // Attach the files to the newly created instance
3328 for (Attachments::const_iterator it = attachments_.begin();
3329 it != attachments_.end(); ++it)
3330 {
3331 transaction.AddAttachment(instanceId, *it);
3332 }
3333
3334
3335 {
3336 ResourcesContent content;
3337
3338 // Populate the tags of the newly-created resources
3339
3340 content.AddResource(instanceId, ResourceType_Instance, dicomSummary_);
3341
3342 if (status.isNewSeries_)
3343 {
3344 content.AddResource(status.seriesId_, ResourceType_Series, dicomSummary_);
3345 }
3346
3347 if (status.isNewStudy_)
3348 {
3349 content.AddResource(status.studyId_, ResourceType_Study, dicomSummary_);
3350 }
3351
3352 if (status.isNewPatient_)
3353 {
3354 content.AddResource(status.patientId_, ResourceType_Patient, dicomSummary_);
3355 }
3356
3357
3358 // Attach the user-specified metadata
3359
3360 for (MetadataMap::const_iterator
3361 it = metadata_.begin(); it != metadata_.end(); ++it)
3362 {
3363 switch (it->first.first)
3364 {
3365 case ResourceType_Patient:
3366 content.AddMetadata(status.patientId_, it->first.second, it->second);
3367 break;
3368
3369 case ResourceType_Study:
3370 content.AddMetadata(status.studyId_, it->first.second, it->second);
3371 break;
3372
3373 case ResourceType_Series:
3374 content.AddMetadata(status.seriesId_, it->first.second, it->second);
3375 break;
3376
3377 case ResourceType_Instance:
3378 SetInstanceMetadata(content, instanceMetadata_, instanceId,
3379 it->first.second, it->second);
3380 break;
3381
3382 default:
3383 throw OrthancException(ErrorCode_ParameterOutOfRange);
3384 }
3385 }
3386
3387
3388 // Attach the auto-computed metadata for the patient/study/series levels
3389 std::string now = SystemToolbox::GetNowIsoString(true /* use UTC time (not local time) */);
3390 content.AddMetadata(status.seriesId_, MetadataType_LastUpdate, now);
3391 content.AddMetadata(status.studyId_, MetadataType_LastUpdate, now);
3392 content.AddMetadata(status.patientId_, MetadataType_LastUpdate, now);
3393
3394 if (status.isNewSeries_)
3395 {
3396 if (hasExpectedInstances_)
3397 {
3398 content.AddMetadata(status.seriesId_, MetadataType_Series_ExpectedNumberOfInstances,
3399 boost::lexical_cast<std::string>(expectedInstances_));
3400 }
3401
3402 // New in Orthanc 1.9.0
3403 content.AddMetadata(status.seriesId_, MetadataType_RemoteAet,
3404 origin_.GetRemoteAetC());
3405 }
3406
3407
3408 // Attach the auto-computed metadata for the instance level,
3409 // reflecting these additions into the input metadata map
3410 SetInstanceMetadata(content, instanceMetadata_, instanceId,
3411 MetadataType_Instance_ReceptionDate, now);
3412 SetInstanceMetadata(content, instanceMetadata_, instanceId, MetadataType_RemoteAet,
3413 origin_.GetRemoteAetC());
3414 SetInstanceMetadata(content, instanceMetadata_, instanceId, MetadataType_Instance_Origin,
3415 EnumerationToString(origin_.GetRequestOrigin()));
3416
3417
3418 if (hasTransferSyntax_)
3419 {
3420 // New in Orthanc 1.2.0
3421 SetInstanceMetadata(content, instanceMetadata_, instanceId,
3422 MetadataType_Instance_TransferSyntax,
3423 GetTransferSyntaxUid(transferSyntax_));
3424 }
3425
3426 {
3427 std::string s;
3428
3429 if (origin_.LookupRemoteIp(s))
3430 {
3431 // New in Orthanc 1.4.0
3432 SetInstanceMetadata(content, instanceMetadata_, instanceId,
3433 MetadataType_Instance_RemoteIp, s);
3434 }
3435
3436 if (origin_.LookupCalledAet(s))
3437 {
3438 // New in Orthanc 1.4.0
3439 SetInstanceMetadata(content, instanceMetadata_, instanceId,
3440 MetadataType_Instance_CalledAet, s);
3441 }
3442
3443 if (origin_.LookupHttpUsername(s))
3444 {
3445 // New in Orthanc 1.4.0
3446 SetInstanceMetadata(content, instanceMetadata_, instanceId,
3447 MetadataType_Instance_HttpUsername, s);
3448 }
3449 }
3450
3451 if (hasPixelDataOffset_)
3452 {
3453 // New in Orthanc 1.9.1
3454 SetInstanceMetadata(content, instanceMetadata_, instanceId,
3455 MetadataType_Instance_PixelDataOffset,
3456 boost::lexical_cast<std::string>(pixelDataOffset_));
3457 }
3458
3459 const DicomValue* value;
3460 if ((value = dicomSummary_.TestAndGetValue(DICOM_TAG_SOP_CLASS_UID)) != NULL &&
3461 !value->IsNull() &&
3462 !value->IsBinary())
3463 {
3464 SetInstanceMetadata(content, instanceMetadata_, instanceId,
3465 MetadataType_Instance_SopClassUid, value->GetContent());
3466 }
3467
3468
3469 if ((value = dicomSummary_.TestAndGetValue(DICOM_TAG_INSTANCE_NUMBER)) != NULL ||
3470 (value = dicomSummary_.TestAndGetValue(DICOM_TAG_IMAGE_INDEX)) != NULL)
3471 {
3472 if (!value->IsNull() &&
3473 !value->IsBinary())
3474 {
3475 SetInstanceMetadata(content, instanceMetadata_, instanceId,
3476 MetadataType_Instance_IndexInSeries, Toolbox::StripSpaces(value->GetContent()));
3477 }
3478 }
3479
3480
3481 transaction.SetResourcesContent(content);
3482 }
3483
3484
3485 // Check whether the series of this new instance is now completed
3486 int64_t expectedNumberOfInstances;
3487 if (ComputeExpectedNumberOfInstances(expectedNumberOfInstances, dicomSummary_))
3488 {
3489 SeriesStatus seriesStatus = transaction.GetSeriesStatus(status.seriesId_, expectedNumberOfInstances);
3490 if (seriesStatus == SeriesStatus_Complete)
3491 {
3492 transaction.LogChange(status.seriesId_, ChangeType_CompletedSeries, ResourceType_Series, hashSeries_);
3493 }
3494 }
3495
3496
3497 // Mark the parent resources of this instance as unstable
3498 index_.MarkAsUnstable(status.seriesId_, ResourceType_Series, hashSeries_);
3499 index_.MarkAsUnstable(status.studyId_, ResourceType_Study, hashStudy_);
3500 index_.MarkAsUnstable(status.patientId_, ResourceType_Patient, hashPatient_);
3501
3502 transaction.GetListener().SignalAttachmentsAdded(instanceSize);
3503
3504 storeStatus_ = StoreStatus_Success;
3505 }
3506 catch (OrthancException& e)
3507 {
3508 LOG(ERROR) << "EXCEPTION [" << e.What() << "]";
3509 storeStatus_ = StoreStatus_Failure;
3510 }
3511 }
3512 };
3513
3514
3515 std::unique_ptr<Operations> operations;
3516
3517 {
3518 boost::mutex::scoped_lock lock(monitoringMutex_);
3519 operations.reset(new Operations(instanceMetadata, *this, dicomSummary, attachments, metadata, origin,
3520 overwrite, hasTransferSyntax, transferSyntax, hasPixelDataOffset,
3521 pixelDataOffset, maximumStorageSize_, maximumPatients_));
3522 }
3523
3524 Apply(*operations);
3525 return operations->GetStoreStatus();
3526 }
3527
3528
3529 StoreStatus ServerIndex::AddAttachment(const FileInfo& attachment,
3530 const std::string& publicId)
3531 {
3532 class Operations : public IReadWriteOperations
3533 {
3534 private:
3535 StoreStatus status_;
3536 const FileInfo& attachment_;
3537 const std::string& publicId_;
3538 uint64_t maximumStorageSize_;
3539 unsigned int maximumPatientCount_;
3540
3541 public:
3542 Operations(const FileInfo& attachment,
3543 const std::string& publicId,
3544 uint64_t maximumStorageSize,
3545 unsigned int maximumPatientCount) :
3546 status_(StoreStatus_Failure),
3547 attachment_(attachment),
3548 publicId_(publicId),
3549 maximumStorageSize_(maximumStorageSize),
3550 maximumPatientCount_(maximumPatientCount)
3551 {
3552 }
3553
3554 StoreStatus GetStatus() const
3555 {
3556 return status_;
3557 }
3558
3559 virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
3560 {
3561 ResourceType resourceType;
3562 int64_t resourceId;
3563 if (!transaction.LookupResource(resourceId, resourceType, publicId_))
3564 {
3565 status_ = StoreStatus_Failure; // Inexistent resource
3566 }
3567 else
3568 {
3569 // Remove possible previous attachment
3570 transaction.DeleteAttachment(resourceId, attachment_.GetContentType());
3571
3572 // Locate the patient of the target resource
3573 int64_t patientId = resourceId;
3574 for (;;)
3575 {
3576 int64_t parent;
3577 if (transaction.LookupParent(parent, patientId))
3578 {
3579 // We have not reached the patient level yet
3580 patientId = parent;
3581 }
3582 else
3583 {
3584 // We have reached the patient level
3585 break;
3586 }
3587 }
3588
3589 // Possibly apply the recycling mechanism while preserving this patient
3590 assert(transaction.GetResourceType(patientId) == ResourceType_Patient);
3591 transaction.Recycle(maximumStorageSize_, maximumPatientCount_,
3592 attachment_.GetCompressedSize(), transaction.GetPublicId(patientId));
3593
3594 transaction.AddAttachment(resourceId, attachment_);
3595
3596 if (IsUserContentType(attachment_.GetContentType()))
3597 {
3598 transaction.LogChange(resourceId, ChangeType_UpdatedAttachment, resourceType, publicId_);
3599 }
3600
3601 transaction.GetListener().SignalAttachmentsAdded(attachment_.GetCompressedSize());
3602
3603 status_ = StoreStatus_Success;
3604 }
3605 }
3606 };
3607
3608
3609 std::unique_ptr<Operations> operations;
3610
3611 {
3612 boost::mutex::scoped_lock lock(monitoringMutex_);
3613 operations.reset(new Operations(attachment, publicId, maximumStorageSize_, maximumPatients_));
3614 }
3615
3616 Apply(*operations);
3617 return operations->GetStatus();
3618 }
3464 } 3619 }