diff --git a/libs/core/src/ecflow/core/Environment.hpp b/libs/core/src/ecflow/core/Environment.hpp index 63e982dca..77ba9bda0 100644 --- a/libs/core/src/ecflow/core/Environment.hpp +++ b/libs/core/src/ecflow/core/Environment.hpp @@ -60,6 +60,10 @@ constexpr const char* ECF_USER = "ECF_USER"; constexpr const char* ECF_DEBUG_CLIENT = "ECF_DEBUG_CLIENT"; constexpr const char* ECF_DEBUG_LEVEL = "ECF_DEBUG_LEVEL"; +// Mirror kill-switches +constexpr const char* ECF_MIRROR_DISABLE = "ECF_MIRROR_DISABLE"; +constexpr const char* ECF_MIRROR_DISABLE_QUERIES = "ECF_MIRROR_DISABLE_QUERIES"; + namespace /* anonymous */ { template diff --git a/libs/node/src/ecflow/node/MirrorAttr.cpp b/libs/node/src/ecflow/node/MirrorAttr.cpp index c0eff298a..4ad6a6ac9 100644 --- a/libs/node/src/ecflow/node/MirrorAttr.cpp +++ b/libs/node/src/ecflow/node/MirrorAttr.cpp @@ -13,6 +13,7 @@ #include #include "ecflow/core/Ecf.hpp" +#include "ecflow/core/Environment.hpp" #include "ecflow/core/Message.hpp" #include "ecflow/core/Overload.hpp" #include "ecflow/core/exceptions/Exceptions.hpp" @@ -90,6 +91,16 @@ void MirrorAttr::finish() { void MirrorAttr::mirror() { SLOG(D, "MirrorAttr: poll Mirror attribute '" << absolute_name() << "'"); + // Check if local mirrors are disabled via ECF_MIRROR_DISABLE variable + if (parent_) { + std::string disable_mirrors; + if (parent_->findParentVariableValue(ecf::environment::ECF_MIRROR_DISABLE, disable_mirrors)) { + // ECF_MIRROR_DISABLE is defined and non-empty; skip mirror polling + SLOG(D, "MirrorAttr: Mirror attribute '" << absolute_name() << "' disabled via ECF_MIRROR_DISABLE"); + return; + } + } + start_controller(); if (!controller_) { return; // Can't continue without a running controller... diff --git a/libs/node/test/parser/TestMirrorAttr.cpp b/libs/node/test/parser/TestMirrorAttr.cpp index 02802640d..c4d487a36 100644 --- a/libs/node/test/parser/TestMirrorAttr.cpp +++ b/libs/node/test/parser/TestMirrorAttr.cpp @@ -106,6 +106,51 @@ BOOST_AUTO_TEST_CASE(can_parse_mirror_attribute_on_task_with_all_attributes) { BOOST_CHECK_EQUAL(mirror.ssl(), true); } +BOOST_AUTO_TEST_CASE(mirror_disabled_when_ECF_MIRROR_DISABLE_is_set) { + ECF_NAME_THIS_TEST(); + + using namespace ecf; + + std::string definition = R"( + suite s1 + edit ECF_MIRROR_DISABLE "yes" + family f1 + task t1 + mirror --name A --remote_path /s1/f1/t2 --remote_host invalid-host --remote_port 9999 --polling 1 + endfamily + )"; + + Defs defs; + DefsStructureParser parser(&defs, definition, true); + + std::string errorMsg, warningMsg; + bool parsedOK = parser.doParse(errorMsg, warningMsg); + BOOST_CHECK_MESSAGE(parsedOK, "Failed to parse definition: " << errorMsg); + + const auto& suites = defs.suiteVec(); + BOOST_CHECK_EQUAL(suites.size(), static_cast(1)); + + const auto& families = suites[0]->familyVec(); + BOOST_CHECK_EQUAL(families.size(), static_cast(1)); + + const auto& tasks = families[0]->taskVec(); + BOOST_CHECK_EQUAL(tasks.size(), static_cast(1)); + + Task* task = tasks[0].get(); + auto& mirrors = task->mirrors(); + BOOST_CHECK_EQUAL(mirrors.size(), static_cast(1)); + + // Verify the flag is initially clear + BOOST_CHECK(!task->get_flag().is_set(Flag::REMOTE_ERROR)); + + // Call mirror() which would normally set REMOTE_ERROR if the controller fails to start + // But with ECF_MIRROR_DISABLE set, it should return early and NOT set REMOTE_ERROR + mirrors[0].mirror(); + + // Verify the flag remains clear (proving mirror() did not attempt to start controller) + BOOST_CHECK(!task->get_flag().is_set(Flag::REMOTE_ERROR)); +} + BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE_END()