aboutsummaryrefslogtreecommitdiffstats
path: root/library
diff options
context:
space:
mode:
authorarkady-e1ppa <arkady-e1ppa@yandex-team.com>2024-08-15 18:32:55 +0300
committerarkady-e1ppa <arkady-e1ppa@yandex-team.com>2024-08-15 18:44:19 +0300
commit28ff4da78aa89f0226af33522332b7f06521412e (patch)
tree09c5bbbba83b4a04d51a77c654d0eaeab9bb404f /library
parent0ecc758c681a1a8d54fbf95f0b58bba175518fac (diff)
downloadydb-28ff4da78aa89f0226af33522332b7f06521412e.tar.gz
YT-21233: Split error into error and stripped_error to rid the latter of deps on global variables
What happened: 1. error contents has been split into stripped_error and error. stripped_error contains the error itself (with attributes for now) and macros; error contains stripped_error and some extensions, namely, functions to get fiberId, hostname and traceid/spanid and all functions used to (de-)serialize error. This means that you cannot print error if you only include stripped_error, therefore you are likely to still require the entire error.h at the moment. 2. Mechanic for gathering origin attributes has been moved to newly created library/cpp/yt/error thus having no dependency on fibers, net or tracing. stripped_error uses these attributes as extendable semi-erased (meaning, you still would have to add a field and recompile the entire thing, but you don't have to introduce an extra dependency) storage for a bunch of attributes 3. Parsing of said attributes is done in error file (and not stripped_error). P.S. So far the plan is to eventually move stripped_error (once dependency on core/ytree/attributes is eliminated) without any actual change to dependency graph of anything outside of core (e.g. you would still have to include misc/error.h to use it). Next step would be re-teaching the error how to print, which would move some more methods from core to the standalone module. After that one could finally depend on the error itself and not the entire core. Annotations: [nodiff:caesar] 66615172181355821241d2e5f8e4a0f15e0ea791
Diffstat (limited to 'library')
-rw-r--r--library/cpp/yt/error/origin_attributes.cpp112
-rw-r--r--library/cpp/yt/error/origin_attributes.h83
-rw-r--r--library/cpp/yt/error/ya.make18
-rw-r--r--library/cpp/yt/global/access.h80
-rw-r--r--library/cpp/yt/global/impl.cpp126
-rw-r--r--library/cpp/yt/global/mock_modules/module1_defs/direct_access.h17
-rw-r--r--library/cpp/yt/global/mock_modules/module1_defs/test_variable.cpp38
-rw-r--r--library/cpp/yt/global/mock_modules/module1_defs/ya.make14
-rw-r--r--library/cpp/yt/global/mock_modules/module1_public/test_tag.h14
-rw-r--r--library/cpp/yt/global/mock_modules/module1_public/ya.make9
-rw-r--r--library/cpp/yt/global/mock_modules/module2_defs/direct_access.h22
-rw-r--r--library/cpp/yt/global/mock_modules/module2_defs/test_variable.cpp60
-rw-r--r--library/cpp/yt/global/mock_modules/module2_defs/ya.make16
-rw-r--r--library/cpp/yt/global/mock_modules/module2_public/test_tag.h15
-rw-r--r--library/cpp/yt/global/mock_modules/module2_public/ya.make9
-rw-r--r--library/cpp/yt/global/mock_modules/module3_defs/direct_access.h18
-rw-r--r--library/cpp/yt/global/mock_modules/module3_defs/test_variable.cpp41
-rw-r--r--library/cpp/yt/global/mock_modules/module3_defs/ya.make14
-rw-r--r--library/cpp/yt/global/mock_modules/module3_public/test_tag.h21
-rw-r--r--library/cpp/yt/global/mock_modules/module3_public/ya.make9
-rw-r--r--library/cpp/yt/global/unittests/global_variable/just_works_ut/main.cpp73
-rw-r--r--library/cpp/yt/global/unittests/global_variable/just_works_ut/ya.make16
-rw-r--r--library/cpp/yt/global/unittests/global_variable/missing_module_ut/main.cpp23
-rw-r--r--library/cpp/yt/global/unittests/global_variable/missing_module_ut/ya.make13
-rw-r--r--library/cpp/yt/global/unittests/global_variable/odr_violation_avoidance_ut/main.cpp41
-rw-r--r--library/cpp/yt/global/unittests/global_variable/odr_violation_avoidance_ut/ya.make16
-rw-r--r--library/cpp/yt/global/unittests/global_variable/two_modules_ut/main.cpp90
-rw-r--r--library/cpp/yt/global/unittests/global_variable/two_modules_ut/ya.make20
-rw-r--r--library/cpp/yt/global/variable-inl.h95
-rw-r--r--library/cpp/yt/global/variable.h53
-rw-r--r--library/cpp/yt/global/ya.make23
-rw-r--r--library/cpp/yt/memory/erased_storage-inl.h27
-rw-r--r--library/cpp/yt/memory/erased_storage.h36
-rw-r--r--library/cpp/yt/memory/unittests/erased_storage_ut.cpp22
-rw-r--r--library/cpp/yt/threading/public.h5
35 files changed, 1272 insertions, 17 deletions
diff --git a/library/cpp/yt/error/origin_attributes.cpp b/library/cpp/yt/error/origin_attributes.cpp
new file mode 100644
index 0000000000..5ff0b03933
--- /dev/null
+++ b/library/cpp/yt/error/origin_attributes.cpp
@@ -0,0 +1,112 @@
+#include "origin_attributes.h"
+
+#include <library/cpp/yt/assert/assert.h>
+
+#include <library/cpp/yt/misc/thread_name.h>
+#include <library/cpp/yt/misc/tls.h>
+
+#include <library/cpp/yt/string/format.h>
+
+#include <util/system/thread.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+YT_DEFINE_THREAD_LOCAL(bool, ErrorSanitizerEnabled, false);
+YT_DEFINE_THREAD_LOCAL(TInstant, ErrorSanitizerDatetimeOverride);
+YT_DEFINE_THREAD_LOCAL(TSharedRef, ErrorSanitizerLocalHostNameOverride);
+
+TErrorSanitizerGuard::TErrorSanitizerGuard(TInstant datetimeOverride, TSharedRef localHostNameOverride)
+ : SavedEnabled_(ErrorSanitizerEnabled())
+ , SavedDatetimeOverride_(ErrorSanitizerDatetimeOverride())
+ , SavedLocalHostNameOverride_(ErrorSanitizerLocalHostNameOverride())
+{
+ ErrorSanitizerEnabled() = true;
+ ErrorSanitizerDatetimeOverride() = datetimeOverride;
+ ErrorSanitizerLocalHostNameOverride() = std::move(localHostNameOverride);
+}
+
+TErrorSanitizerGuard::~TErrorSanitizerGuard()
+{
+ YT_ASSERT(ErrorSanitizerEnabled());
+
+ ErrorSanitizerEnabled() = SavedEnabled_;
+ ErrorSanitizerDatetimeOverride() = SavedDatetimeOverride_;
+ ErrorSanitizerLocalHostNameOverride() = std::move(SavedLocalHostNameOverride_);
+}
+
+bool IsErrorSanitizerEnabled() noexcept
+{
+ return ErrorSanitizerEnabled();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+bool TOriginAttributes::operator==(const TOriginAttributes& other) const noexcept
+{
+ return
+ Host == other.Host &&
+ Datetime == other.Datetime &&
+ Pid == other.Pid &&
+ Tid == other.Tid &&
+ ExtensionData == other.ExtensionData;
+}
+
+void TOriginAttributes::Capture()
+{
+ if (ErrorSanitizerEnabled()) {
+ Datetime = ErrorSanitizerDatetimeOverride();
+ HostHolder = ErrorSanitizerLocalHostNameOverride();
+ Host = HostHolder.empty() ? TStringBuf() : TStringBuf(HostHolder.Begin(), HostHolder.End());
+ return;
+ }
+
+ Datetime = TInstant::Now();
+ Pid = GetPID();
+ Tid = TThread::CurrentThreadId();
+ ThreadName = GetCurrentThreadName();
+ ExtensionData = NDetail::GetExtensionData();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+std::optional<TOriginAttributes::TErasedExtensionData> GetExtensionData()
+{
+ using TFunctor = TOriginAttributes::TErasedExtensionData(*)();
+
+ if (auto strong = NGlobal::GetErasedVariable(GetExtensionDataTag)) {
+ return strong->AsConcrete<TFunctor>()();
+ }
+ return std::nullopt;
+}
+
+TString FormatOrigin(const TOriginAttributes& attributes)
+{
+ using TFunctor = TString(*)(const TOriginAttributes&);
+
+ if (auto strong = NGlobal::GetErasedVariable(FormatOriginTag)) {
+ return strong->AsConcrete<TFunctor>()(attributes);
+ }
+
+ return Format(
+ "%v (pid %v, thread %v)",
+ attributes.Host,
+ attributes.Pid,
+ MakeFormatterWrapper([&] (auto* builder) {
+ auto threadName = attributes.ThreadName.ToStringBuf();
+ if (threadName.empty()) {
+ FormatValue(builder, attributes.Tid, "v");
+ return;
+ }
+ FormatValue(builder, threadName, "v");
+ }));
+}
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/library/cpp/yt/error/origin_attributes.h b/library/cpp/yt/error/origin_attributes.h
new file mode 100644
index 0000000000..05815c1e38
--- /dev/null
+++ b/library/cpp/yt/error/origin_attributes.h
@@ -0,0 +1,83 @@
+#pragma once
+
+#include <library/cpp/yt/global/access.h>
+
+#include <library/cpp/yt/memory/ref.h>
+
+#include <library/cpp/yt/misc/guid.h>
+#include <library/cpp/yt/misc/thread_name.h>
+
+#include <library/cpp/yt/threading/public.h>
+
+#include <util/datetime/base.h>
+
+#include <util/system/getpid.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! When this guard is set, newly created errors do not have non-deterministic
+//! system attributes and have "datetime" and "host" attributes overridden with a given values.
+class TErrorSanitizerGuard
+ : public TNonCopyable
+{
+public:
+ TErrorSanitizerGuard(TInstant datetimeOverride, TSharedRef localHostNameOverride);
+ ~TErrorSanitizerGuard();
+
+private:
+ const bool SavedEnabled_;
+ const TInstant SavedDatetimeOverride_;
+ const TSharedRef SavedLocalHostNameOverride_;
+};
+
+bool IsErrorSanitizerEnabled() noexcept;
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TOriginAttributes
+{
+ static constexpr size_t ExtensionDataByteSizeCap = 64;
+ using TErasedExtensionData = TErasedStorage<ExtensionDataByteSizeCap>;
+
+ TProcessId Pid;
+
+ NThreading::TThreadId Tid;
+ TThreadName ThreadName;
+
+ TInstant Datetime;
+
+ TSharedRef HostHolder;
+ mutable TStringBuf Host;
+
+ // Opaque storage for data from yt/yt/core.
+ // Currently may contain FiberId, TraceId, SpandId.
+ std::optional<TErasedExtensionData> ExtensionData;
+
+ bool operator==(const TOriginAttributes& other) const noexcept;
+
+ void Capture();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+inline constexpr NGlobal::TVariableTag GetExtensionDataTag = {};
+inline constexpr NGlobal::TVariableTag FormatOriginTag = {};
+inline constexpr NGlobal::TVariableTag ExtractFromDictionaryTag = {};
+
+////////////////////////////////////////////////////////////////////////////////
+
+// These are "weak" symbols.
+// NB(arkady-e1ppa): ExtractFromDictionary symbol is left in yt/yt/core/misc/origin_attributes
+// because it depends on ytree for now.
+std::optional<TOriginAttributes::TErasedExtensionData> GetExtensionData();
+TString FormatOrigin(const TOriginAttributes& attributes);
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/library/cpp/yt/error/ya.make b/library/cpp/yt/error/ya.make
new file mode 100644
index 0000000000..f5bd946199
--- /dev/null
+++ b/library/cpp/yt/error/ya.make
@@ -0,0 +1,18 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/library/cpp/yt/ya_cpp.make.inc)
+
+PEERDIR(
+ library/cpp/yt/assert
+ library/cpp/yt/global
+ library/cpp/yt/memory
+ library/cpp/yt/misc
+ library/cpp/yt/threading
+ library/cpp/yt/string
+)
+
+SRCS(
+ origin_attributes.cpp
+)
+
+END()
diff --git a/library/cpp/yt/global/access.h b/library/cpp/yt/global/access.h
new file mode 100644
index 0000000000..e746f80c8b
--- /dev/null
+++ b/library/cpp/yt/global/access.h
@@ -0,0 +1,80 @@
+#pragma once
+
+#include <library/cpp/yt/memory/erased_storage.h>
+
+#include <library/cpp/yt/misc/strong_typedef.h>
+
+#include <atomic>
+
+namespace NYT::NGlobal {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+class TGlobalVariablesRegistry;
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline constexpr size_t GlobalVariableMaxByteSize = 32;
+using TErasedStorage = NYT::TErasedStorage<GlobalVariableMaxByteSize>;
+
+// NB(arkady-e1ppa): Accessor must ensure thread-safety on its own.
+using TAccessor = TErasedStorage(*)() noexcept;
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Usage:
+/*
+ * // public.h file:
+ * // NB: It's important to mark it inline for linker to deduplicate
+ * // addresses accross different UT's.
+ * inline constexpr NGlobal::TVariableTag MyGlobalVarTag = {};
+ *
+ *
+ *
+ * // some_stuff.cpp file
+ *
+ * TErasedStorage GetMyVar()
+ * {
+ * // definition here
+ * }
+ * NGlobal::Variable<int> MyGlobalVar{MyGlobalVarTag, &GetMyVar};
+ *
+ *
+ *
+ * // other_stuff.cpp file
+ *
+ *
+ * int ReadMyVar()
+ * {
+ * auto erased = NGlobal::GetErasedVariable(MyGlobalVarTag);
+ * return erased->AsConcrete<int>();
+ * }
+ */
+class TVariableTag
+{
+public:
+ TVariableTag() = default;
+
+ TVariableTag(const TVariableTag& other) = delete;
+ TVariableTag& operator=(const TVariableTag& other) = delete;
+
+private:
+ friend class ::NYT::NGlobal::NDetail::TGlobalVariablesRegistry;
+
+ mutable std::atomic<bool> Initialized_ = false;
+ mutable std::atomic<int> Key_ = -1;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Defined in impl.cpp.
+// |std::nullopt| iff global variable with a given tag is not present.
+std::optional<TErasedStorage> GetErasedVariable(const TVariableTag& tag);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NGlobal
diff --git a/library/cpp/yt/global/impl.cpp b/library/cpp/yt/global/impl.cpp
new file mode 100644
index 0000000000..033eedca34
--- /dev/null
+++ b/library/cpp/yt/global/impl.cpp
@@ -0,0 +1,126 @@
+#include "access.h"
+#include "variable.h"
+
+#include <library/cpp/yt/memory/leaky_singleton.h>
+
+#include <array>
+
+namespace NYT::NGlobal {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+inline constexpr int MaxTrackedGlobalVariables = 32;
+
+////////////////////////////////////////////////////////////////////////////////
+
+class TGlobalVariablesRegistry
+{
+public:
+ static TGlobalVariablesRegistry* Get()
+ {
+ return LeakySingleton<TGlobalVariablesRegistry>();
+ }
+
+ void RegisterAccessor(const TVariableTag& tag, TAccessor accessor)
+ {
+ if (!tag.Initialized_.exchange(true, std::memory_order::relaxed)) { // (a)
+ DoRegisterAccessor(tag, accessor);
+ return;
+ }
+
+ TryVerifyingExistingAccessor(tag, accessor);
+ }
+
+ std::optional<TErasedStorage> GetVariable(const TVariableTag& tag)
+ {
+ auto key = tag.Key_.load(std::memory_order::acquire); // (e)
+ if (key != -1) {
+ return Accessors_[key](); // (f)
+ }
+
+ return std::nullopt;
+ }
+
+private:
+ std::atomic<int> KeyGenerator_ = 0;
+ std::array<TAccessor, MaxTrackedGlobalVariables> Accessors_;
+
+ void DoRegisterAccessor(const TVariableTag& tag, TAccessor accessor)
+ {
+ // Get id -> place accessor -> store id
+ auto key = KeyGenerator_.fetch_add(1, std::memory_order::relaxed); // (b)
+
+ YT_VERIFY(key < MaxTrackedGlobalVariables);
+
+ Accessors_[key] = accessor; // (c)
+
+ tag.Key_.store(key, std::memory_order::release); // (d)
+ }
+
+ void TryVerifyingExistingAccessor(const TVariableTag& tag, TAccessor accessor)
+ {
+ auto key = tag.Key_.load(std::memory_order::acquire); // (e')
+ if (key == -1) {
+ // Accessor is about to be set.
+
+ // In order to avoid deadlock caused by forks
+ // we just leave. We could try acquiring fork
+ // locks here but this makes our check too expensive
+ // to be bothered.
+ return;
+ }
+
+ // Accessor has been already set -> safe to read it.
+ YT_VERIFY(Accessors_[key] == accessor); // (f')
+ }
+};
+
+// (arkady-e1ppa): Memory orders:
+/*
+ We have two scenarios: 2 writes and write & read:
+
+ 2 writes: Accessors_ is protected via Initialized_ flag
+ and KeyGenerator_ counter.
+ 1) RMW (a) reads the last value in modification order
+ thus relaxed is enough to ensure <= 1 threads registering
+ per Tag.
+ 2) KeyGenerator_ uses the same logic (see (b))
+ to ensure <= 1 threads registering per index in array.
+
+ If there are two writes per tag, then there is a "losing"
+ thread which read Initialized_ // true. For all intents
+ and purposes TryVerifyingExistingAccessor call is identical
+ to GetVariable call.
+
+ write & read: Relevant execution is below
+ W^na(Accessors_[id], 0x0) // Ctor
+ T1(Register) T2(Read)
+ W^na(Accessors_[id], 0x42) (c) R^acq(Key_, id) (e)
+ W^rel(Key_, id) (d) R^na(Accessors_[id], 0x42) (f)
+
+ (d) -rf-> (e) => (d) -SW-> (e). Since (c) -SB-> (d) and (e) -SB-> (f)
+ we have (c) -strongly HB-> (f) (strongly happens before). Thus we must
+ read 0x42 from Accessors_[id] (and not 0x0 which was written in ctor).
+ */
+
+////////////////////////////////////////////////////////////////////////////////
+
+void RegisterVariable(const TVariableTag& tag, TAccessor accessor)
+{
+ TGlobalVariablesRegistry::Get()->RegisterAccessor(tag, accessor);
+}
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::optional<TErasedStorage> GetErasedVariable(const TVariableTag& tag)
+{
+ return NDetail::TGlobalVariablesRegistry::Get()->GetVariable(tag);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NGlobal
diff --git a/library/cpp/yt/global/mock_modules/module1_defs/direct_access.h b/library/cpp/yt/global/mock_modules/module1_defs/direct_access.h
new file mode 100644
index 0000000000..abd536474c
--- /dev/null
+++ b/library/cpp/yt/global/mock_modules/module1_defs/direct_access.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include <cstdint>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+int GetTestVariable1();
+void SetTestVariable1(int val);
+
+int GetTlsVariable();
+void SetTlsVariable(int val);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/library/cpp/yt/global/mock_modules/module1_defs/test_variable.cpp b/library/cpp/yt/global/mock_modules/module1_defs/test_variable.cpp
new file mode 100644
index 0000000000..78912be660
--- /dev/null
+++ b/library/cpp/yt/global/mock_modules/module1_defs/test_variable.cpp
@@ -0,0 +1,38 @@
+#include "direct_access.h"
+
+#include <library/cpp/yt/global/mock_modules/module1_public/test_tag.h>
+
+#include <library/cpp/yt/global/variable.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+YT_DEFINE_TRACKED_GLOBAL(int, TestVariable1, TestTag1, 42);
+YT_DEFINE_TRACKED_THREAD_LOCAL(int, TlsVariable, ThreadLocalTag, 0);
+
+////////////////////////////////////////////////////////////////////////////////
+
+int GetTestVariable1()
+{
+ return TestVariable1.Get();
+}
+
+void SetTestVariable1(int val)
+{
+ TestVariable1.Set(val);
+}
+
+int GetTlsVariable()
+{
+ return TlsVariable();
+}
+
+void SetTlsVariable(int val)
+{
+ TlsVariable() = val;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/library/cpp/yt/global/mock_modules/module1_defs/ya.make b/library/cpp/yt/global/mock_modules/module1_defs/ya.make
new file mode 100644
index 0000000000..7152eb10b2
--- /dev/null
+++ b/library/cpp/yt/global/mock_modules/module1_defs/ya.make
@@ -0,0 +1,14 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/library/cpp/yt/ya_cpp.make.inc)
+
+SRC(
+ test_variable.cpp
+)
+
+PEERDIR(
+ library/cpp/yt/global
+ library/cpp/yt/global/mock_modules/module1_public
+)
+
+END()
diff --git a/library/cpp/yt/global/mock_modules/module1_public/test_tag.h b/library/cpp/yt/global/mock_modules/module1_public/test_tag.h
new file mode 100644
index 0000000000..0b3ed55ebd
--- /dev/null
+++ b/library/cpp/yt/global/mock_modules/module1_public/test_tag.h
@@ -0,0 +1,14 @@
+#pragma once
+
+#include <library/cpp/yt/global/access.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline constexpr NGlobal::TVariableTag TestTag1 = {};
+inline constexpr NGlobal::TVariableTag ThreadLocalTag = {};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/library/cpp/yt/global/mock_modules/module1_public/ya.make b/library/cpp/yt/global/mock_modules/module1_public/ya.make
new file mode 100644
index 0000000000..524e22b97e
--- /dev/null
+++ b/library/cpp/yt/global/mock_modules/module1_public/ya.make
@@ -0,0 +1,9 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/library/cpp/yt/ya_cpp.make.inc)
+
+PEERDIR(
+ library/cpp/yt/global
+)
+
+END()
diff --git a/library/cpp/yt/global/mock_modules/module2_defs/direct_access.h b/library/cpp/yt/global/mock_modules/module2_defs/direct_access.h
new file mode 100644
index 0000000000..d5f47c1be7
--- /dev/null
+++ b/library/cpp/yt/global/mock_modules/module2_defs/direct_access.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include <library/cpp/yt/misc/guid.h>
+
+#include <array>
+#include <cstdint>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TGuid GetTestVariable2();
+
+void SetTestVariable2(TGuid val);
+
+std::array<int, 4> GetTestVariable3();
+
+void SetTestVariable3(std::array<int, 4> val);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/library/cpp/yt/global/mock_modules/module2_defs/test_variable.cpp b/library/cpp/yt/global/mock_modules/module2_defs/test_variable.cpp
new file mode 100644
index 0000000000..3db411bcb5
--- /dev/null
+++ b/library/cpp/yt/global/mock_modules/module2_defs/test_variable.cpp
@@ -0,0 +1,60 @@
+#include "direct_access.h"
+
+#include <library/cpp/yt/global/mock_modules/module2_public/test_tag.h>
+
+#include <library/cpp/yt/global/variable.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+NGlobal::TErasedStorage GetGlobal2() noexcept;
+
+NGlobal::TErasedStorage GetGlobal3() noexcept;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static NGlobal::TVariable<TGuid> TestVariable2{TestTag2, &GetGlobal2, TGuid{42, 77}};
+
+static NGlobal::TVariable<std::array<int, 4>> TestVariable3{
+ TestTag3,
+ &GetGlobal3,
+ std::array{0, 0, 42, 77}};
+
+////////////////////////////////////////////////////////////////////////////////
+
+NGlobal::TErasedStorage GetGlobal2() noexcept
+{
+ return NGlobal::TErasedStorage{TestVariable2.Get()};
+}
+
+NGlobal::TErasedStorage GetGlobal3() noexcept
+{
+ return NGlobal::TErasedStorage{TestVariable3.Get()};
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TGuid GetTestVariable2()
+{
+ return TestVariable2.Get();
+}
+
+void SetTestVariable2(TGuid val)
+{
+ TestVariable2.Set(val);
+}
+
+std::array<int, 4> GetTestVariable3()
+{
+ return TestVariable3.Get();
+}
+
+void SetTestVariable3(std::array<int, 4> val)
+{
+ TestVariable3.Set(val);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/library/cpp/yt/global/mock_modules/module2_defs/ya.make b/library/cpp/yt/global/mock_modules/module2_defs/ya.make
new file mode 100644
index 0000000000..6692debb2f
--- /dev/null
+++ b/library/cpp/yt/global/mock_modules/module2_defs/ya.make
@@ -0,0 +1,16 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/library/cpp/yt/ya_cpp.make.inc)
+
+SRC(
+ test_variable.cpp
+)
+
+PEERDIR(
+ library/cpp/yt/misc
+
+ library/cpp/yt/global
+ library/cpp/yt/global/mock_modules/module2_public
+)
+
+END()
diff --git a/library/cpp/yt/global/mock_modules/module2_public/test_tag.h b/library/cpp/yt/global/mock_modules/module2_public/test_tag.h
new file mode 100644
index 0000000000..3578368dcd
--- /dev/null
+++ b/library/cpp/yt/global/mock_modules/module2_public/test_tag.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include <library/cpp/yt/global/access.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline constexpr NGlobal::TVariableTag TestTag2 = {};
+
+inline constexpr NGlobal::TVariableTag TestTag3 = {};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/library/cpp/yt/global/mock_modules/module2_public/ya.make b/library/cpp/yt/global/mock_modules/module2_public/ya.make
new file mode 100644
index 0000000000..524e22b97e
--- /dev/null
+++ b/library/cpp/yt/global/mock_modules/module2_public/ya.make
@@ -0,0 +1,9 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/library/cpp/yt/ya_cpp.make.inc)
+
+PEERDIR(
+ library/cpp/yt/global
+)
+
+END()
diff --git a/library/cpp/yt/global/mock_modules/module3_defs/direct_access.h b/library/cpp/yt/global/mock_modules/module3_defs/direct_access.h
new file mode 100644
index 0000000000..3a2a4e24c4
--- /dev/null
+++ b/library/cpp/yt/global/mock_modules/module3_defs/direct_access.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include <library/cpp/yt/misc/guid.h>
+
+#include <array>
+#include <cstdint>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::array<int, 2> GetTestVariable3();
+
+void SetTestVariable3(std::array<int, 2> val);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/library/cpp/yt/global/mock_modules/module3_defs/test_variable.cpp b/library/cpp/yt/global/mock_modules/module3_defs/test_variable.cpp
new file mode 100644
index 0000000000..a02fd15130
--- /dev/null
+++ b/library/cpp/yt/global/mock_modules/module3_defs/test_variable.cpp
@@ -0,0 +1,41 @@
+#include "direct_access.h"
+
+#include <library/cpp/yt/global/mock_modules/module3_public/test_tag.h>
+
+#include <library/cpp/yt/global/variable.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+NGlobal::TErasedStorage GetGlobal3() noexcept;
+
+////////////////////////////////////////////////////////////////////////////////
+
+static NGlobal::TVariable<std::array<int, 2>> TestVariable3{
+ TestTag3,
+ &GetGlobal3,
+ std::array{11, 22}};
+
+////////////////////////////////////////////////////////////////////////////////
+
+NGlobal::TErasedStorage GetGlobal3() noexcept
+{
+ return NGlobal::TErasedStorage{TestVariable3.Get()};
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+std::array<int, 2> GetTestVariable3()
+{
+ return TestVariable3.Get();
+}
+
+void SetTestVariable3(std::array<int, 2> val)
+{
+ TestVariable3.Set(val);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/library/cpp/yt/global/mock_modules/module3_defs/ya.make b/library/cpp/yt/global/mock_modules/module3_defs/ya.make
new file mode 100644
index 0000000000..9fbc310488
--- /dev/null
+++ b/library/cpp/yt/global/mock_modules/module3_defs/ya.make
@@ -0,0 +1,14 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/library/cpp/yt/ya_cpp.make.inc)
+
+SRC(
+ test_variable.cpp
+)
+
+PEERDIR(
+ library/cpp/yt/global
+ library/cpp/yt/global/mock_modules/module3_public
+)
+
+END()
diff --git a/library/cpp/yt/global/mock_modules/module3_public/test_tag.h b/library/cpp/yt/global/mock_modules/module3_public/test_tag.h
new file mode 100644
index 0000000000..a7e59f2762
--- /dev/null
+++ b/library/cpp/yt/global/mock_modules/module3_public/test_tag.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include <library/cpp/yt/global/access.h>
+
+namespace NYT {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// NB(arkady-e1ppa): This tag name conflicts with the one in module2.
+// However, if module2_defs is not linked at the same time as
+// module3_defs, you are guaranteed to read global from
+// module3_defs.
+// If you happen to mistakenly link module2_defs
+// instead of module3_defs, you will actually
+// read erased varibale from module_defs.
+// Be careful!
+inline constexpr NGlobal::TVariableTag TestTag3 = {};
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT
diff --git a/library/cpp/yt/global/mock_modules/module3_public/ya.make b/library/cpp/yt/global/mock_modules/module3_public/ya.make
new file mode 100644
index 0000000000..524e22b97e
--- /dev/null
+++ b/library/cpp/yt/global/mock_modules/module3_public/ya.make
@@ -0,0 +1,9 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/library/cpp/yt/ya_cpp.make.inc)
+
+PEERDIR(
+ library/cpp/yt/global
+)
+
+END()
diff --git a/library/cpp/yt/global/unittests/global_variable/just_works_ut/main.cpp b/library/cpp/yt/global/unittests/global_variable/just_works_ut/main.cpp
new file mode 100644
index 0000000000..8618944404
--- /dev/null
+++ b/library/cpp/yt/global/unittests/global_variable/just_works_ut/main.cpp
@@ -0,0 +1,73 @@
+#include <library/cpp/testing/gtest/gtest.h>
+
+#include <library/cpp/yt/global/access.h>
+
+#include <library/cpp/yt/global/mock_modules/module1_defs/direct_access.h>
+#include <library/cpp/yt/global/mock_modules/module1_public/test_tag.h>
+
+#include <optional>
+#include <thread>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TGlobalVariableTest, JustWorks)
+{
+ auto erasedVar = NGlobal::GetErasedVariable(TestTag1);
+ EXPECT_TRUE(erasedVar.has_value());
+
+ auto concreteVar = erasedVar->AsConcrete<int>();
+ EXPECT_EQ(concreteVar, GetTestVariable1());
+
+ SetTestVariable1(12344);
+
+ // NB: We copied variable, not a reference to it!
+ EXPECT_EQ(concreteVar, erasedVar->AsConcrete<int>());
+
+ EXPECT_EQ(NGlobal::GetErasedVariable(TestTag1)->AsConcrete<int>(), 12344);
+}
+
+TEST(TGlobalVariableTest, MissingTag)
+{
+ static constexpr NGlobal::TVariableTag MissingTag = {};
+ EXPECT_FALSE(NGlobal::GetErasedVariable(MissingTag));
+}
+
+TEST(TGlobalVariableTest, ThreadLocal)
+{
+ auto ensureConstructed = [] {
+ // NB: tls variable is constructed only after
+ // being referred to for the first time.
+ auto val = GetTlsVariable();
+ ++val;
+ Y_UNUSED(val);
+ };
+
+ auto checkTls = [&ensureConstructed] (int val) {
+ ensureConstructed();
+
+ auto erasedVar = NGlobal::GetErasedVariable(ThreadLocalTag);
+ EXPECT_TRUE(erasedVar);
+ EXPECT_EQ(erasedVar->AsConcrete<int>(), 0);
+
+ EXPECT_EQ(GetTlsVariable(), 0);
+ SetTlsVariable(val);
+
+ EXPECT_EQ(erasedVar->AsConcrete<int>(), 0);
+ EXPECT_EQ(NGlobal::GetErasedVariable(ThreadLocalTag)->AsConcrete<int>(), val);
+ };
+
+ checkTls(42);
+
+ for (int idx = 0; idx < 42; ++idx) {
+ auto thread = std::thread(std::bind(checkTls, idx << 2));
+ thread.join();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/library/cpp/yt/global/unittests/global_variable/just_works_ut/ya.make b/library/cpp/yt/global/unittests/global_variable/just_works_ut/ya.make
new file mode 100644
index 0000000000..570ab888dc
--- /dev/null
+++ b/library/cpp/yt/global/unittests/global_variable/just_works_ut/ya.make
@@ -0,0 +1,16 @@
+GTEST(unittester-library-global-just-works)
+
+INCLUDE(${ARCADIA_ROOT}/library/cpp/yt/ya_cpp.make.inc)
+
+SRCS(
+ main.cpp
+)
+
+PEERDIR(
+ library/cpp/yt/global
+
+ library/cpp/yt/global/mock_modules/module1_public
+ library/cpp/yt/global/mock_modules/module1_defs
+)
+
+END()
diff --git a/library/cpp/yt/global/unittests/global_variable/missing_module_ut/main.cpp b/library/cpp/yt/global/unittests/global_variable/missing_module_ut/main.cpp
new file mode 100644
index 0000000000..355bb64d78
--- /dev/null
+++ b/library/cpp/yt/global/unittests/global_variable/missing_module_ut/main.cpp
@@ -0,0 +1,23 @@
+#include <library/cpp/testing/gtest/gtest.h>
+
+#include <library/cpp/yt/global/access.h>
+
+#include <library/cpp/yt/global/mock_modules/module1_public/test_tag.h>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// NB: Module containing global variable definition is not
+// included in peerdirs. We expect it to be invisible from
+// here.
+TEST(TGlobalVariableTest, MissingModule)
+{
+ EXPECT_FALSE(NGlobal::GetErasedVariable(TestTag1));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/library/cpp/yt/global/unittests/global_variable/missing_module_ut/ya.make b/library/cpp/yt/global/unittests/global_variable/missing_module_ut/ya.make
new file mode 100644
index 0000000000..f99a596455
--- /dev/null
+++ b/library/cpp/yt/global/unittests/global_variable/missing_module_ut/ya.make
@@ -0,0 +1,13 @@
+GTEST(unittester-library-global-missing-module)
+
+INCLUDE(${ARCADIA_ROOT}/library/cpp/yt/ya_cpp.make.inc)
+
+SRCS(
+ main.cpp
+)
+
+PEERDIR(
+ library/cpp/yt/global/mock_modules/module1_public
+)
+
+END()
diff --git a/library/cpp/yt/global/unittests/global_variable/odr_violation_avoidance_ut/main.cpp b/library/cpp/yt/global/unittests/global_variable/odr_violation_avoidance_ut/main.cpp
new file mode 100644
index 0000000000..d872763bf9
--- /dev/null
+++ b/library/cpp/yt/global/unittests/global_variable/odr_violation_avoidance_ut/main.cpp
@@ -0,0 +1,41 @@
+#include <library/cpp/testing/gtest/gtest.h>
+
+#include <library/cpp/yt/global/access.h>
+
+#include <library/cpp/yt/global/mock_modules/module3_defs/direct_access.h>
+#include <library/cpp/yt/global/mock_modules/module3_public/test_tag.h>
+
+#include <array>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T, size_t N>
+bool Equal(const std::array<T, N>& left, const std::array<T, N>& right)
+{
+ return [&] <size_t... Idx> (std::index_sequence<Idx...>) {
+ return ([&] {
+ return left[Idx] == right[Idx];
+ } () && ...);
+ } (std::make_index_sequence<N>());
+}
+
+TEST(TGlobalVariableTest, NoOdrViolation)
+{
+ auto erasedVar = NGlobal::GetErasedVariable(TestTag3);
+ EXPECT_TRUE(erasedVar);
+
+ EXPECT_TRUE(Equal(erasedVar->AsConcrete<std::array<int, 2>>(), std::array<int, 2>{11, 22}));
+
+ SetTestVariable3(std::array<int, 2>{44, 55});
+ EXPECT_TRUE(Equal(erasedVar->AsConcrete<std::array<int, 2>>(), std::array<int, 2>{11, 22}));
+
+ EXPECT_TRUE(Equal(NGlobal::GetErasedVariable(TestTag3)->AsConcrete<std::array<int, 2>>(), std::array<int, 2>{44, 55}));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/library/cpp/yt/global/unittests/global_variable/odr_violation_avoidance_ut/ya.make b/library/cpp/yt/global/unittests/global_variable/odr_violation_avoidance_ut/ya.make
new file mode 100644
index 0000000000..9b409eb0d1
--- /dev/null
+++ b/library/cpp/yt/global/unittests/global_variable/odr_violation_avoidance_ut/ya.make
@@ -0,0 +1,16 @@
+GTEST(unittester-library-global-ord-violation-avoidence)
+
+INCLUDE(${ARCADIA_ROOT}/library/cpp/yt/ya_cpp.make.inc)
+
+SRCS(
+ main.cpp
+)
+
+PEERDIR(
+ library/cpp/yt/global
+
+ library/cpp/yt/global/mock_modules/module3_public
+ library/cpp/yt/global/mock_modules/module3_defs
+)
+
+END()
diff --git a/library/cpp/yt/global/unittests/global_variable/two_modules_ut/main.cpp b/library/cpp/yt/global/unittests/global_variable/two_modules_ut/main.cpp
new file mode 100644
index 0000000000..1a31bcc81d
--- /dev/null
+++ b/library/cpp/yt/global/unittests/global_variable/two_modules_ut/main.cpp
@@ -0,0 +1,90 @@
+#include <library/cpp/testing/gtest/gtest.h>
+
+#include <library/cpp/yt/global/access.h>
+
+#include <library/cpp/yt/global/mock_modules/module1_defs/direct_access.h>
+#include <library/cpp/yt/global/mock_modules/module1_public/test_tag.h>
+
+#include <library/cpp/yt/global/mock_modules/module2_defs/direct_access.h>
+#include <library/cpp/yt/global/mock_modules/module2_public/test_tag.h>
+
+#include <optional>
+#include <thread>
+
+namespace NYT {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// NB: Test is identical to just_works_ut.
+// We check that linking non-conflicting module has no interference.
+TEST(TGlobalVariableTest, JustWorks)
+{
+ auto erasedVar = NGlobal::GetErasedVariable(TestTag1);
+ EXPECT_TRUE(erasedVar.has_value());
+
+ auto concreteVar = erasedVar->AsConcrete<int>();
+ EXPECT_EQ(concreteVar, GetTestVariable1());
+
+ SetTestVariable1(12344);
+
+ // NB: We copied variable, not a reference to it!
+ EXPECT_EQ(concreteVar, erasedVar->AsConcrete<int>());
+
+ EXPECT_EQ(NGlobal::GetErasedVariable(TestTag1)->AsConcrete<int>(), 12344);
+}
+
+TEST(TGlobalVariableTest, MissingTag)
+{
+ static constexpr NGlobal::TVariableTag MissingTag = {};
+ EXPECT_FALSE(NGlobal::GetErasedVariable(MissingTag));
+}
+
+TEST(TGlobalVariableTest, ThreadLocal)
+{
+ auto ensureConstructed = [] {
+ // NB: tls variable is constructed only after
+ // being referred to for the first time.
+ auto val = GetTlsVariable();
+ ++val;
+ Y_UNUSED(val);
+ };
+
+ auto checkTls = [&ensureConstructed] (int val) {
+ ensureConstructed();
+
+ auto erasedVar = NGlobal::GetErasedVariable(ThreadLocalTag);
+ EXPECT_TRUE(erasedVar);
+ EXPECT_EQ(erasedVar->AsConcrete<int>(), 0);
+
+ EXPECT_EQ(GetTlsVariable(), 0);
+ SetTlsVariable(val);
+
+ EXPECT_EQ(erasedVar->AsConcrete<int>(), 0);
+ EXPECT_EQ(NGlobal::GetErasedVariable(ThreadLocalTag)->AsConcrete<int>(), val);
+ };
+
+ checkTls(42);
+
+ for (int idx = 0; idx < 42; ++idx) {
+ auto thread = std::thread(std::bind(checkTls, idx << 2));
+ thread.join();
+ }
+}
+
+TEST(TGlobalVariableTest, JustWorksAnotherModule)
+{
+ auto erasedVar = NGlobal::GetErasedVariable(TestTag2);
+ EXPECT_TRUE(erasedVar.has_value());
+
+ EXPECT_EQ(erasedVar->AsConcrete<TGuid>(), GetTestVariable2());
+
+ auto val = TGuid{1, 2, 3, 4};
+ SetTestVariable2(val);
+ EXPECT_EQ(NGlobal::GetErasedVariable(TestTag2)->AsConcrete<TGuid>(), val);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT
diff --git a/library/cpp/yt/global/unittests/global_variable/two_modules_ut/ya.make b/library/cpp/yt/global/unittests/global_variable/two_modules_ut/ya.make
new file mode 100644
index 0000000000..ae7757ab4d
--- /dev/null
+++ b/library/cpp/yt/global/unittests/global_variable/two_modules_ut/ya.make
@@ -0,0 +1,20 @@
+GTEST(unittester-library-global-two_modules)
+
+INCLUDE(${ARCADIA_ROOT}/library/cpp/yt/ya_cpp.make.inc)
+
+SRCS(
+ main.cpp
+)
+
+PEERDIR(
+ library/cpp/yt/global
+ library/cpp/yt/misc
+
+ library/cpp/yt/global/mock_modules/module1_public
+ library/cpp/yt/global/mock_modules/module1_defs
+
+ library/cpp/yt/global/mock_modules/module2_public
+ library/cpp/yt/global/mock_modules/module2_defs
+)
+
+END()
diff --git a/library/cpp/yt/global/variable-inl.h b/library/cpp/yt/global/variable-inl.h
new file mode 100644
index 0000000000..32d22fd7c8
--- /dev/null
+++ b/library/cpp/yt/global/variable-inl.h
@@ -0,0 +1,95 @@
+#ifndef GLOBAL_VARIABLE_INL_H_
+#error "Direct inclusion of this file is not allowed, include variable.h"
+// For the sake of sane code completion.
+#include "variable.h"
+#endif
+
+namespace NYT::NGlobal {
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <CTriviallyErasable<GlobalVariableMaxByteSize> T>
+TVariable<T>::TVariable(
+ const TVariableTag& tag,
+ TAccessor accessor,
+ T initValue) noexcept
+ : Value_(initValue)
+{
+ NDetail::RegisterVariable(tag, accessor);
+}
+
+template <CTriviallyErasable<GlobalVariableMaxByteSize> T>
+T TVariable<T>::Get() const noexcept
+{
+ return Value_;
+}
+
+template <CTriviallyErasable<GlobalVariableMaxByteSize> T>
+void TVariable<T>::Set(T value) noexcept
+{
+ Value_ = value;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+#undef YT_DEFINE_TRACKED_GLOBAL
+#undef YT_DEFINE_TRACKED_THREAD_LOCAL
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define YT_DEFINE_TRACKED_GLOBAL(Type, Name, Tag, InitExpr) \
+ namespace NGlobalTracking##Name##Tag { \
+ \
+ ::NYT::NGlobal::TErasedStorage GetErased##Name() noexcept; \
+ \
+ static ::NYT::NGlobal::TVariable<Type> Name{Tag, GetErased##Name, (InitExpr)}; \
+ \
+ ::NYT::NGlobal::TErasedStorage GetErased##Name() noexcept \
+ { \
+ return ::NYT::NGlobal::TErasedStorage{Name.Get()}; \
+ } \
+ \
+ } /*namespace NGlobalTracking##Name##Tag*/ \
+ using NGlobalTracking##Name##Tag::Name; \
+ static_assert(true)
+
+// NB(arkady-e1ppa): We must ensure that tracker is constructed thus
+// we have to call ref tracker inside tls accessor.
+// NB(arkady-e1ppa): Unlike normal static variable, we cannot just pull
+// varibale name out as we might want to forward-declare thread local variable
+// now that it is modelled as function. Pulling alias from ns unfortunately
+// doesn't work as function definition :(.
+#define YT_DEFINE_TRACKED_THREAD_LOCAL(Type, Name, Tag, ...) \
+ Y_NO_INLINE Type& Name(); \
+ namespace NGlobalTracking##Name##Tag { \
+ \
+ void EnsureTracked() noexcept; \
+ ::NYT::NGlobal::TErasedStorage GetErased##Name() noexcept; \
+ \
+ static ::NYT::NGlobal::TVariable<std::byte> TlsTrackerFor##Name{Tag, GetErased##Name}; \
+ \
+ void EnsureTracked() noexcept \
+ { \
+ auto val = TlsTrackerFor##Name.Get(); \
+ Y_UNUSED(val); \
+ } \
+ \
+ ::NYT::NGlobal::TErasedStorage GetErased##Name() noexcept \
+ { \
+ return ::NYT::NGlobal::TErasedStorage{Name()}; \
+ } \
+ \
+ } /*namespace NGlobalTracking##Name##Tag*/ \
+ Y_NO_INLINE Type& Name() \
+ { \
+ thread_local Type tlsData { __VA_ARGS__ }; \
+ asm volatile(""); \
+ NGlobalTracking##Name##Tag::EnsureTracked(); \
+ return tlsData; \
+ } \
+ \
+ static_assert(true)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NGlobal
diff --git a/library/cpp/yt/global/variable.h b/library/cpp/yt/global/variable.h
new file mode 100644
index 0000000000..8b1a6c92a9
--- /dev/null
+++ b/library/cpp/yt/global/variable.h
@@ -0,0 +1,53 @@
+#pragma once
+
+#include "access.h"
+
+namespace NYT::NGlobal {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace NDetail {
+
+// Defined in impl.cpp.
+void RegisterVariable(const TVariableTag& tag, TAccessor accessor);
+
+} // namespace NDetail
+
+////////////////////////////////////////////////////////////////////////////////
+
+template <CTriviallyErasable<GlobalVariableMaxByteSize> T>
+class TVariable
+{
+public:
+ TVariable(
+ const TVariableTag& tag,
+ TAccessor accessor,
+ T initValue = {}) noexcept;
+
+ TVariable(const TVariable& other) = delete;
+ TVariable& operator=(const TVariable& other) = delete;
+
+ T Get() const noexcept;
+ void Set(T value) noexcept;
+
+private:
+ // NB(arkady-e1ppa): Ban TVariable<TVariable<T>>.
+ static_assert(!requires (T t) {
+ [] <class U> (TVariable<U>&) { } (t);
+ });
+
+ T Value_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+#define YT_DEFINE_TRACKED_GLOBAL(Type, Name, Tag, InitExpr)
+#define YT_DEFINE_TRACKED_THREAD_LOCAL(Type, Name, Tag, ...)
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NGlobal
+
+#define GLOBAL_VARIABLE_INL_H_
+#include "variable-inl.h"
+#undef GLOBAL_VARIABLE_INL_H_
diff --git a/library/cpp/yt/global/ya.make b/library/cpp/yt/global/ya.make
new file mode 100644
index 0000000000..a423fb1539
--- /dev/null
+++ b/library/cpp/yt/global/ya.make
@@ -0,0 +1,23 @@
+LIBRARY()
+
+INCLUDE(${ARCADIA_ROOT}/library/cpp/yt/ya_cpp.make.inc)
+
+SRCS(
+ impl.cpp
+)
+
+PEERDIR(
+ library/cpp/yt/assert
+ library/cpp/yt/misc
+ library/cpp/yt/memory
+ library/cpp/yt/threading
+)
+
+END()
+
+RECURSE_FOR_TESTS(
+ unittests/global_variable/just_works_ut
+ unittests/global_variable/missing_module_ut
+ unittests/global_variable/odr_violation_avoidance_ut
+ unittests/global_variable/two_modules_ut
+)
diff --git a/library/cpp/yt/memory/erased_storage-inl.h b/library/cpp/yt/memory/erased_storage-inl.h
index e4dc148e11..8b6a303dff 100644
--- a/library/cpp/yt/memory/erased_storage-inl.h
+++ b/library/cpp/yt/memory/erased_storage-inl.h
@@ -4,34 +4,45 @@
#include "erased_storage.h"
#endif
+#include <algorithm>
+
namespace NYT {
////////////////////////////////////////////////////////////////////////////////
-template <CTriviallyErasable TDecayedConcrete>
-TErasedStorage::TErasedStorage(TDecayedConcrete concrete) noexcept
+template <size_t MaxByteSize>
+template <CTriviallyErasable<MaxByteSize> TDecayedConcrete>
+TErasedStorage<MaxByteSize>::TErasedStorage(TDecayedConcrete concrete) noexcept
{
+ // NB(arkady-e1ppa): We want to be able to compare
+ // erased objects as if they are not erased.
+ // Assuming erased type's operator ==
+ // is equivalent to bitwise comparison.
+ std::ranges::fill(Bytes_, std::byte(0));
std::construct_at(
&AsConcrete<TDecayedConcrete>(),
concrete);
}
-template <CTriviallyErasable TDecayedConcrete>
-TDecayedConcrete& TErasedStorage::AsConcrete() & noexcept
+template <size_t MaxByteSize>
+template <CTriviallyErasable<MaxByteSize> TDecayedConcrete>
+TDecayedConcrete& TErasedStorage<MaxByteSize>::AsConcrete() & noexcept
{
using TPtr = TDecayedConcrete*;
return *std::launder(reinterpret_cast<TPtr>(&Bytes_));
}
-template <CTriviallyErasable TDecayedConcrete>
-const TDecayedConcrete& TErasedStorage::AsConcrete() const & noexcept
+template <size_t MaxByteSize>
+template <CTriviallyErasable<MaxByteSize> TDecayedConcrete>
+const TDecayedConcrete& TErasedStorage<MaxByteSize>::AsConcrete() const & noexcept
{
using TPtr = const TDecayedConcrete*;
return *std::launder(reinterpret_cast<TPtr>(&Bytes_));
}
-template <CTriviallyErasable TDecayedConcrete>
-TDecayedConcrete&& TErasedStorage::AsConcrete() && noexcept
+template <size_t MaxByteSize>
+template <CTriviallyErasable<MaxByteSize> TDecayedConcrete>
+TDecayedConcrete&& TErasedStorage<MaxByteSize>::AsConcrete() && noexcept
{
using TPtr = TDecayedConcrete*;
return std::move(*std::launder(reinterpret_cast<TPtr>(&Bytes_)));
diff --git a/library/cpp/yt/memory/erased_storage.h b/library/cpp/yt/memory/erased_storage.h
index 8e85430d0b..46ea5cc854 100644
--- a/library/cpp/yt/memory/erased_storage.h
+++ b/library/cpp/yt/memory/erased_storage.h
@@ -7,15 +7,28 @@ namespace NYT {
////////////////////////////////////////////////////////////////////////////////
-constexpr size_t ErasedStorageMaxByteSize = 32;
+template <size_t MaxByteSize>
+class TErasedStorage;
////////////////////////////////////////////////////////////////////////////////
-class TErasedStorage;
+namespace NDetail {
+
+template <class T>
+struct TIsErasedStorage
+ : public std::false_type
+{ };
+
+template <size_t N>
+struct TIsErasedStorage<TErasedStorage<N>>
+ : public std::true_type
+{ };
+
+} // namespace NDetail
////////////////////////////////////////////////////////////////////////////////
-template <class T>
+template <class T, size_t ErasedStorageMaxByteSize>
concept CTriviallyErasable =
std::default_initializable<T> &&
std::is_trivially_destructible_v<T> &&
@@ -23,33 +36,38 @@ concept CTriviallyErasable =
(sizeof(T) <= ErasedStorageMaxByteSize) &&
(alignof(T) <= ErasedStorageMaxByteSize) &&
!std::is_reference_v<T> &&
- !std::same_as<T, TErasedStorage>;
+ !NDetail::TIsErasedStorage<T>::value;
////////////////////////////////////////////////////////////////////////////////
// This class does not call dtor of erased object
// thus we require trivial destructability.
+template <size_t MaxByteSize>
class TErasedStorage
{
public:
- template <CTriviallyErasable TDecayedConcrete>
+ static constexpr size_t ByteSize = MaxByteSize;
+
+ template <CTriviallyErasable<MaxByteSize> TDecayedConcrete>
explicit TErasedStorage(TDecayedConcrete concrete) noexcept;
TErasedStorage(const TErasedStorage& other) = default;
TErasedStorage& operator=(const TErasedStorage& other) = default;
- template <CTriviallyErasable TDecayedConcrete>
+ template <CTriviallyErasable<MaxByteSize> TDecayedConcrete>
TDecayedConcrete& AsConcrete() & noexcept;
- template <CTriviallyErasable TDecayedConcrete>
+ template <CTriviallyErasable<MaxByteSize> TDecayedConcrete>
const TDecayedConcrete& AsConcrete() const & noexcept;
- template <CTriviallyErasable TDecayedConcrete>
+ template <CTriviallyErasable<MaxByteSize> TDecayedConcrete>
TDecayedConcrete&& AsConcrete() && noexcept;
+ bool operator==(const TErasedStorage& other) const = default;
+
private:
// NB(arkady-e1ppa): aligned_storage is deprecated.
- alignas(ErasedStorageMaxByteSize) std::byte Bytes_[ErasedStorageMaxByteSize];
+ alignas(MaxByteSize) std::byte Bytes_[MaxByteSize];
};
////////////////////////////////////////////////////////////////////////////////
diff --git a/library/cpp/yt/memory/unittests/erased_storage_ut.cpp b/library/cpp/yt/memory/unittests/erased_storage_ut.cpp
index 194c8e7fce..5545423bcf 100644
--- a/library/cpp/yt/memory/unittests/erased_storage_ut.cpp
+++ b/library/cpp/yt/memory/unittests/erased_storage_ut.cpp
@@ -46,6 +46,13 @@ private:
[[maybe_unused]] std::array<std::byte, 8> Data_;
};
+// Overshadow to bind template parameter.
+inline constexpr size_t TestSize = 32;
+
+template <class T>
+concept CTriviallyErasable = ::NYT::CTriviallyErasable<T, TestSize>;
+using TErasedStorage = ::NYT::TErasedStorage<TestSize>;
+
////////////////////////////////////////////////////////////////////////////////
TEST(TErasedStorageTest, Types)
@@ -124,6 +131,21 @@ TEST(TErasedStorageTest, MutateStorage)
EXPECT_EQ(stor.AsConcrete<int>(), 88);
}
+TEST(TErasedStorageTest, EqualityComparison)
+{
+ struct TWidget
+ {
+ alignas(8) int Value;
+
+ alignas(16) bool Flag;
+ } widget{1, false};
+
+ TErasedStorage stor1(widget);
+ TErasedStorage stor2(widget);
+
+ EXPECT_EQ(stor1, stor2);
+}
+
////////////////////////////////////////////////////////////////////////////////
} // namespace
diff --git a/library/cpp/yt/threading/public.h b/library/cpp/yt/threading/public.h
index ab6cb22663..5b02930c63 100644
--- a/library/cpp/yt/threading/public.h
+++ b/library/cpp/yt/threading/public.h
@@ -11,4 +11,9 @@ namespace NYT::NThreading {
////////////////////////////////////////////////////////////////////////////////
+using TThreadId = size_t;
+constexpr size_t InvalidThreadId = 0;
+
+////////////////////////////////////////////////////////////////////////////////
+
} // namespace NYT::NThreading