From 4c416c9c95b47804d36015ec7bae61d6cc84d65d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Apr 2026 06:38:29 +0000 Subject: [PATCH 1/7] fix: make PipelineConfig::sCurrentTime public to fix Windows MSVC access error Agent-Logs-Url: https://github.com/alibaba/loongcollector/sessions/75a7db26-9448-439d-a9ea-33e319a7e2ce Co-authored-by: yyuuttaaoo <1827594+yyuuttaaoo@users.noreply.github.com> --- core/config/PipelineConfig.cpp | 16 ++++++++++-- core/config/PipelineConfig.h | 25 +++++++++++++++++++ .../config/OnetimeConfigUpdateUnittest.cpp | 21 +++++++++++----- .../config/PipelineConfigUnittest.cpp | 12 ++++++--- 4 files changed, 63 insertions(+), 11 deletions(-) diff --git a/core/config/PipelineConfig.cpp b/core/config/PipelineConfig.cpp index 5988e83dac..866f346153 100644 --- a/core/config/PipelineConfig.cpp +++ b/core/config/PipelineConfig.cpp @@ -22,6 +22,18 @@ using namespace std; namespace logtail { +#ifdef APSARA_UNIT_TEST_MAIN +std::function PipelineConfig::sCurrentTime = []() -> time_t { return time(nullptr); }; + +static time_t GetCurrentTime() { + return PipelineConfig::sCurrentTime(); +} +#else +static time_t GetCurrentTime() { + return time(nullptr); +} +#endif + static constexpr uint32_t minExpireTime = 600; // 10 minutes static constexpr uint32_t maxExpireTime = 604800; // 1 week @@ -96,12 +108,12 @@ bool PipelineConfig::GetExpireTimeIfOneTime(const Json::Value& global) { return true; case OnetimeConfigStatus::NEW: // NEW状态表示是新配置,或已有配置Rerun了 - mOnetimeStartTime = time(nullptr); + mOnetimeStartTime = static_cast(GetCurrentTime()); mOnetimeExpireTime = mOnetimeStartTime.value() + mExcutionTimeout; return true; case OnetimeConfigStatus::UPDATED: // UPDATED状态表示配置hash改变但input hash未变,保持原有checkpoint,但是更新过期时间 - mOnetimeStartTime = time(nullptr); + mOnetimeStartTime = static_cast(GetCurrentTime()); mOnetimeExpireTime = mOnetimeStartTime.value() + mExcutionTimeout; mIsRunningBeforeStart = true; LOG_INFO(sLogger, diff --git a/core/config/PipelineConfig.h b/core/config/PipelineConfig.h index 80cba4bced..fe30ac7b6a 100644 --- a/core/config/PipelineConfig.h +++ b/core/config/PipelineConfig.h @@ -17,8 +17,10 @@ #pragma once #include +#include #include +#include #include #include #include @@ -50,11 +52,34 @@ struct PipelineConfig { virtual bool Parse() = 0; +#ifdef APSARA_UNIT_TEST_MAIN + // Overridable clock for deterministic unit tests. + // Placed in the public section so that the free helper GetCurrentTime() in + // PipelineConfig.cpp can access it even under MSVC, which (unlike GCC/Clang + // with -fno-access-control) enforces protected-member access strictly. + static std::function sCurrentTime; + + // RAII guard that temporarily replaces the clock with a fixed value. + struct ScopedClockOverride { + explicit ScopedClockOverride(time_t fixedNow) : mPrev(sCurrentTime) { + sCurrentTime = [fixedNow]() -> time_t { return fixedNow; }; + } + ~ScopedClockOverride() { sCurrentTime = mPrev; } + + ScopedClockOverride(const ScopedClockOverride&) = delete; + ScopedClockOverride& operator=(const ScopedClockOverride&) = delete; + + private: + std::function mPrev; + }; +#endif + protected: bool GetExpireTimeIfOneTime(const Json::Value& global); #ifdef APSARA_UNIT_TEST_MAIN friend class PipelineConfigUnittest; + friend class OnetimeConfigUpdateUnittest; #endif }; diff --git a/core/unittest/config/OnetimeConfigUpdateUnittest.cpp b/core/unittest/config/OnetimeConfigUpdateUnittest.cpp index 6b5eddb684..77a3b901dc 100644 --- a/core/unittest/config/OnetimeConfigUpdateUnittest.cpp +++ b/core/unittest/config/OnetimeConfigUpdateUnittest.cpp @@ -15,6 +15,7 @@ #include "collection_pipeline/CollectionPipelineManager.h" #include "common/JsonUtil.h" #include "config/OnetimeConfigInfoManager.h" +#include "config/PipelineConfig.h" #include "config/watcher/PipelineConfigWatcher.h" #include "unittest/Unittest.h" #include "unittest/plugin/PluginMock.h" @@ -238,20 +239,24 @@ void OnetimeConfigUpdateUnittest::OnCollectionConfigUpdate() const { auto diff = PipelineConfigWatcher::GetInstance()->CheckConfigDiff(); APSARA_TEST_TRUE(diff.first.HasDiff()); - CollectionPipelineManager::GetInstance()->UpdatePipelines(diff.first); + auto restartNow = time(nullptr); + { + PipelineConfig::ScopedClockOverride clockGuard(restartNow); + CollectionPipelineManager::GetInstance()->UpdatePipelines(diff.first); + } sConfigManager->DumpCheckpointFile(); APSARA_TEST_EQUAL(3U, sConfigManager->mConfigInfoMap.size()); { const auto& item = sConfigManager->mConfigInfoMap.at("new_config"); - APSARA_TEST_EQUAL(time(nullptr) + 3600U, item.mExpireTime); + APSARA_TEST_EQUAL(static_cast(restartNow) + 3600U, item.mExpireTime); APSARA_TEST_EQUAL(configHash["new_config.json"], item.mConfigHash); APSARA_TEST_EQUAL(ConfigType::Collection, item.mType); APSARA_TEST_EQUAL(mConfigDir / filenames[0], item.mFilepath); } { const auto& item = sConfigManager->mConfigInfoMap.at("changed_config"); - APSARA_TEST_EQUAL(time(nullptr) + 7200U, item.mExpireTime); + APSARA_TEST_EQUAL(static_cast(restartNow) + 7200U, item.mExpireTime); APSARA_TEST_EQUAL(configHash["changed_config.json"], item.mConfigHash); APSARA_TEST_EQUAL(ConfigType::Collection, item.mType); APSARA_TEST_EQUAL(mConfigDir / filenames[1], item.mFilepath); @@ -333,20 +338,24 @@ void OnetimeConfigUpdateUnittest::OnCollectionConfigUpdate() const { auto diff = PipelineConfigWatcher::GetInstance()->CheckConfigDiff(); APSARA_TEST_TRUE(diff.first.HasDiff()); - CollectionPipelineManager::GetInstance()->UpdatePipelines(diff.first); + auto updateNow = time(nullptr); + { + PipelineConfig::ScopedClockOverride clockGuard(updateNow); + CollectionPipelineManager::GetInstance()->UpdatePipelines(diff.first); + } sConfigManager->DumpCheckpointFile(); APSARA_TEST_EQUAL(3U, sConfigManager->mConfigInfoMap.size()); { const auto& item = sConfigManager->mConfigInfoMap.at("new_config"); - APSARA_TEST_EQUAL(time(nullptr) + 1000U, item.mExpireTime); + APSARA_TEST_EQUAL(static_cast(updateNow) + 1000U, item.mExpireTime); APSARA_TEST_EQUAL(configHash["new_config.json"], item.mConfigHash); APSARA_TEST_EQUAL(ConfigType::Collection, item.mType); APSARA_TEST_EQUAL(mConfigDir / filenames[0], item.mFilepath); } { const auto& item = sConfigManager->mConfigInfoMap.at("old_config"); - APSARA_TEST_EQUAL(time(nullptr) + 1200U, item.mExpireTime); + APSARA_TEST_EQUAL(static_cast(updateNow) + 1200U, item.mExpireTime); APSARA_TEST_EQUAL(configHash["old_config.json"], item.mConfigHash); APSARA_TEST_EQUAL(ConfigType::Collection, item.mType); APSARA_TEST_EQUAL(mConfigDir / filenames[1], item.mFilepath); diff --git a/core/unittest/config/PipelineConfigUnittest.cpp b/core/unittest/config/PipelineConfigUnittest.cpp index cc8320e68d..543edb40c8 100644 --- a/core/unittest/config/PipelineConfigUnittest.cpp +++ b/core/unittest/config/PipelineConfigUnittest.cpp @@ -71,9 +71,11 @@ void PipelineConfigUnittest::TestOnetimeConfig() const { auto configJson = make_unique(); (*configJson)["global"]["ExcutionTimeout"] = true; + auto now = time(nullptr); + PipelineConfig::ScopedClockOverride clockGuard(now); ConfigMock config("test", std::move(configJson), filepath); APSARA_TEST_TRUE(config.GetExpireTimeIfOneTime((*config.mDetail)["global"])); - APSARA_TEST_EQUAL(time(nullptr) + 604800U, config.mOnetimeExpireTime); + APSARA_TEST_EQUAL(static_cast(now) + 604800U, config.mOnetimeExpireTime); APSARA_TEST_FALSE(config.mIsRunningBeforeStart); } { @@ -81,9 +83,11 @@ void PipelineConfigUnittest::TestOnetimeConfig() const { auto configJson = make_unique(); (*configJson)["global"]["ExcutionTimeout"] = 1U; + auto now = time(nullptr); + PipelineConfig::ScopedClockOverride clockGuard(now); ConfigMock config("test", std::move(configJson), filepath); APSARA_TEST_TRUE(config.GetExpireTimeIfOneTime((*config.mDetail)["global"])); - APSARA_TEST_EQUAL(time(nullptr) + 600U, config.mOnetimeExpireTime); + APSARA_TEST_EQUAL(static_cast(now) + 600U, config.mOnetimeExpireTime); APSARA_TEST_FALSE(config.mIsRunningBeforeStart); } { @@ -91,9 +95,11 @@ void PipelineConfigUnittest::TestOnetimeConfig() const { auto configJson = make_unique(); (*configJson)["global"]["ExcutionTimeout"] = 1000000U; + auto now = time(nullptr); + PipelineConfig::ScopedClockOverride clockGuard(now); ConfigMock config("test", std::move(configJson), filepath); APSARA_TEST_TRUE(config.GetExpireTimeIfOneTime((*config.mDetail)["global"])); - APSARA_TEST_EQUAL(time(nullptr) + 604800U, config.mOnetimeExpireTime); + APSARA_TEST_EQUAL(static_cast(now) + 604800U, config.mOnetimeExpireTime); APSARA_TEST_FALSE(config.mIsRunningBeforeStart); } From 3a708e88ea1c877ca97c468d4ff8d5faefc2e828 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Apr 2026 06:42:08 +0000 Subject: [PATCH 2/7] fix: add thread-safety note to ScopedClockOverride comment Agent-Logs-Url: https://github.com/alibaba/loongcollector/sessions/75a7db26-9448-439d-a9ea-33e319a7e2ce Co-authored-by: yyuuttaaoo <1827594+yyuuttaaoo@users.noreply.github.com> --- core/config/PipelineConfig.h | 1 + 1 file changed, 1 insertion(+) diff --git a/core/config/PipelineConfig.h b/core/config/PipelineConfig.h index fe30ac7b6a..23520275ac 100644 --- a/core/config/PipelineConfig.h +++ b/core/config/PipelineConfig.h @@ -60,6 +60,7 @@ struct PipelineConfig { static std::function sCurrentTime; // RAII guard that temporarily replaces the clock with a fixed value. + // Not thread-safe — intended for single-threaded unit tests only. struct ScopedClockOverride { explicit ScopedClockOverride(time_t fixedNow) : mPrev(sCurrentTime) { sCurrentTime = [fixedNow]() -> time_t { return fixedNow; }; From 59fc980dce7a99111f188a9832d7bdf5d0403a13 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Apr 2026 06:55:32 +0000 Subject: [PATCH 3/7] refactor: move injectable clock to TimeUtil as global gCurrentTime Agent-Logs-Url: https://github.com/alibaba/loongcollector/sessions/dbe89a33-7fcb-4c52-a407-7db4dd54b7db Co-authored-by: yyuuttaaoo <1827594+yyuuttaaoo@users.noreply.github.com> --- core/common/TimeUtil.cpp | 27 ++++++++++++++++++ core/common/TimeUtil.h | 28 ++++++++++++++++++- core/config/PipelineConfig.cpp | 17 ++--------- core/config/PipelineConfig.h | 25 ----------------- .../config/OnetimeConfigUpdateUnittest.cpp | 6 ++-- .../config/PipelineConfigUnittest.cpp | 7 +++-- 6 files changed, 64 insertions(+), 46 deletions(-) diff --git a/core/common/TimeUtil.cpp b/core/common/TimeUtil.cpp index 03ba5d56f6..7f341f7e02 100644 --- a/core/common/TimeUtil.cpp +++ b/core/common/TimeUtil.cpp @@ -19,6 +19,9 @@ #include #include +#ifdef APSARA_UNIT_TEST_MAIN +#include +#endif #include #if defined(__linux__) #include @@ -35,6 +38,10 @@ namespace logtail { const std::string PRECISE_TIMESTAMP_DEFAULT_KEY = "precise_timestamp"; +#ifdef APSARA_UNIT_TEST_MAIN +std::function gCurrentTime = []() -> time_t { return time(nullptr); }; +#endif + std::string ConvertToTimeStamp(const time_t& t, const std::string& format) { return GetTimeStamp(t, format); } @@ -59,14 +66,30 @@ std::string GetTimeStamp(time_t t, const std::string& format, bool isLocal) { return (0 == ret) ? "" : std::string(buf, ret); } +uint64_t GetCurrentTimeInSeconds() { +#ifdef APSARA_UNIT_TEST_MAIN + return static_cast(gCurrentTime()); +#else + return static_cast(time(nullptr)); +#endif +} + uint64_t GetCurrentTimeInMicroSeconds() { +#ifdef APSARA_UNIT_TEST_MAIN + return static_cast(gCurrentTime()) * 1000000ULL; +#else return std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()) .count(); +#endif } uint64_t GetCurrentTimeInMilliSeconds() { +#ifdef APSARA_UNIT_TEST_MAIN + return static_cast(gCurrentTime()) * 1000ULL; +#else return std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()) .count(); +#endif } int GetLocalTimeZoneOffsetSecond() { @@ -365,8 +388,12 @@ uint64_t GetPreciseTimestamp(uint64_t secondTimestamp, } uint64_t GetCurrentTimeInNanoSeconds() { +#ifdef APSARA_UNIT_TEST_MAIN + return static_cast(gCurrentTime()) * 1000000000ULL; +#else return std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()) .count(); +#endif } bool ParseTimeZoneOffsetSecond(const std::string& logTZ, int& logTZSecond) { diff --git a/core/common/TimeUtil.h b/core/common/TimeUtil.h index 31dbbcb0ae..0c303a93e7 100644 --- a/core/common/TimeUtil.h +++ b/core/common/TimeUtil.h @@ -21,6 +21,10 @@ #include #include +#ifdef APSARA_UNIT_TEST_MAIN +#include +#endif + #include "common/Strptime.h" #include "protobuf/sls/sls_logs.pb.h" @@ -41,11 +45,33 @@ struct PreciseTimestampConfig { typedef timespec LogtailTime; +#ifdef APSARA_UNIT_TEST_MAIN +// Injectable clock for deterministic unit tests. Defaults to time(nullptr). +extern std::function gCurrentTime; + +// RAII guard that temporarily pins the clock to a fixed value for the duration +// of a test and restores the previous value on destruction. +// Not thread-safe — intended for single-threaded unit tests only. +struct ScopedClockOverride { + explicit ScopedClockOverride(time_t fixedNow) : mPrev(gCurrentTime) { + gCurrentTime = [fixedNow]() -> time_t { return fixedNow; }; + } + ~ScopedClockOverride() { gCurrentTime = mPrev; } + + ScopedClockOverride(const ScopedClockOverride&) = delete; + ScopedClockOverride& operator=(const ScopedClockOverride&) = delete; + +private: + std::function mPrev; +}; +#endif + // Convert @tm to string according to @format. TODO: Merge ConvertToTimeStamp and GetTimeStamp. std::string ConvertToTimeStamp(const time_t& tm, const std::string& format = "%Y%m%d%H%M%S"); std::string GetTimeStamp(time_t tm, const std::string& format = "%Y%m%d%H%M%S", bool isLocal = true); -// Get current time in us or ms. +// Get current time in s, us, ms, or ns. +uint64_t GetCurrentTimeInSeconds(); uint64_t GetCurrentTimeInMicroSeconds(); uint64_t GetCurrentTimeInMilliSeconds(); uint64_t GetCurrentTimeInNanoSeconds(); diff --git a/core/config/PipelineConfig.cpp b/core/config/PipelineConfig.cpp index 866f346153..caccbf4c1e 100644 --- a/core/config/PipelineConfig.cpp +++ b/core/config/PipelineConfig.cpp @@ -15,6 +15,7 @@ #include "config/PipelineConfig.h" #include "common/JsonUtil.h" +#include "common/TimeUtil.h" #include "config/OnetimeConfigInfoManager.h" #include "logger/Logger.h" @@ -22,18 +23,6 @@ using namespace std; namespace logtail { -#ifdef APSARA_UNIT_TEST_MAIN -std::function PipelineConfig::sCurrentTime = []() -> time_t { return time(nullptr); }; - -static time_t GetCurrentTime() { - return PipelineConfig::sCurrentTime(); -} -#else -static time_t GetCurrentTime() { - return time(nullptr); -} -#endif - static constexpr uint32_t minExpireTime = 600; // 10 minutes static constexpr uint32_t maxExpireTime = 604800; // 1 week @@ -108,12 +97,12 @@ bool PipelineConfig::GetExpireTimeIfOneTime(const Json::Value& global) { return true; case OnetimeConfigStatus::NEW: // NEW状态表示是新配置,或已有配置Rerun了 - mOnetimeStartTime = static_cast(GetCurrentTime()); + mOnetimeStartTime = static_cast(GetCurrentTimeInSeconds()); mOnetimeExpireTime = mOnetimeStartTime.value() + mExcutionTimeout; return true; case OnetimeConfigStatus::UPDATED: // UPDATED状态表示配置hash改变但input hash未变,保持原有checkpoint,但是更新过期时间 - mOnetimeStartTime = static_cast(GetCurrentTime()); + mOnetimeStartTime = static_cast(GetCurrentTimeInSeconds()); mOnetimeExpireTime = mOnetimeStartTime.value() + mExcutionTimeout; mIsRunningBeforeStart = true; LOG_INFO(sLogger, diff --git a/core/config/PipelineConfig.h b/core/config/PipelineConfig.h index 23520275ac..881e0f3fbe 100644 --- a/core/config/PipelineConfig.h +++ b/core/config/PipelineConfig.h @@ -17,10 +17,8 @@ #pragma once #include -#include #include -#include #include #include #include @@ -52,29 +50,6 @@ struct PipelineConfig { virtual bool Parse() = 0; -#ifdef APSARA_UNIT_TEST_MAIN - // Overridable clock for deterministic unit tests. - // Placed in the public section so that the free helper GetCurrentTime() in - // PipelineConfig.cpp can access it even under MSVC, which (unlike GCC/Clang - // with -fno-access-control) enforces protected-member access strictly. - static std::function sCurrentTime; - - // RAII guard that temporarily replaces the clock with a fixed value. - // Not thread-safe — intended for single-threaded unit tests only. - struct ScopedClockOverride { - explicit ScopedClockOverride(time_t fixedNow) : mPrev(sCurrentTime) { - sCurrentTime = [fixedNow]() -> time_t { return fixedNow; }; - } - ~ScopedClockOverride() { sCurrentTime = mPrev; } - - ScopedClockOverride(const ScopedClockOverride&) = delete; - ScopedClockOverride& operator=(const ScopedClockOverride&) = delete; - - private: - std::function mPrev; - }; -#endif - protected: bool GetExpireTimeIfOneTime(const Json::Value& global); diff --git a/core/unittest/config/OnetimeConfigUpdateUnittest.cpp b/core/unittest/config/OnetimeConfigUpdateUnittest.cpp index 77a3b901dc..78532a50e3 100644 --- a/core/unittest/config/OnetimeConfigUpdateUnittest.cpp +++ b/core/unittest/config/OnetimeConfigUpdateUnittest.cpp @@ -14,8 +14,8 @@ #include "collection_pipeline/CollectionPipelineManager.h" #include "common/JsonUtil.h" +#include "common/TimeUtil.h" #include "config/OnetimeConfigInfoManager.h" -#include "config/PipelineConfig.h" #include "config/watcher/PipelineConfigWatcher.h" #include "unittest/Unittest.h" #include "unittest/plugin/PluginMock.h" @@ -241,7 +241,7 @@ void OnetimeConfigUpdateUnittest::OnCollectionConfigUpdate() const { APSARA_TEST_TRUE(diff.first.HasDiff()); auto restartNow = time(nullptr); { - PipelineConfig::ScopedClockOverride clockGuard(restartNow); + ScopedClockOverride clockGuard(restartNow); CollectionPipelineManager::GetInstance()->UpdatePipelines(diff.first); } sConfigManager->DumpCheckpointFile(); @@ -340,7 +340,7 @@ void OnetimeConfigUpdateUnittest::OnCollectionConfigUpdate() const { APSARA_TEST_TRUE(diff.first.HasDiff()); auto updateNow = time(nullptr); { - PipelineConfig::ScopedClockOverride clockGuard(updateNow); + ScopedClockOverride clockGuard(updateNow); CollectionPipelineManager::GetInstance()->UpdatePipelines(diff.first); } sConfigManager->DumpCheckpointFile(); diff --git a/core/unittest/config/PipelineConfigUnittest.cpp b/core/unittest/config/PipelineConfigUnittest.cpp index 543edb40c8..b6253ee1fb 100644 --- a/core/unittest/config/PipelineConfigUnittest.cpp +++ b/core/unittest/config/PipelineConfigUnittest.cpp @@ -14,6 +14,7 @@ #include "config/OnetimeConfigInfoManager.h" #include "config/PipelineConfig.h" +#include "common/TimeUtil.h" #include "logger/Logger.h" #include "unittest/Unittest.h" @@ -72,7 +73,7 @@ void PipelineConfigUnittest::TestOnetimeConfig() const { (*configJson)["global"]["ExcutionTimeout"] = true; auto now = time(nullptr); - PipelineConfig::ScopedClockOverride clockGuard(now); + ScopedClockOverride clockGuard(now); ConfigMock config("test", std::move(configJson), filepath); APSARA_TEST_TRUE(config.GetExpireTimeIfOneTime((*config.mDetail)["global"])); APSARA_TEST_EQUAL(static_cast(now) + 604800U, config.mOnetimeExpireTime); @@ -84,7 +85,7 @@ void PipelineConfigUnittest::TestOnetimeConfig() const { (*configJson)["global"]["ExcutionTimeout"] = 1U; auto now = time(nullptr); - PipelineConfig::ScopedClockOverride clockGuard(now); + ScopedClockOverride clockGuard(now); ConfigMock config("test", std::move(configJson), filepath); APSARA_TEST_TRUE(config.GetExpireTimeIfOneTime((*config.mDetail)["global"])); APSARA_TEST_EQUAL(static_cast(now) + 600U, config.mOnetimeExpireTime); @@ -96,7 +97,7 @@ void PipelineConfigUnittest::TestOnetimeConfig() const { (*configJson)["global"]["ExcutionTimeout"] = 1000000U; auto now = time(nullptr); - PipelineConfig::ScopedClockOverride clockGuard(now); + ScopedClockOverride clockGuard(now); ConfigMock config("test", std::move(configJson), filepath); APSARA_TEST_TRUE(config.GetExpireTimeIfOneTime((*config.mDetail)["global"])); APSARA_TEST_EQUAL(static_cast(now) + 604800U, config.mOnetimeExpireTime); From be905c034df5c804fd8f568a58b3ecc2c6d5f01e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Apr 2026 07:12:04 +0000 Subject: [PATCH 4/7] refactor: rename gCurrentTime to gCurrentTimeNs (ns precision), unify non-test impls with system_clock Agent-Logs-Url: https://github.com/alibaba/loongcollector/sessions/c9045f85-6c3a-4a59-9cb4-21453adba656 Co-authored-by: yyuuttaaoo <1827594+yyuuttaaoo@users.noreply.github.com> --- core/common/TimeUtil.cpp | 32 ++++++++++++++++++++------------ core/common/TimeUtil.h | 14 ++++++++------ 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/core/common/TimeUtil.cpp b/core/common/TimeUtil.cpp index 7f341f7e02..477dfbfc12 100644 --- a/core/common/TimeUtil.cpp +++ b/core/common/TimeUtil.cpp @@ -39,7 +39,11 @@ namespace logtail { const std::string PRECISE_TIMESTAMP_DEFAULT_KEY = "precise_timestamp"; #ifdef APSARA_UNIT_TEST_MAIN -std::function gCurrentTime = []() -> time_t { return time(nullptr); }; +std::function gCurrentTimeNs = []() -> uint64_t { + return static_cast( + std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()) + .count()); +}; #endif std::string ConvertToTimeStamp(const time_t& t, const std::string& format) { @@ -68,27 +72,30 @@ std::string GetTimeStamp(time_t t, const std::string& format, bool isLocal) { uint64_t GetCurrentTimeInSeconds() { #ifdef APSARA_UNIT_TEST_MAIN - return static_cast(gCurrentTime()); + return gCurrentTimeNs() / 1000000000ULL; #else - return static_cast(time(nullptr)); + return static_cast( + std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count()); #endif } uint64_t GetCurrentTimeInMicroSeconds() { #ifdef APSARA_UNIT_TEST_MAIN - return static_cast(gCurrentTime()) * 1000000ULL; + return gCurrentTimeNs() / 1000ULL; #else - return std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()) - .count(); + return static_cast( + std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()) + .count()); #endif } uint64_t GetCurrentTimeInMilliSeconds() { #ifdef APSARA_UNIT_TEST_MAIN - return static_cast(gCurrentTime()) * 1000ULL; + return gCurrentTimeNs() / 1000000ULL; #else - return std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()) - .count(); + return static_cast( + std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()) + .count()); #endif } @@ -389,10 +396,11 @@ uint64_t GetPreciseTimestamp(uint64_t secondTimestamp, uint64_t GetCurrentTimeInNanoSeconds() { #ifdef APSARA_UNIT_TEST_MAIN - return static_cast(gCurrentTime()) * 1000000000ULL; + return gCurrentTimeNs(); #else - return std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()) - .count(); + return static_cast( + std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()) + .count()); #endif } diff --git a/core/common/TimeUtil.h b/core/common/TimeUtil.h index 0c303a93e7..a4f91a5f84 100644 --- a/core/common/TimeUtil.h +++ b/core/common/TimeUtil.h @@ -46,23 +46,25 @@ struct PreciseTimestampConfig { typedef timespec LogtailTime; #ifdef APSARA_UNIT_TEST_MAIN -// Injectable clock for deterministic unit tests. Defaults to time(nullptr). -extern std::function gCurrentTime; +// Injectable clock for deterministic unit tests. Returns nanoseconds since Unix epoch. +extern std::function gCurrentTimeNs; // RAII guard that temporarily pins the clock to a fixed value for the duration // of a test and restores the previous value on destruction. +// Accepts a second-granular time_t for convenience; precision is stored as nanoseconds. // Not thread-safe — intended for single-threaded unit tests only. struct ScopedClockOverride { - explicit ScopedClockOverride(time_t fixedNow) : mPrev(gCurrentTime) { - gCurrentTime = [fixedNow]() -> time_t { return fixedNow; }; + explicit ScopedClockOverride(time_t fixedNow) : mPrev(gCurrentTimeNs) { + const uint64_t fixedNs = static_cast(fixedNow) * 1000000000ULL; + gCurrentTimeNs = [fixedNs]() -> uint64_t { return fixedNs; }; } - ~ScopedClockOverride() { gCurrentTime = mPrev; } + ~ScopedClockOverride() { gCurrentTimeNs = mPrev; } ScopedClockOverride(const ScopedClockOverride&) = delete; ScopedClockOverride& operator=(const ScopedClockOverride&) = delete; private: - std::function mPrev; + std::function mPrev; }; #endif From 5d1f01d1924a400a185b810062afc62e4f7f4441 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Apr 2026 08:25:57 +0000 Subject: [PATCH 5/7] refactor: modernize ScopedClockOverride to accept system_clock::time_point, update test call sites Agent-Logs-Url: https://github.com/alibaba/loongcollector/sessions/c9205b39-a601-47e8-9b61-7580709691c6 Co-authored-by: yyuuttaaoo <1827594+yyuuttaaoo@users.noreply.github.com> --- core/common/TimeUtil.h | 7 +++-- .../config/OnetimeConfigUpdateUnittest.cpp | 28 +++++++++++++++---- .../config/PipelineConfigUnittest.cpp | 21 ++++++++++---- 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/core/common/TimeUtil.h b/core/common/TimeUtil.h index a4f91a5f84..957743c960 100644 --- a/core/common/TimeUtil.h +++ b/core/common/TimeUtil.h @@ -17,6 +17,7 @@ #pragma once #include +#include #include #include #include @@ -51,11 +52,11 @@ extern std::function gCurrentTimeNs; // RAII guard that temporarily pins the clock to a fixed value for the duration // of a test and restores the previous value on destruction. -// Accepts a second-granular time_t for convenience; precision is stored as nanoseconds. // Not thread-safe — intended for single-threaded unit tests only. struct ScopedClockOverride { - explicit ScopedClockOverride(time_t fixedNow) : mPrev(gCurrentTimeNs) { - const uint64_t fixedNs = static_cast(fixedNow) * 1000000000ULL; + explicit ScopedClockOverride(std::chrono::system_clock::time_point fixedNow) : mPrev(gCurrentTimeNs) { + const uint64_t fixedNs = static_cast( + std::chrono::duration_cast(fixedNow.time_since_epoch()).count()); gCurrentTimeNs = [fixedNs]() -> uint64_t { return fixedNs; }; } ~ScopedClockOverride() { gCurrentTimeNs = mPrev; } diff --git a/core/unittest/config/OnetimeConfigUpdateUnittest.cpp b/core/unittest/config/OnetimeConfigUpdateUnittest.cpp index 78532a50e3..c9e59db281 100644 --- a/core/unittest/config/OnetimeConfigUpdateUnittest.cpp +++ b/core/unittest/config/OnetimeConfigUpdateUnittest.cpp @@ -239,7 +239,7 @@ void OnetimeConfigUpdateUnittest::OnCollectionConfigUpdate() const { auto diff = PipelineConfigWatcher::GetInstance()->CheckConfigDiff(); APSARA_TEST_TRUE(diff.first.HasDiff()); - auto restartNow = time(nullptr); + auto restartNow = std::chrono::system_clock::now(); { ScopedClockOverride clockGuard(restartNow); CollectionPipelineManager::GetInstance()->UpdatePipelines(diff.first); @@ -249,14 +249,22 @@ void OnetimeConfigUpdateUnittest::OnCollectionConfigUpdate() const { APSARA_TEST_EQUAL(3U, sConfigManager->mConfigInfoMap.size()); { const auto& item = sConfigManager->mConfigInfoMap.at("new_config"); - APSARA_TEST_EQUAL(static_cast(restartNow) + 3600U, item.mExpireTime); + APSARA_TEST_EQUAL( + static_cast( + std::chrono::duration_cast(restartNow.time_since_epoch()).count()) + + 3600U, + item.mExpireTime); APSARA_TEST_EQUAL(configHash["new_config.json"], item.mConfigHash); APSARA_TEST_EQUAL(ConfigType::Collection, item.mType); APSARA_TEST_EQUAL(mConfigDir / filenames[0], item.mFilepath); } { const auto& item = sConfigManager->mConfigInfoMap.at("changed_config"); - APSARA_TEST_EQUAL(static_cast(restartNow) + 7200U, item.mExpireTime); + APSARA_TEST_EQUAL( + static_cast( + std::chrono::duration_cast(restartNow.time_since_epoch()).count()) + + 7200U, + item.mExpireTime); APSARA_TEST_EQUAL(configHash["changed_config.json"], item.mConfigHash); APSARA_TEST_EQUAL(ConfigType::Collection, item.mType); APSARA_TEST_EQUAL(mConfigDir / filenames[1], item.mFilepath); @@ -338,7 +346,7 @@ void OnetimeConfigUpdateUnittest::OnCollectionConfigUpdate() const { auto diff = PipelineConfigWatcher::GetInstance()->CheckConfigDiff(); APSARA_TEST_TRUE(diff.first.HasDiff()); - auto updateNow = time(nullptr); + auto updateNow = std::chrono::system_clock::now(); { ScopedClockOverride clockGuard(updateNow); CollectionPipelineManager::GetInstance()->UpdatePipelines(diff.first); @@ -348,14 +356,22 @@ void OnetimeConfigUpdateUnittest::OnCollectionConfigUpdate() const { APSARA_TEST_EQUAL(3U, sConfigManager->mConfigInfoMap.size()); { const auto& item = sConfigManager->mConfigInfoMap.at("new_config"); - APSARA_TEST_EQUAL(static_cast(updateNow) + 1000U, item.mExpireTime); + APSARA_TEST_EQUAL( + static_cast( + std::chrono::duration_cast(updateNow.time_since_epoch()).count()) + + 1000U, + item.mExpireTime); APSARA_TEST_EQUAL(configHash["new_config.json"], item.mConfigHash); APSARA_TEST_EQUAL(ConfigType::Collection, item.mType); APSARA_TEST_EQUAL(mConfigDir / filenames[0], item.mFilepath); } { const auto& item = sConfigManager->mConfigInfoMap.at("old_config"); - APSARA_TEST_EQUAL(static_cast(updateNow) + 1200U, item.mExpireTime); + APSARA_TEST_EQUAL( + static_cast( + std::chrono::duration_cast(updateNow.time_since_epoch()).count()) + + 1200U, + item.mExpireTime); APSARA_TEST_EQUAL(configHash["old_config.json"], item.mConfigHash); APSARA_TEST_EQUAL(ConfigType::Collection, item.mType); APSARA_TEST_EQUAL(mConfigDir / filenames[1], item.mFilepath); diff --git a/core/unittest/config/PipelineConfigUnittest.cpp b/core/unittest/config/PipelineConfigUnittest.cpp index b6253ee1fb..fed4909fb5 100644 --- a/core/unittest/config/PipelineConfigUnittest.cpp +++ b/core/unittest/config/PipelineConfigUnittest.cpp @@ -72,11 +72,14 @@ void PipelineConfigUnittest::TestOnetimeConfig() const { auto configJson = make_unique(); (*configJson)["global"]["ExcutionTimeout"] = true; - auto now = time(nullptr); + auto now = std::chrono::system_clock::now(); ScopedClockOverride clockGuard(now); ConfigMock config("test", std::move(configJson), filepath); APSARA_TEST_TRUE(config.GetExpireTimeIfOneTime((*config.mDetail)["global"])); - APSARA_TEST_EQUAL(static_cast(now) + 604800U, config.mOnetimeExpireTime); + APSARA_TEST_EQUAL( + static_cast(std::chrono::duration_cast(now.time_since_epoch()).count()) + + 604800U, + config.mOnetimeExpireTime); APSARA_TEST_FALSE(config.mIsRunningBeforeStart); } { @@ -84,11 +87,14 @@ void PipelineConfigUnittest::TestOnetimeConfig() const { auto configJson = make_unique(); (*configJson)["global"]["ExcutionTimeout"] = 1U; - auto now = time(nullptr); + auto now = std::chrono::system_clock::now(); ScopedClockOverride clockGuard(now); ConfigMock config("test", std::move(configJson), filepath); APSARA_TEST_TRUE(config.GetExpireTimeIfOneTime((*config.mDetail)["global"])); - APSARA_TEST_EQUAL(static_cast(now) + 600U, config.mOnetimeExpireTime); + APSARA_TEST_EQUAL( + static_cast(std::chrono::duration_cast(now.time_since_epoch()).count()) + + 600U, + config.mOnetimeExpireTime); APSARA_TEST_FALSE(config.mIsRunningBeforeStart); } { @@ -96,11 +102,14 @@ void PipelineConfigUnittest::TestOnetimeConfig() const { auto configJson = make_unique(); (*configJson)["global"]["ExcutionTimeout"] = 1000000U; - auto now = time(nullptr); + auto now = std::chrono::system_clock::now(); ScopedClockOverride clockGuard(now); ConfigMock config("test", std::move(configJson), filepath); APSARA_TEST_TRUE(config.GetExpireTimeIfOneTime((*config.mDetail)["global"])); - APSARA_TEST_EQUAL(static_cast(now) + 604800U, config.mOnetimeExpireTime); + APSARA_TEST_EQUAL( + static_cast(std::chrono::duration_cast(now.time_since_epoch()).count()) + + 604800U, + config.mOnetimeExpireTime); APSARA_TEST_FALSE(config.mIsRunningBeforeStart); } From 3277e8f2a3480261ad4d08617d492d3a71d432cc Mon Sep 17 00:00:00 2001 From: Tom Yu Date: Wed, 15 Apr 2026 17:43:04 +0800 Subject: [PATCH 6/7] Remove duplicate include for TimeUtil.h Removed duplicate include of 'common/TimeUtil.h'. --- core/unittest/config/PipelineConfigUnittest.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/unittest/config/PipelineConfigUnittest.cpp b/core/unittest/config/PipelineConfigUnittest.cpp index fed4909fb5..dfc7ae2f24 100644 --- a/core/unittest/config/PipelineConfigUnittest.cpp +++ b/core/unittest/config/PipelineConfigUnittest.cpp @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include "common/TimeUtil.h" #include "config/OnetimeConfigInfoManager.h" #include "config/PipelineConfig.h" -#include "common/TimeUtil.h" #include "logger/Logger.h" #include "unittest/Unittest.h" From c32a7a43b8901bdd1b6dd855257e02d6ed099270 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Apr 2026 15:34:54 +0000 Subject: [PATCH 7/7] fix: address review comments - thread_local clock, remove unnecessary friend, fix remaining flaky assertion Agent-Logs-Url: https://github.com/alibaba/loongcollector/sessions/4d1f4f26-225f-43cd-a0e1-199437031b90 Co-authored-by: yyuuttaaoo <1827594+yyuuttaaoo@users.noreply.github.com> --- core/common/TimeUtil.cpp | 2 +- core/common/TimeUtil.h | 6 ++++-- core/config/PipelineConfig.h | 1 - core/unittest/config/PipelineConfigUnittest.cpp | 7 ++++++- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/core/common/TimeUtil.cpp b/core/common/TimeUtil.cpp index 477dfbfc12..2f28ee9d6b 100644 --- a/core/common/TimeUtil.cpp +++ b/core/common/TimeUtil.cpp @@ -39,7 +39,7 @@ namespace logtail { const std::string PRECISE_TIMESTAMP_DEFAULT_KEY = "precise_timestamp"; #ifdef APSARA_UNIT_TEST_MAIN -std::function gCurrentTimeNs = []() -> uint64_t { +thread_local std::function gCurrentTimeNs = []() -> uint64_t { return static_cast( std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()) .count()); diff --git a/core/common/TimeUtil.h b/core/common/TimeUtil.h index 957743c960..6b09d13027 100644 --- a/core/common/TimeUtil.h +++ b/core/common/TimeUtil.h @@ -48,11 +48,13 @@ typedef timespec LogtailTime; #ifdef APSARA_UNIT_TEST_MAIN // Injectable clock for deterministic unit tests. Returns nanoseconds since Unix epoch. -extern std::function gCurrentTimeNs; +// thread_local ensures background threads spawned by pipelines always use the real clock, +// while the test thread can safely override without data races. +extern thread_local std::function gCurrentTimeNs; // RAII guard that temporarily pins the clock to a fixed value for the duration // of a test and restores the previous value on destruction. -// Not thread-safe — intended for single-threaded unit tests only. +// Safe even when pipelines start background threads — each thread has its own copy. struct ScopedClockOverride { explicit ScopedClockOverride(std::chrono::system_clock::time_point fixedNow) : mPrev(gCurrentTimeNs) { const uint64_t fixedNs = static_cast( diff --git a/core/config/PipelineConfig.h b/core/config/PipelineConfig.h index 881e0f3fbe..80cba4bced 100644 --- a/core/config/PipelineConfig.h +++ b/core/config/PipelineConfig.h @@ -55,7 +55,6 @@ struct PipelineConfig { #ifdef APSARA_UNIT_TEST_MAIN friend class PipelineConfigUnittest; - friend class OnetimeConfigUpdateUnittest; #endif }; diff --git a/core/unittest/config/PipelineConfigUnittest.cpp b/core/unittest/config/PipelineConfigUnittest.cpp index dfc7ae2f24..996ca2fbab 100644 --- a/core/unittest/config/PipelineConfigUnittest.cpp +++ b/core/unittest/config/PipelineConfigUnittest.cpp @@ -158,9 +158,14 @@ void PipelineConfigUnittest::TestOnetimeConfig() const { auto configJson = make_unique(); (*configJson)["global"]["ExcutionTimeout"] = 3600U; + auto now = std::chrono::system_clock::now(); + ScopedClockOverride clockGuard(now); ConfigMock config("new_config", std::move(configJson), filepath); APSARA_TEST_TRUE(config.GetExpireTimeIfOneTime((*config.mDetail)["global"])); - APSARA_TEST_EQUAL(time(nullptr) + 3600U, config.mOnetimeExpireTime); + APSARA_TEST_EQUAL( + static_cast(std::chrono::duration_cast(now.time_since_epoch()).count()) + + 3600U, + config.mOnetimeExpireTime); APSARA_TEST_FALSE(config.mIsRunningBeforeStart); APSARA_TEST_EQUAL(sConfigManager->mConfigCheckpointMap.end(), sConfigManager->mConfigCheckpointMap.find("new_config"));