diff --git a/catalogue/TapeSearchCriteria.hpp b/catalogue/TapeSearchCriteria.hpp index 1a0eaa0876deca753299bdc1f1da544cc0ee77af..05160b6b2eb1d260f9f086b7a4dbfd3d4b9ea8b3 100644 --- a/catalogue/TapeSearchCriteria.hpp +++ b/catalogue/TapeSearchCriteria.hpp @@ -95,6 +95,11 @@ struct TapeSearchCriteria { */ std::optional<std::vector<std::string>> diskFileIds; + /** + * Check tapes with missing tape file copies + */ + std::optional<bool> checkMissingFileCopies; + /** * The state of the tapes to look for */ diff --git a/catalogue/rdbms/RdbmsTapeCatalogue.cpp b/catalogue/rdbms/RdbmsTapeCatalogue.cpp index 6461932ba42caa56cdd731cb181e535cd99ea4d7..308a297ca24293eeaad5613bad8be9b248eb4f8c 100644 --- a/catalogue/rdbms/RdbmsTapeCatalogue.cpp +++ b/catalogue/rdbms/RdbmsTapeCatalogue.cpp @@ -1299,7 +1299,8 @@ std::list<common::dataStructures::Tape> RdbmsTapeCatalogue::getTapes(rdbms::Conn searchCriteria.state || searchCriteria.fromCastor || searchCriteria.purchaseOrder || - searchCriteria.physicalLibraryName) { + searchCriteria.physicalLibraryName || + searchCriteria.checkMissingFileCopies) { sql += R"SQL( WHERE )SQL"; } @@ -1425,6 +1426,30 @@ std::list<common::dataStructures::Tape> RdbmsTapeCatalogue::getTapes(rdbms::Conn sql += R"SQL( PHYSICAL_LIBRARY.PHYSICAL_LIBRARY_NAME = :PHYSICAL_LIBRARY_NAME )SQL"; + addedAWhereConstraint = true; + } + if (searchCriteria.checkMissingFileCopies.value_or(false)) { + if (addedAWhereConstraint) { + sql += R"SQL( AND )SQL"; + } + sql += R"SQL( + VID IN ( + SELECT TF.VID FROM ( + SELECT AF.ARCHIVE_FILE_ID, SC.NB_COPIES, COUNT(TF.ARCHIVE_FILE_ID) AS NB_TAPE_COPIES + FROM + ARCHIVE_FILE AF + INNER JOIN STORAGE_CLASS SC ON AF.STORAGE_CLASS_ID = SC.STORAGE_CLASS_ID + INNER JOIN TAPE_FILE TF ON AF.ARCHIVE_FILE_ID = TF.ARCHIVE_FILE_ID + WHERE + SC.NB_COPIES > 1 + GROUP BY + AF.ARCHIVE_FILE_ID, SC.NB_COPIES + HAVING + SC.NB_COPIES <> COUNT(TF.ARCHIVE_FILE_ID) + ) MISSING_COPIES + INNER JOIN TAPE_FILE TF ON MISSING_COPIES.ARCHIVE_FILE_ID = TF.ARCHIVE_FILE_ID + ) + )SQL"; } sql += R"SQL( ORDER BY TAPE.VID )SQL"; diff --git a/catalogue/tests/modules/ArchiveFileCatalogueTest.cpp b/catalogue/tests/modules/ArchiveFileCatalogueTest.cpp index 5584cfe9b6e4e7158994fed7a44b7efa5eee4919..c443f2864f85d0bbe13e8e498de4e2047e8a18f2 100644 --- a/catalogue/tests/modules/ArchiveFileCatalogueTest.cpp +++ b/catalogue/tests/modules/ArchiveFileCatalogueTest.cpp @@ -42,6 +42,7 @@ cta_catalogue_ArchiveFileTest::cta_catalogue_ArchiveFileTest() : m_dummyLog("dummy", "dummy"), m_tape1(CatalogueTestUtils::getTape1()), m_tape2(CatalogueTestUtils::getTape2()), + m_tape3(CatalogueTestUtils::getTape3()), m_mediaType(CatalogueTestUtils::getMediaType()), m_admin(CatalogueTestUtils::getAdmin()), m_diskInstance(CatalogueTestUtils::getDiskInstance()), @@ -4987,4 +4988,171 @@ TEST_P(cta_catalogue_ArchiveFileTest, getArchiveFileQueueCriteria_ignore_repack_ ASSERT_EQ(tapePoolName_default_2, copyToPoolMap_it->second); } +TEST_P(cta_catalogue_ArchiveFileTest, getTapesWithMissingTapeFileCopies) { + const bool logicalLibraryIsDisabled= false; + const uint64_t nbPartialTapes = 2; + const bool isEncrypted = true; + const std::list<std::string> supply; + const std::string diskInstance = m_diskInstance.name; + std::optional<std::string> physicalLibraryName; + + m_catalogue->MediaType()->createMediaType(m_admin, m_mediaType); + m_catalogue->LogicalLibrary()->createLogicalLibrary(m_admin, m_tape1.logicalLibraryName, logicalLibraryIsDisabled, physicalLibraryName, "Create logical library"); + m_catalogue->DiskInstance()->createDiskInstance(m_admin, m_diskInstance.name, m_diskInstance.comment); + m_catalogue->VO()->createVirtualOrganization(m_admin, m_vo); + m_catalogue->TapePool()->createTapePool(m_admin, m_tape1.tapePoolName, m_vo.name, nbPartialTapes, isEncrypted, supply, "Create tape pool"); + m_catalogue->Tape()->createTape(m_admin, m_tape1); + m_catalogue->Tape()->createTape(m_admin, m_tape2); + m_catalogue->Tape()->createTape(m_admin, m_tape3); + m_catalogue->StorageClass()->createStorageClass(m_admin, m_storageClassDualCopy); + + // Storage class 'm_storageClassDualCopy' expects two tape copies for each file + // 1.1. Write 1 single file copy of 'archiveFileId_1' on 'm_tape1' + // 1.2. Write 1 single file copy of 'archiveFileId_2' on 'm_tape2' (different file) + // 2. Check that both tapes are identified as missing a second tape file copy + + constexpr uint64_t archiveFileId_1 = 1234; + constexpr uint64_t archiveFileId_2 = 5678; + + constexpr uint64_t archiveFileSize = 1; + const std::string tapeDrive = "tape_drive"; + + { + auto tapeFileWrittenPtr = std::make_unique<cta::catalogue::TapeFileWritten>(); + auto & tapeFileWritten = *tapeFileWrittenPtr; + + tapeFileWritten.archiveFileId = archiveFileId_1; + tapeFileWritten.diskInstance = diskInstance; + tapeFileWritten.diskFileId = "12340000"; + + tapeFileWritten.diskFileOwnerUid = PUBLIC_DISK_USER; + tapeFileWritten.diskFileGid = PUBLIC_DISK_GROUP; + tapeFileWritten.size = archiveFileSize; + tapeFileWritten.checksumBlob.insert(cta::checksum::ADLER32, "1234"); + tapeFileWritten.storageClassName = m_storageClassDualCopy.name; + tapeFileWritten.vid = m_tape1.vid; + tapeFileWritten.fSeq = 1; + tapeFileWritten.blockId = 4321; + tapeFileWritten.copyNb = 1; + tapeFileWritten.tapeDrive = tapeDrive; + + std::set<cta::catalogue::TapeItemWrittenPointer> fileWrittenSet; + fileWrittenSet.insert(tapeFileWrittenPtr.release()); + m_catalogue->TapeFile()->filesWrittenToTape(fileWrittenSet); + } + + { + auto tapeFileWrittenPtr = std::make_unique<cta::catalogue::TapeFileWritten>(); + auto & tapeFileWritten = *tapeFileWrittenPtr; + + tapeFileWritten.archiveFileId = archiveFileId_2; + tapeFileWritten.diskInstance = diskInstance; + tapeFileWritten.diskFileId = "56780000"; + + tapeFileWritten.diskFileOwnerUid = PUBLIC_DISK_USER; + tapeFileWritten.diskFileGid = PUBLIC_DISK_GROUP; + tapeFileWritten.size = archiveFileSize; + tapeFileWritten.checksumBlob.insert(cta::checksum::ADLER32, "8765"); + tapeFileWritten.storageClassName = m_storageClassDualCopy.name; + tapeFileWritten.vid = m_tape2.vid; + tapeFileWritten.fSeq = 1; + tapeFileWritten.blockId = 5678; + tapeFileWritten.copyNb = 1; + tapeFileWritten.tapeDrive = tapeDrive; + + std::set<cta::catalogue::TapeItemWrittenPointer> fileWrittenSet; + fileWrittenSet.insert(tapeFileWrittenPtr.release()); + m_catalogue->TapeFile()->filesWrittenToTape(fileWrittenSet); + } + + // Tape 1 and 2 should be identified as missing a 2nd copy + { + cta::catalogue::TapeSearchCriteria searchCriteria; + searchCriteria.checkMissingFileCopies = true; + const std::list<cta::common::dataStructures::Tape> tapes = m_catalogue->Tape()->getTapes(searchCriteria); + + ASSERT_EQ(2, tapes.size()); + + const auto vidToTape = CatalogueTestUtils::tapeListToMap(tapes); + const cta::common::dataStructures::Tape & tape_1 = vidToTape.at(m_tape1.vid); + ASSERT_EQ(m_tape1.vid, tape_1.vid); + const cta::common::dataStructures::Tape & tape_2 = vidToTape.at(m_tape2.vid); + ASSERT_EQ(m_tape2.vid, tape_2.vid); + } + + // 1.1. Write 1 new file copy of 'archiveFileId_1' on 'm_tape3' + // 2. Only 'm_tape3' should now be identified as missing a second tape file copy + { + auto tapeFileWrittenPtr = std::make_unique<cta::catalogue::TapeFileWritten>(); + auto & tapeFileWritten = *tapeFileWrittenPtr; + + tapeFileWritten.archiveFileId = archiveFileId_1; + tapeFileWritten.diskInstance = diskInstance; + tapeFileWritten.diskFileId = "12340000"; + + tapeFileWritten.diskFileOwnerUid = PUBLIC_DISK_USER; + tapeFileWritten.diskFileGid = PUBLIC_DISK_GROUP; + tapeFileWritten.size = archiveFileSize; + tapeFileWritten.checksumBlob.insert(cta::checksum::ADLER32, "1234"); + tapeFileWritten.storageClassName = m_storageClassDualCopy.name; + tapeFileWritten.vid = m_tape3.vid; + tapeFileWritten.fSeq = 1; + tapeFileWritten.blockId = 4321; + tapeFileWritten.copyNb = 2; + tapeFileWritten.tapeDrive = tapeDrive; + + std::set<cta::catalogue::TapeItemWrittenPointer> fileWrittenSet; + fileWrittenSet.insert(tapeFileWrittenPtr.release()); + m_catalogue->TapeFile()->filesWrittenToTape(fileWrittenSet); + } + + // Only tape 2 should now be identified as missing a 2nd copy + { + cta::catalogue::TapeSearchCriteria searchCriteria; + searchCriteria.checkMissingFileCopies = true; + const std::list<cta::common::dataStructures::Tape> tapes = m_catalogue->Tape()->getTapes(searchCriteria); + + ASSERT_EQ(1, tapes.size()); + + const auto vidToTape = CatalogueTestUtils::tapeListToMap(tapes); + const cta::common::dataStructures::Tape & tape_2 = vidToTape.at(m_tape2.vid); + ASSERT_EQ(m_tape2.vid, tape_2.vid); + } + + // 1.1. Write 1 new file copy of 'archiveFileId_2' on 'm_tape3' + // 2. No tapes should now be identified as missing a second tape file copy + { + auto tapeFileWrittenPtr = std::make_unique<cta::catalogue::TapeFileWritten>(); + auto & tapeFileWritten = *tapeFileWrittenPtr; + + tapeFileWritten.archiveFileId = archiveFileId_2; + tapeFileWritten.diskInstance = diskInstance; + tapeFileWritten.diskFileId = "56780000"; + + tapeFileWritten.diskFileOwnerUid = PUBLIC_DISK_USER; + tapeFileWritten.diskFileGid = PUBLIC_DISK_GROUP; + tapeFileWritten.size = archiveFileSize; + tapeFileWritten.checksumBlob.insert(cta::checksum::ADLER32, "8765"); + tapeFileWritten.storageClassName = m_storageClassDualCopy.name; + tapeFileWritten.vid = m_tape3.vid; + tapeFileWritten.fSeq = 2; + tapeFileWritten.blockId = 5678; + tapeFileWritten.copyNb = 2; + tapeFileWritten.tapeDrive = tapeDrive; + + std::set<cta::catalogue::TapeItemWrittenPointer> fileWrittenSet; + fileWrittenSet.insert(tapeFileWrittenPtr.release()); + m_catalogue->TapeFile()->filesWrittenToTape(fileWrittenSet); + } + + // No tapes should now be identified as missing a 2nd copy + { + cta::catalogue::TapeSearchCriteria searchCriteria; + searchCriteria.checkMissingFileCopies = true; + const std::list<cta::common::dataStructures::Tape> tapes = m_catalogue->Tape()->getTapes(searchCriteria); + + ASSERT_TRUE(tapes.empty()); + } +} + } // namespace unitTests diff --git a/catalogue/tests/modules/ArchiveFileCatalogueTest.hpp b/catalogue/tests/modules/ArchiveFileCatalogueTest.hpp index da6516c6476ce87b9ded78b5e8694034dd9c1b17..b2e39c32650360f3b06d30234a52217b6c20c44c 100644 --- a/catalogue/tests/modules/ArchiveFileCatalogueTest.hpp +++ b/catalogue/tests/modules/ArchiveFileCatalogueTest.hpp @@ -46,6 +46,7 @@ protected: const cta::catalogue::CreateTapeAttributes m_tape1; const cta::catalogue::CreateTapeAttributes m_tape2; + const cta::catalogue::CreateTapeAttributes m_tape3; const cta::catalogue::MediaType m_mediaType; const cta::common::dataStructures::SecurityIdentity m_admin; const cta::common::dataStructures::DiskInstance m_diskInstance; diff --git a/cmdline/CtaAdminCmdParse.hpp b/cmdline/CtaAdminCmdParse.hpp index e6029f801aef5c08a3e1f56a6de61994bdda981e..c73e72adf2f0b948f400f57534b85db3dcf0e99b 100644 --- a/cmdline/CtaAdminCmdParse.hpp +++ b/cmdline/CtaAdminCmdParse.hpp @@ -259,17 +259,18 @@ const std::map<std::string, OptionBoolean::Key> boolOptions = { {"--fromcastor", OptionBoolean::FROM_CASTOR }, // hasOption options - {"--disabledtape", OptionBoolean::DISABLED }, - {"--justarchive", OptionBoolean::JUSTARCHIVE }, - {"--justmove", OptionBoolean::JUSTMOVE }, - {"--justaddcopies", OptionBoolean::JUSTADDCOPIES }, - {"--justretrieve", OptionBoolean::JUSTRETRIEVE }, - {"--log", OptionBoolean::SHOW_LOG_ENTRIES}, - {"--lookupnamespace", OptionBoolean::LOOKUP_NAMESPACE}, - {"--summary", OptionBoolean::SUMMARY }, - {"--no-recall", OptionBoolean::NO_RECALL }, - {"--dirtybit", OptionBoolean::DIRTY_BIT }, - {"--isrepackvo", OptionBoolean::IS_REPACK_VO } + {"--disabledtape", OptionBoolean::DISABLED }, + {"--justarchive", OptionBoolean::JUSTARCHIVE }, + {"--justmove", OptionBoolean::JUSTMOVE }, + {"--justaddcopies", OptionBoolean::JUSTADDCOPIES }, + {"--justretrieve", OptionBoolean::JUSTRETRIEVE }, + {"--log", OptionBoolean::SHOW_LOG_ENTRIES }, + {"--lookupnamespace", OptionBoolean::LOOKUP_NAMESPACE }, + {"--summary", OptionBoolean::SUMMARY }, + {"--no-recall", OptionBoolean::NO_RECALL }, + {"--dirtybit", OptionBoolean::DIRTY_BIT }, + {"--isrepackvo", OptionBoolean::IS_REPACK_VO }, + {"--missingfilecopies", OptionBoolean::MISSING_FILE_COPIES }, }; /*! @@ -516,6 +517,7 @@ const Option opt_archive_route_type {Option::OPT_STR, std::string(R"( <")") + cta::common::dataStructures::toString(ArchiveRouteType::DEFAULT) + R"(" or ")" + cta::common::dataStructures::toString(ArchiveRouteType::REPACK) + R"(">)"}; +const Option opt_missingfilecopes {Option::OPT_FLAG, "--missingfilecopies", "--ifc", ""}; /*! * Subset of commands that return streaming output @@ -1034,7 +1036,8 @@ tape (ta) opt_state.optional(), opt_fromcastor.optional(), opt_purchase_order.optional(), - opt_physical_library.optional()} }, + opt_physical_library.optional(), + opt_missingfilecopes.optional()} }, /**md tapefile (tf) diff --git a/frontend/grpc/ServerTapeLsRequestHandler.cpp b/frontend/grpc/ServerTapeLsRequestHandler.cpp index a2739d7b88700ae7772ac1fe145f4cd1c25aa2ad..04e069a1c37fd97684a66921d6c3b0df403fd150 100644 --- a/frontend/grpc/ServerTapeLsRequestHandler.cpp +++ b/frontend/grpc/ServerTapeLsRequestHandler.cpp @@ -83,17 +83,18 @@ bool cta::frontend::grpc::server::TapeLsRequestHandler::next(const bool bOk) { // Get the search criteria from the optional options - m_searchCriteria.full = requestMsg.getOptional(cta::admin::OptionBoolean::FULL, &bHasAny); - m_searchCriteria.fromCastor = requestMsg.getOptional(cta::admin::OptionBoolean::FROM_CASTOR, &bHasAny); - m_searchCriteria.capacityInBytes = requestMsg.getOptional(cta::admin::OptionUInt64::CAPACITY, &bHasAny); - m_searchCriteria.logicalLibrary = requestMsg.getOptional(cta::admin::OptionString::LOGICAL_LIBRARY, &bHasAny); - m_searchCriteria.tapePool = requestMsg.getOptional(cta::admin::OptionString::TAPE_POOL, &bHasAny); - m_searchCriteria.vo = requestMsg.getOptional(cta::admin::OptionString::VO, &bHasAny); - m_searchCriteria.vid = requestMsg.getOptional(cta::admin::OptionString::VID, &bHasAny); - m_searchCriteria.mediaType = requestMsg.getOptional(cta::admin::OptionString::MEDIA_TYPE, &bHasAny); - m_searchCriteria.vendor = requestMsg.getOptional(cta::admin::OptionString::VENDOR, &bHasAny); - m_searchCriteria.purchaseOrder = requestMsg.getOptional(cta::admin::OptionString::MEDIA_PURCHASE_ORDER_NUMBER, &bHasAny); - m_searchCriteria.diskFileIds = requestMsg.getOptional(cta::admin::OptionStrList::FILE_ID, &bHasAny); + m_searchCriteria.full = requestMsg.getOptional(cta::admin::OptionBoolean::FULL, &bHasAny); + m_searchCriteria.fromCastor = requestMsg.getOptional(cta::admin::OptionBoolean::FROM_CASTOR, &bHasAny); + m_searchCriteria.capacityInBytes = requestMsg.getOptional(cta::admin::OptionUInt64::CAPACITY, &bHasAny); + m_searchCriteria.logicalLibrary = requestMsg.getOptional(cta::admin::OptionString::LOGICAL_LIBRARY, &bHasAny); + m_searchCriteria.tapePool = requestMsg.getOptional(cta::admin::OptionString::TAPE_POOL, &bHasAny); + m_searchCriteria.vo = requestMsg.getOptional(cta::admin::OptionString::VO, &bHasAny); + m_searchCriteria.vid = requestMsg.getOptional(cta::admin::OptionString::VID, &bHasAny); + m_searchCriteria.mediaType = requestMsg.getOptional(cta::admin::OptionString::MEDIA_TYPE, &bHasAny); + m_searchCriteria.vendor = requestMsg.getOptional(cta::admin::OptionString::VENDOR, &bHasAny); + m_searchCriteria.purchaseOrder = requestMsg.getOptional(cta::admin::OptionString::MEDIA_PURCHASE_ORDER_NUMBER, &bHasAny); + m_searchCriteria.diskFileIds = requestMsg.getOptional(cta::admin::OptionStrList::FILE_ID, &bHasAny); + m_searchCriteria.checkMissingFileCopies = requestMsg.getOptional(cta::admin::OptionBoolean::MISSING_FILE_COPIES, &bHasAny); auto stateOpt = requestMsg.getOptional(cta::admin::OptionString::STATE, &bHasAny); if(stateOpt){ m_searchCriteria.state = common::dataStructures::Tape::stringToState(stateOpt.value()); diff --git a/xroot_plugins/XrdCtaTapeLs.hpp b/xroot_plugins/XrdCtaTapeLs.hpp index 8210729141c6bc400631f51d07ac9060c5e39289..07edc5b2d0fdce8d2ce7042e5e5ab04d429a4aec 100644 --- a/xroot_plugins/XrdCtaTapeLs.hpp +++ b/xroot_plugins/XrdCtaTapeLs.hpp @@ -68,19 +68,21 @@ TapeLsStream::TapeLsStream(const frontend::AdminCmdStream& requestMsg, cta::cata // Get the search criteria from the optional options - searchCriteria.full = requestMsg.getOptional(OptionBoolean::FULL, &has_any); - searchCriteria.fromCastor = requestMsg.getOptional(OptionBoolean::FROM_CASTOR, &has_any); - searchCriteria.capacityInBytes = requestMsg.getOptional(OptionUInt64::CAPACITY, &has_any); - searchCriteria.logicalLibrary = requestMsg.getOptional(OptionString::LOGICAL_LIBRARY, &has_any); - searchCriteria.tapePool = requestMsg.getOptional(OptionString::TAPE_POOL, &has_any); - searchCriteria.vo = requestMsg.getOptional(OptionString::VO, &has_any); - searchCriteria.vid = requestMsg.getOptional(OptionString::VID, &has_any); - searchCriteria.mediaType = requestMsg.getOptional(OptionString::MEDIA_TYPE, &has_any); - searchCriteria.vendor = requestMsg.getOptional(OptionString::VENDOR, &has_any); - searchCriteria.purchaseOrder = requestMsg.getOptional(OptionString::MEDIA_PURCHASE_ORDER_NUMBER, &has_any); - searchCriteria.physicalLibraryName = requestMsg.getOptional(OptionString::PHYSICAL_LIBRARY, &has_any); - searchCriteria.diskFileIds = requestMsg.getOptional(OptionStrList::FILE_ID, &has_any); + searchCriteria.full = requestMsg.getOptional(OptionBoolean::FULL, &has_any); + searchCriteria.fromCastor = requestMsg.getOptional(OptionBoolean::FROM_CASTOR, &has_any); + searchCriteria.capacityInBytes = requestMsg.getOptional(OptionUInt64::CAPACITY, &has_any); + searchCriteria.logicalLibrary = requestMsg.getOptional(OptionString::LOGICAL_LIBRARY, &has_any); + searchCriteria.tapePool = requestMsg.getOptional(OptionString::TAPE_POOL, &has_any); + searchCriteria.vo = requestMsg.getOptional(OptionString::VO, &has_any); + searchCriteria.vid = requestMsg.getOptional(OptionString::VID, &has_any); + searchCriteria.mediaType = requestMsg.getOptional(OptionString::MEDIA_TYPE, &has_any); + searchCriteria.vendor = requestMsg.getOptional(OptionString::VENDOR, &has_any); + searchCriteria.purchaseOrder = requestMsg.getOptional(OptionString::MEDIA_PURCHASE_ORDER_NUMBER, &has_any); + searchCriteria.physicalLibraryName = requestMsg.getOptional(OptionString::PHYSICAL_LIBRARY, &has_any); + searchCriteria.diskFileIds = requestMsg.getOptional(OptionStrList::FILE_ID, &has_any); + searchCriteria.checkMissingFileCopies = requestMsg.getOptional(OptionBoolean::MISSING_FILE_COPIES, &has_any); auto stateOpt = requestMsg.getOptional(OptionString::STATE, &has_any); + if(stateOpt){ searchCriteria.state = common::dataStructures::Tape::stringToState(stateOpt.value(), true); } diff --git a/xrootd-ssi-protobuf-interface b/xrootd-ssi-protobuf-interface index 95ca6aca153a6e72a33a281258e3372d54fe6a74..f59e44742c6436a421157e76e5ee062c88d813bf 160000 --- a/xrootd-ssi-protobuf-interface +++ b/xrootd-ssi-protobuf-interface @@ -1 +1 @@ -Subproject commit 95ca6aca153a6e72a33a281258e3372d54fe6a74 +Subproject commit f59e44742c6436a421157e76e5ee062c88d813bf