diff --git a/.github/import_generation.txt b/.github/import_generation.txt index d81cc0710eb..920a1396648 100644 --- a/.github/import_generation.txt +++ b/.github/import_generation.txt @@ -1 +1 @@ -42 +43 diff --git a/.github/last_commit.txt b/.github/last_commit.txt index 50efb6d06a0..7fd1813ea92 100644 --- a/.github/last_commit.txt +++ b/.github/last_commit.txt @@ -1 +1 @@ -03a9e93a82181a6a9dc51ad88263b3ecedbb2413 +b3b57fcb5915100b21bf9a507bf0a8c0627b7fee diff --git a/.github/scripts/copy_sources.sh b/.github/scripts/copy_sources.sh index 77053a3d979..9813b2434d2 100755 --- a/.github/scripts/copy_sources.sh +++ b/.github/scripts/copy_sources.sh @@ -54,9 +54,13 @@ cp $2/CMakePresets.json $tmp_dir cp $2/CMakeLists.txt $tmp_dir cp $2/LICENSE $tmp_dir cp $2/README.md $tmp_dir -cp $2/tests/slo_workloads/.dockerignore $tmp_dir/tests/slo_workloads -cp $2/tests/slo_workloads/Dockerfile $tmp_dir/tests/slo_workloads -cp $2/tests/slo_workloads/Dockerfile.userver $tmp_dir/tests/slo_workloads +for oss_test_dir in slo_workloads deb_package; do + if [ -d "$2/tests/$oss_test_dir" ]; then + rm -rf "$tmp_dir/tests/$oss_test_dir" + mkdir -p "$tmp_dir/tests" + cp -a "$2/tests/$oss_test_dir" "$tmp_dir/tests/" + fi +done cp $2/include/ydb-cpp-sdk/type_switcher.h $tmp_dir/include/ydb-cpp-sdk/type_switcher.h cp $2/src/version.h $tmp_dir/src/version.h diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f9ec1cc517..80e8d11280b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ +## v3.19.0 + +* Added metric buffer for batched metric writes to reduce OpenTelemetry hot-path overhead. + * Added a helper to detect specific issue codes within a TStatus (including nested sub-issues) and introduces the CONSTRAINT_VIOLATION issue code constant, intended to make it easier for SDK users to detect primary key / unique index conflicts. +* Added `PartitionWriteSpeedMessagesPerSecond` and `PartitionWriteBurstMessages` to topic create/alter settings and corresponding getters in `TTopicDescription`. + ## v3.18.0 * Fixed self thread join core dump in IAM credentials provider diff --git a/CMakeFiles/CMakeSystem.cmake b/CMakeFiles/CMakeSystem.cmake deleted file mode 100644 index 12893705dc6..00000000000 --- a/CMakeFiles/CMakeSystem.cmake +++ /dev/null @@ -1,15 +0,0 @@ -set(CMAKE_HOST_SYSTEM "Linux-5.4.210-39.1.pagevecsize") -set(CMAKE_HOST_SYSTEM_NAME "Linux") -set(CMAKE_HOST_SYSTEM_VERSION "5.4.210-39.1.pagevecsize") -set(CMAKE_HOST_SYSTEM_PROCESSOR "x86_64") - - - -set(CMAKE_SYSTEM "Linux-5.4.210-39.1.pagevecsize") -set(CMAKE_SYSTEM_NAME "Linux") -set(CMAKE_SYSTEM_VERSION "5.4.210-39.1.pagevecsize") -set(CMAKE_SYSTEM_PROCESSOR "x86_64") - -set(CMAKE_CROSSCOMPILING "FALSE") - -set(CMAKE_SYSTEM_LOADED 1) diff --git a/artifacts/libydb-cpp-dev_3.18.0_amd64.deb b/artifacts/libydb-cpp-dev_3.18.0_amd64.deb new file mode 100644 index 00000000000..c1f73aad8a3 Binary files /dev/null and b/artifacts/libydb-cpp-dev_3.18.0_amd64.deb differ diff --git a/artifacts/libydb-cpp-iam-dev_3.18.0_amd64.deb b/artifacts/libydb-cpp-iam-dev_3.18.0_amd64.deb new file mode 100644 index 00000000000..0a5e141f5b1 Binary files /dev/null and b/artifacts/libydb-cpp-iam-dev_3.18.0_amd64.deb differ diff --git a/artifacts/libydb-cpp-otel-metrics-dev_3.18.0_amd64.deb b/artifacts/libydb-cpp-otel-metrics-dev_3.18.0_amd64.deb new file mode 100644 index 00000000000..69f190587a7 Binary files /dev/null and b/artifacts/libydb-cpp-otel-metrics-dev_3.18.0_amd64.deb differ diff --git a/artifacts/libydb-cpp-otel-tracing-dev_3.18.0_amd64.deb b/artifacts/libydb-cpp-otel-tracing-dev_3.18.0_amd64.deb new file mode 100644 index 00000000000..55f896edf0e Binary files /dev/null and b/artifacts/libydb-cpp-otel-tracing-dev_3.18.0_amd64.deb differ diff --git a/artifacts/yandex-googleapis-api-common-protos-1.0.0-Linux.deb b/artifacts/yandex-googleapis-api-common-protos-1.0.0-Linux.deb new file mode 100644 index 00000000000..1fe295a15d0 Binary files /dev/null and b/artifacts/yandex-googleapis-api-common-protos-1.0.0-Linux.deb differ diff --git a/examples/metric_buffer_benchmark/main.cpp b/examples/metric_buffer_benchmark/main.cpp new file mode 100644 index 00000000000..5ea923ce16e --- /dev/null +++ b/examples/metric_buffer_benchmark/main.cpp @@ -0,0 +1,285 @@ +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace NYdb; +using namespace NYdb::NMetrics; +using namespace NYdb::NObservability; + +namespace { + +class TBenchCounter : public ICounter { +public: + void Inc() override { + Value_.fetch_add(1, std::memory_order_relaxed); + IncCalls_.fetch_add(1, std::memory_order_relaxed); + } + void Add(std::uint64_t delta) override { + if (delta == 0) return; + Value_.fetch_add(delta, std::memory_order_relaxed); + AddCalls_.fetch_add(1, std::memory_order_relaxed); + } + std::uint64_t Get() const { return Value_.load(); } + std::uint64_t IncCalls() const { return IncCalls_.load(); } + std::uint64_t AddCalls() const { return AddCalls_.load(); } + +private: + std::atomic Value_{0}; + std::atomic IncCalls_{0}; + std::atomic AddCalls_{0}; +}; + +class TBenchHistogram : public IHistogram { +public: + void Record(double value) override { + (void)value; + Count_.fetch_add(1, std::memory_order_relaxed); + RecordCalls_.fetch_add(1, std::memory_order_relaxed); + } + void RecordMany(const std::vector& values) override { + if (values.empty()) return; + Count_.fetch_add(values.size(), std::memory_order_relaxed); + RecordManyCalls_.fetch_add(1, std::memory_order_relaxed); + } + std::uint64_t Count() const { + return Count_.load(std::memory_order_relaxed); + } + std::uint64_t RecordCalls() const { + return RecordCalls_.load(std::memory_order_relaxed); + } + std::uint64_t RecordManyCalls() const { + return RecordManyCalls_.load(std::memory_order_relaxed); + } + +private: + std::atomic Count_{0}; + std::atomic RecordCalls_{0}; + std::atomic RecordManyCalls_{0}; +}; + +class TBenchGauge : public IGauge { +public: + void Add(double delta) override { Value_ += delta; } + void Set(double value) override { Value_ = value; } + double Get() const { return Value_; } +private: + double Value_ = 0.0; +}; + +class TBenchRegistry : public IMetricRegistry { +public: + std::shared_ptr Counter(const std::string& name, + const TLabels& labels, + const std::string&, + const std::string&) override { + std::lock_guard lock(Mu_); + auto& slot = Counters_[Key(name, labels)]; + if (!slot) slot = std::make_shared(); + return slot; + } + std::shared_ptr Histogram(const std::string& name, + const std::vector&, + const TLabels& labels, + const std::string&, + const std::string&) override { + std::lock_guard lock(Mu_); + auto& slot = Histograms_[Key(name, labels)]; + if (!slot) slot = std::make_shared(); + return slot; + } + std::shared_ptr Gauge(const std::string& name, + const TLabels& labels, + const std::string&, + const std::string&) override { + std::lock_guard lock(Mu_); + auto& slot = Gauges_[Key(name, labels)]; + if (!slot) slot = std::make_shared(); + return slot; + } + + std::shared_ptr GetCounter(const std::string& name, + const TLabels& labels = {}) const { + std::lock_guard lock(Mu_); + auto it = Counters_.find(Key(name, labels)); + return it == Counters_.end() ? nullptr : it->second; + } + std::shared_ptr GetHistogram(const std::string& name, + const TLabels& labels = {}) const { + std::lock_guard lock(Mu_); + auto it = Histograms_.find(Key(name, labels)); + return it == Histograms_.end() ? nullptr : it->second; + } + +private: + static std::string Key(const std::string& name, const TLabels& labels) { + std::string k = name; + for (const auto& [a, b] : labels) { + k.push_back('|'); + k.append(a); + k.push_back('='); + k.append(b); + } + return k; + } + mutable std::mutex Mu_; + std::map> Counters_; + std::map> Histograms_; + std::map> Gauges_; +}; + +struct TResult { + std::string Mode; + std::uint64_t TotalOps = 0; + std::uint64_t IncCalls = 0; + std::uint64_t AddCalls = 0; + std::uint64_t RecordCalls = 0; + std::uint64_t RecordManyCalls = 0; + double DurationMs = 0.0; +}; + +TResult RunWorkload(const std::string& mode, + int threads, + std::uint64_t opsPerThread, + std::shared_ptr registry, + std::shared_ptr sink) { + auto counter = registry->Counter("bench.counter", {}, "", ""); + auto hist = registry->Histogram("bench.histogram", + {0.001, 0.01, 0.1, 1.0, 10.0}, + {}, "", ""); + + std::atomic go{false}; + + std::vector workers; + workers.reserve(threads); + for (int t = 0; t < threads; ++t) { + workers.emplace_back([&, t] { + while (!go.load(std::memory_order_acquire)) { + std::this_thread::yield(); + } + for (std::uint64_t i = 0; i < opsPerThread; ++i) { + counter->Inc(); + hist->Record(static_cast(i % 1000) * 0.001 + + static_cast(t) * 0.0001); + } + }); + } + + const auto t0 = std::chrono::steady_clock::now(); + go.store(true, std::memory_order_release); + + for (auto& w : workers) { + w.join(); + } + + counter.reset(); + hist.reset(); + registry.reset(); + + const auto duration = std::chrono::duration( + std::chrono::steady_clock::now() - t0).count(); + + auto sinkCounter = sink->GetCounter("bench.counter", {}); + auto sinkHist = sink->GetHistogram("bench.histogram", {}); + + TResult r; + r.Mode = mode; + r.TotalOps = static_cast(threads) * opsPerThread; + r.IncCalls = sinkCounter ? sinkCounter->IncCalls() : 0; + r.AddCalls = sinkCounter ? sinkCounter->AddCalls() : 0; + r.RecordCalls = sinkHist ? sinkHist->RecordCalls() : 0; + r.RecordManyCalls = sinkHist ? sinkHist->RecordManyCalls() : 0; + r.DurationMs = duration; + return r; +} + +void PrintRow(const TResult& r) { + const double thr = r.DurationMs > 0 + ? (static_cast(r.TotalOps) * 2.0 * 1000.0 / r.DurationMs) + : 0.0; + + const std::uint64_t underlying = r.IncCalls + r.AddCalls + + r.RecordCalls + r.RecordManyCalls; + const double coalesce = underlying > 0 + ? (static_cast(r.TotalOps) * 2.0 / static_cast(underlying)) + : 0.0; + + std::cout + << std::left << std::setw(10) << r.Mode + << " total_ops=" << std::setw(10) << (r.TotalOps * 2) + << " duration_ms=" << std::fixed << std::setprecision(1) << std::setw(8) << r.DurationMs + << " throughput=" << std::setprecision(0) << std::setw(11) << thr << " ops/s" + << "\n " + << " counter[Inc=" << r.IncCalls << ", Add=" << r.AddCalls << "]" + << " histogram[Record=" << r.RecordCalls << ", RecordMany=" << r.RecordManyCalls << "]" + << " coalesce=" << std::setprecision(2) << coalesce << "x" + << std::endl; +} + +} // namespace + +int main(int argc, char** argv) { + int threads = 8; + std::uint64_t ops = 200'000; + int flushMs = 100; + bool runDirect = true; + bool runBuffered = true; + + NLastGetopt::TOpts opts; + opts.AddLongOption("threads", "Number of concurrent worker threads") + .DefaultValue(std::to_string(threads)).StoreResult(&threads); + opts.AddLongOption("ops", "Counter+histogram updates per worker thread") + .DefaultValue(std::to_string(ops)).StoreResult(&ops); + opts.AddLongOption("flush-ms", + "TMetricBuffer FlushInterval (ms) for the buffered mode") + .DefaultValue(std::to_string(flushMs)).StoreResult(&flushMs); + opts.AddLongOption("no-direct", "Skip the baseline 'direct' run").NoArgument() + .Handler0([&]{ runDirect = false; }); + opts.AddLongOption("no-buffered", "Skip the 'buffered' run").NoArgument() + .Handler0([&]{ runBuffered = false; }); + NLastGetopt::TOptsParseResult(&opts, argc, argv); + + std::cout + << "TMetricBuffer micro-benchmark\n" + << " threads = " << threads << "\n" + << " ops_per_thread = " << ops << "\n" + << " flush_interval_ms = " << flushMs << "\n" + << " (each op = 1 Inc() + 1 Record())\n" + << std::endl; + + std::cout + << std::left << std::setw(10) << "mode" + << " result" + << std::endl; + std::cout << std::string(100, '-') << std::endl; + + if (runDirect) { + auto sink = std::make_shared(); + auto registry = std::static_pointer_cast(sink); + auto r = RunWorkload("direct", threads, ops, registry, sink); + PrintRow(r); + } + + if (runBuffered) { + auto sink = std::make_shared(); + TMetricBufferSettings settings; + settings.FlushInterval = std::chrono::milliseconds(flushMs); + auto registry = CreateBufferedMetricRegistry(sink, settings); + auto r = RunWorkload("buffered", threads, ops, registry, sink); + PrintRow(r); + } + + return 0; +} diff --git a/examples/otel_tracing/README.md b/examples/otel_tracing/README.md index 07687d1b45e..dd6bccb6dcf 100644 --- a/examples/otel_tracing/README.md +++ b/examples/otel_tracing/README.md @@ -88,6 +88,15 @@ cmake --build . --target otel_tracing_example -j$(nproc) | `--iterations`,`-n`| `20` | Итераций в Query- и Table-нагрузке | | `--retry-workers` | `6` | Параллельных воркеров в retry-нагрузке (`0` чтобы пропустить) | | `--retry-ops` | `30` | Операций на каждого retry-воркера | +| `--disable-tracing`| off | Не передавать `TraceProvider` в YDB SDK | +| `--disable-metrics`| off | Не передавать `MetricRegistry` в YDB SDK | +| `--trace-max-queue-size` | `4096` | `BatchSpanProcessor`: ёмкость in-process очереди спанов | +| `--trace-schedule-delay-ms` | `1000` | `BatchSpanProcessor`: интервал между экспортами, мс | +| `--trace-max-export-batch-size` | `512` | `BatchSpanProcessor`: максимум спанов в одном OTLP-вызове | +| `--metric-export-interval-ms` | `5000` | `PeriodicExportingMetricReader`: интервал экспорта метрик, мс | +| `--metric-export-timeout-ms` | `3000` | `PeriodicExportingMetricReader`: таймаут одного экспорта, мс | +| `--metric-buffer-flush-ms` | `100` | `TMetricBuffer`: интервал flush'а внутреннего буфера, мс (`0` отключает буфер) | +| `--telemetry-drain-sleep-ms` | `3000` | Пауза после `ForceFlush`, чтобы внешние системы успели принять demo-данные (`0` для бенчмарка) | #### Демонстрация реальных ретраев @@ -136,6 +145,108 @@ RunWithRetry (INTER > wait > ``` +#### Конвейер доставки телеметрии + +Демо явно демонстрирует **двойной** уровень батчинга на пути «приложение +→ backend» — это и есть пункт «доставка и пакетная обработка данных» +для телеметрии: + +``` + приложение + │ + │ span metric points + ▼ ▼ + BatchSpanProcessor PeriodicExportingMetricReader + ─────────────────── ───────────────────────────── + - in-process очередь - in-process агрегация + (max_queue_size) - export_interval_ms (push-pull) + - schedule_delay_ms - export_timeout_ms + - max_export_batch_size │ + │ OTLP/HTTP │ OTLP/HTTP + ▼ ▼ + OpenTelemetry Collector + ────────────────────── + processors: batch + (timeout: 1s, send_batch_size: 1024) + │ + ┌───────┴────────┐ + ▼ ▼ + Jaeger Prometheus → Grafana +``` + +* **Spans.** В демо используется `BatchSpanProcessor` (а не + `SimpleSpanProcessor`): спаны накапливаются в ограниченной очереди и + отправляются пачками. Параметры доступны через флаги + `--trace-max-queue-size`, `--trace-schedule-delay-ms`, + `--trace-max-export-batch-size`. + +* **Метрики.** `PeriodicExportingMetricReader` уже реализует pull-batch + модель: SDK агрегирует точки в памяти и сбрасывает их на коллектор + каждые `--metric-export-interval-ms`. + +* **Коллектор.** `processors: batch` (см. `otel-collector/config.yml`) + делает второй уровень батчинга поверх входящих OTLP-потоков перед + пересылкой в Jaeger / Prometheus. + +#### Внутренний батчинг эмиссии метрик — `NObservability::TMetricBuffer` + +Поверх двух транспортных уровней OTel (`BatchSpanProcessor` и +`PeriodicExportingMetricReader`) в SDK добавлен **третий, внутренний** +уровень пакетной обработки телеметрических данных — +`NObservability::TMetricBuffer`. Он стоит **до** OTel-плагина, на +горячем пути вызовов `IMetricRegistry::Counter()->Inc()` / +`Histogram()->Record()` из `TStatCollector`: + +``` + SDK hot-path: ┌────────────────────┐ + TRequestMetrics::End() Inc/Record ──▶ │ TMetricBuffer │ + TStatCollector ... │ (этот компонент) │ + │ │ + │ thread-local │ + │ буферы │ + │ flush раз в │ + │ MetricBuffer- │ + │ FlushInterval ms │ + └─────────┬──────────┘ + │ Add(N), + │ RecordMany([...]) + ▼ + ┌────────────────────┐ + │ IMetricRegistry │ + │ (OTel-плагин) │ + └─────────┬──────────┘ + │ + ▼ + PeriodicExportingMetricReader + │ OTLP + ▼ + OTel Collector +``` + +Зачем он нужен сверх того, что уже делает OTel: + +* OTel-плагин в C++ выполняет агрегацию **на каждом** `Inc()`/`Record()`: + hash набора атрибутов → захват мьютекса аггрегатора в OTel SDK → + обновление бакетов. При высокой нагрузке и общих счётчиках это + становится узким местом — N потоков сериализуются на одной мьютекс- + локации в OTel SDK. +* `TMetricBuffer` сдвигает эту работу на отдельный фоновой поток. + Application-потоки пишут только в свои **thread-local** агрегаты + (счётчики — `uint64`-инкременты, гистограммы — `small_vector` сэмплов); + раз в `MetricBufferFlushIntervalMs` фоновый поток обходит все + буферы и сбрасывает накопленные данные **одним** вызовом + `ICounter::Add(uint64_t)` / `IHistogram::RecordMany(values)` на + каждый (instrument, поток). +* Это классический LongAdder / striped counter, недостающий в + `opentelemetry-cpp`: OTel батчит **экспорт**, `TMetricBuffer` + батчит **эмиссию**. + +Конфигурируется одним флагом — `--metric-buffer-flush-ms` (по умолчанию +`100` ms). `0` отключает буфер и возвращает поведение «прямой записи». + +Сам буфер инструментирован собственным набором метрик +(тегируются префиксом `ydb_sdk_metric_buffer_*` — см. раздел Grafana). + ### 4. Открыть дашборды | Сервис | URL | Описание | @@ -217,7 +328,50 @@ Session-pool метрики (для **Query**-клиента — `ydb_query_sess определяется в порядке: явный `TClientSettings::PoolName` → дефолт `@`. -### 6. Остановить +Метрики `TMetricBuffer` (см. row «SDK Metric Buffer» в дашборде Grafana): +- `ydb_sdk_metric_buffer_pending_updates{instrument=counter|histogram|gauge}` — + текущее число накопленных, но ещё не сброшенных обновлений по типам + инструментов; растёт между flush'ами и обнуляется в момент сброса. +- `ydb_sdk_metric_buffer_flush_duration_seconds_*` — гистограмма + длительности одного flush'а буфера; p50/p95/p99 нарисованы в Grafana. +- `ydb_sdk_metric_buffer_events_buffered_total` — суммарное число + «логических» обновлений, прошедших через буфер (`Inc()`, `Record()`, + `Set()`). Делённое на `flushes_total` даёт средний коэффициент + коалесинга: чем он больше, тем меньше нагрузка на OTel-аггрегатор. +- `ydb_sdk_metric_buffer_flushes_total{trigger=interval|shutdown|threshold}` — + счётчик сбросов по причине срабатывания. +- `ydb_sdk_metric_buffer_underlying_calls_total{kind=add|record_many|set}` — + фактическое число вызовов к нижестоящему `IMetricRegistry`; для + диагностики выигрыша от батчинга. + +### 6. Бенчмарк внутреннего батчинга метрик + +В `examples/metric_buffer_benchmark` лежит standalone-бенчмарк, который +сравнивает два режима эмиссии в синтетической нагрузке (8 потоков × +N инкрементов / `Record()` на общий счётчик и общую гистограмму): + +1. **`direct`** — без `TMetricBuffer`: каждый `Inc()` / `Record()` + идёт сразу в `IMetricRegistry` (как сейчас по умолчанию). +2. **`buffered`** — через `TMetricBuffer` с настраиваемым + `FlushIntervalMs`. + +Бенчмарк прогоняется на `TFakeMetricRegistry`, так что измеряется +именно overhead клиентской стороны (без YDB и без OTel-плагина) и +печатает: +- общее время `duration_ms`, +- пропускную способность `ops/sec`, +- число фактических `Inc()`/`Record()` вызовов в registry, +- коэффициент коалесинга (`logical_ops / underlying_calls`). + +Запуск: + +```bash +cmake --build . --target metric_buffer_benchmark -j$(nproc) +./examples/metric_buffer_benchmark/metric_buffer_benchmark \ + --threads 8 --ops 200000 --flush-ms 100 +``` + +### 7. Остановить ```bash cd examples/otel_tracing diff --git a/examples/otel_tracing/grafana/dashboards/ydb-query-service.json b/examples/otel_tracing/grafana/dashboards/ydb-query-service.json index 96c6c4357e2..db945231756 100644 --- a/examples/otel_tracing/grafana/dashboards/ydb-query-service.json +++ b/examples/otel_tracing/grafana/dashboards/ydb-query-service.json @@ -186,10 +186,97 @@ } ] }, + { + "type": "row", + "title": "SDK Metric Buffer (in-process batching)", + "collapsed": false, + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 32 } + }, + { + "title": "Metric Buffer — Pending Increments (per type)", + "type": "timeseries", + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 33 }, + "datasource": { "type": "prometheus", "uid": "${prometheus_ds}" }, + "fieldConfig": { + "defaults": { + "unit": "short", + "custom": { "drawStyle": "line", "fillOpacity": 10 } + } + }, + "targets": [ + { + "expr": "sum by (instrument) (ydb_sdk_metric_buffer_pending_updates)", + "legendFormat": "{{instrument}}" + } + ] + }, + { + "title": "Metric Buffer — Flush Duration p50/p95/p99 (s)", + "type": "timeseries", + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 33 }, + "datasource": { "type": "prometheus", "uid": "${prometheus_ds}" }, + "fieldConfig": { + "defaults": { + "unit": "s", + "custom": { "drawStyle": "line", "fillOpacity": 5 } + } + }, + "targets": [ + { + "expr": "histogram_quantile(0.50, sum by (le) (rate(ydb_sdk_metric_buffer_flush_duration_seconds_bucket[1m])))", + "legendFormat": "p50" + }, + { + "expr": "histogram_quantile(0.95, sum by (le) (rate(ydb_sdk_metric_buffer_flush_duration_seconds_bucket[1m])))", + "legendFormat": "p95" + }, + { + "expr": "histogram_quantile(0.99, sum by (le) (rate(ydb_sdk_metric_buffer_flush_duration_seconds_bucket[1m])))", + "legendFormat": "p99" + } + ] + }, + { + "title": "Metric Buffer — Coalescing Ratio (events / flush)", + "type": "timeseries", + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 41 }, + "datasource": { "type": "prometheus", "uid": "${prometheus_ds}" }, + "fieldConfig": { + "defaults": { + "unit": "short", + "custom": { "drawStyle": "line", "fillOpacity": 5 } + } + }, + "targets": [ + { + "expr": "rate(ydb_sdk_metric_buffer_events_buffered_total[$__rate_interval]) / clamp_min(rate(ydb_sdk_metric_buffer_flushes_total[$__rate_interval]), 1)", + "legendFormat": "events per flush" + } + ] + }, + { + "title": "Metric Buffer — Flushes by Trigger (rps)", + "type": "timeseries", + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 41 }, + "datasource": { "type": "prometheus", "uid": "${prometheus_ds}" }, + "fieldConfig": { + "defaults": { + "unit": "ops", + "custom": { "drawStyle": "bars", "fillOpacity": 30 }, + "color": { "mode": "palette-classic" } + } + }, + "targets": [ + { + "expr": "sum by (trigger) (rate(ydb_sdk_metric_buffer_flushes_total[$__rate_interval]))", + "legendFormat": "{{trigger}}" + } + ] + }, { "title": "Recent Traces", "type": "table", - "gridPos": { "h": 10, "w": 24, "x": 0, "y": 32 }, + "gridPos": { "h": 10, "w": 24, "x": 0, "y": 49 }, "datasource": { "type": "jaeger", "uid": "${jaeger_ds}" }, "targets": [ { diff --git a/examples/otel_tracing/main.cpp b/examples/otel_tracing/main.cpp index 0140e6af835..9b7df9a2bdf 100644 --- a/examples/otel_tracing/main.cpp +++ b/examples/otel_tracing/main.cpp @@ -8,13 +8,15 @@ #include #include +#include #include #include #include #include #include -#include +#include +#include #include #include #include @@ -49,6 +51,19 @@ struct TConfig { int Iterations = 20; int RetryWorkers = 6; int RetryOps = 30; + + bool EnableTracing = true; + bool EnableMetrics = true; + + int TraceMaxQueueSize = 4096; + int TraceScheduleDelayMs = 1000; + int TraceMaxExportBatchSize = 512; + + int MetricExportIntervalMs = 5000; + int MetricExportTimeoutMs = 3000; + + int MetricBufferFlushIntervalMs = 100; + int TelemetryDrainSleepMs = 3000; }; nostd::shared_ptr InitTracing(const TConfig& cfg) { @@ -56,7 +71,13 @@ nostd::shared_ptr InitTracing(const TConfi opts.url = cfg.OtlpEndpoint + "/v1/traces"; auto exporter = otlp::OtlpHttpExporterFactory::Create(opts); - auto processor = sdktrace::SimpleSpanProcessorFactory::Create(std::move(exporter)); + + sdktrace::BatchSpanProcessorOptions batchOpts; + batchOpts.max_queue_size = static_cast(cfg.TraceMaxQueueSize); + batchOpts.schedule_delay_millis = std::chrono::milliseconds(cfg.TraceScheduleDelayMs); + batchOpts.max_export_batch_size = static_cast(cfg.TraceMaxExportBatchSize); + + auto processor = sdktrace::BatchSpanProcessorFactory::Create(std::move(exporter), batchOpts); auto res = resource::Resource::Create({ {"service.name", "ydb-cpp-sdk-demo"}, @@ -71,12 +92,13 @@ nostd::shared_ptr InitTracing(const TConfi nostd::shared_ptr InitMetrics(const TConfig& cfg) { otlp::OtlpHttpMetricExporterOptions opts; opts.url = cfg.OtlpEndpoint + "/v1/metrics"; + opts.aggregation_temporality = otlp::PreferredAggregationTemporality::kCumulative; auto exporter = otlp::OtlpHttpMetricExporterFactory::Create(opts); sdkmetrics::PeriodicExportingMetricReaderOptions readerOpts; - readerOpts.export_interval_millis = std::chrono::milliseconds(5000); - readerOpts.export_timeout_millis = std::chrono::milliseconds(3000); + readerOpts.export_interval_millis = std::chrono::milliseconds(cfg.MetricExportIntervalMs); + readerOpts.export_timeout_millis = std::chrono::milliseconds(cfg.MetricExportTimeoutMs); auto reader = sdkmetrics::PeriodicExportingMetricReaderFactory::Create(std::move(exporter), readerOpts); @@ -379,6 +401,41 @@ int main(int argc, char** argv) { opts.AddLongOption("retry-ops", "Operations per retry worker") .DefaultValue(std::to_string(cfg.RetryOps)).StoreResult(&cfg.RetryOps); + opts.AddLongOption("disable-tracing", "Do not pass TraceProvider to YDB SDK") + .NoArgument() + .Handler0([&]{ cfg.EnableTracing = false; }); + opts.AddLongOption("disable-metrics", "Do not pass MetricRegistry to YDB SDK") + .NoArgument() + .Handler0([&]{ cfg.EnableMetrics = false; }); + + opts.AddLongOption("trace-max-queue-size", + "BatchSpanProcessor: max queued spans before drop") + .DefaultValue(std::to_string(cfg.TraceMaxQueueSize)).StoreResult(&cfg.TraceMaxQueueSize); + opts.AddLongOption("trace-schedule-delay-ms", + "BatchSpanProcessor: wait between exports, ms") + .DefaultValue(std::to_string(cfg.TraceScheduleDelayMs)).StoreResult(&cfg.TraceScheduleDelayMs); + opts.AddLongOption("trace-max-export-batch-size", + "BatchSpanProcessor: max spans per export RPC") + .DefaultValue(std::to_string(cfg.TraceMaxExportBatchSize)).StoreResult(&cfg.TraceMaxExportBatchSize); + + opts.AddLongOption("metric-export-interval-ms", + "PeriodicExportingMetricReader: export interval, ms") + .DefaultValue(std::to_string(cfg.MetricExportIntervalMs)) + .StoreResult(&cfg.MetricExportIntervalMs); + opts.AddLongOption("metric-export-timeout-ms", + "PeriodicExportingMetricReader: export timeout, ms") + .DefaultValue(std::to_string(cfg.MetricExportTimeoutMs)) + .StoreResult(&cfg.MetricExportTimeoutMs); + + opts.AddLongOption("metric-buffer-flush-ms", + "TMetricBuffer: flush interval, ms (0 disables the buffer)") + .DefaultValue(std::to_string(cfg.MetricBufferFlushIntervalMs)) + .StoreResult(&cfg.MetricBufferFlushIntervalMs); + opts.AddLongOption("telemetry-drain-sleep-ms", + "Sleep after ForceFlush so external collectors can scrape demo data") + .DefaultValue(std::to_string(cfg.TelemetryDrainSleepMs)) + .StoreResult(&cfg.TelemetryDrainSleepMs); + NLastGetopt::TOptsParseResult parsedOpts(&opts, argc, argv); if (cfg.Endpoint.rfind("grpc://", 0) == 0) { @@ -388,50 +445,86 @@ int main(int argc, char** argv) { } std::cout << "Initializing OpenTelemetry..." << std::endl; + std::cout << " Tracing: " << (cfg.EnableTracing ? "enabled" : "disabled") << std::endl; + std::cout << " Metrics: " << (cfg.EnableMetrics ? "enabled" : "disabled") << std::endl; std::cout << " OTLP endpoint: " << cfg.OtlpEndpoint << std::endl; + std::cout << " Trace batching: max_queue_size=" << cfg.TraceMaxQueueSize + << ", schedule_delay_ms=" << cfg.TraceScheduleDelayMs + << ", max_export_batch_size=" << cfg.TraceMaxExportBatchSize << std::endl; + std::cout << " Metric batching: export_interval_ms=" << cfg.MetricExportIntervalMs + << ", export_timeout_ms=" << cfg.MetricExportTimeoutMs << std::endl; + std::cout << " Metric buffer (in-SDK): flush_interval_ms=" + << cfg.MetricBufferFlushIntervalMs + << (cfg.MetricBufferFlushIntervalMs == 0 ? " (disabled)" : "") << std::endl; + std::cout << " Telemetry drain sleep: " << cfg.TelemetryDrainSleepMs << " ms" << std::endl; + + auto tracerProvider = cfg.EnableTracing ? InitTracing(cfg) : nullptr; + auto meterProvider = cfg.EnableMetrics ? InitMetrics(cfg) : nullptr; + + std::shared_ptr ydbTraceProvider; + std::shared_ptr ydbMetricRegistry; + + if (cfg.EnableTracing) { + opentelemetry::trace::Provider::SetTracerProvider(tracerProvider); + ydbTraceProvider = NTrace::CreateOtelTraceProvider(tracerProvider); + } - auto tracerProvider = InitTracing(cfg); - auto meterProvider = InitMetrics(cfg); + if (cfg.EnableMetrics) { + opentelemetry::metrics::Provider::SetMeterProvider(meterProvider); + auto otelMetricRegistry = NMetrics::CreateOtelMetricRegistry(meterProvider); + if (cfg.MetricBufferFlushIntervalMs > 0) { + NObservability::TMetricBufferSettings bufferSettings; + bufferSettings.FlushInterval = + std::chrono::milliseconds(cfg.MetricBufferFlushIntervalMs); + ydbMetricRegistry = NObservability::CreateBufferedMetricRegistry( + otelMetricRegistry, bufferSettings); + } else { + ydbMetricRegistry = otelMetricRegistry; + } + } - opentelemetry::trace::Provider::SetTracerProvider(tracerProvider); - opentelemetry::metrics::Provider::SetMeterProvider(meterProvider); + std::cout << "Connecting to YDB at " << cfg.Endpoint << cfg.Database << std::endl; - auto ydbTraceProvider = NTrace::CreateOtelTraceProvider(tracerProvider); - auto ydbMetricRegistry = NMetrics::CreateOtelMetricRegistry(meterProvider); + { + auto driverConfig = TDriverConfig() + .SetEndpoint(cfg.Endpoint) + .SetDatabase(cfg.Database) + .SetDiscoveryMode(EDiscoveryMode::Off); - std::cout << "Connecting to YDB at " << cfg.Endpoint << cfg.Database << std::endl; + if (ydbTraceProvider) { + driverConfig.SetTraceProvider(ydbTraceProvider); + } + if (ydbMetricRegistry) { + driverConfig.SetMetricRegistry(ydbMetricRegistry); + } - auto driverConfig = TDriverConfig() - .SetEndpoint(cfg.Endpoint) - .SetDatabase(cfg.Database) - .SetDiscoveryMode(EDiscoveryMode::Off) - .SetTraceProvider(ydbTraceProvider) - .SetMetricRegistry(ydbMetricRegistry); + TDriver driver(driverConfig); + NQuery::TQueryClient queryClient(driver); + NTable::TTableClient tableClient(driver); - TDriver driver(driverConfig); - NQuery::TQueryClient queryClient(driver); - NTable::TTableClient tableClient(driver); + try { + RunQueryWorkload(queryClient, cfg.Iterations); + RunTableWorkload(tableClient, cfg.Iterations); - try { - RunQueryWorkload(queryClient, cfg.Iterations); - RunTableWorkload(tableClient, cfg.Iterations); + if (cfg.RetryWorkers > 0 && cfg.RetryOps > 0) { + RunRetryWorkload(queryClient, cfg.RetryWorkers, cfg.RetryOps); + } - if (cfg.RetryWorkers > 0 && cfg.RetryOps > 0) { - RunRetryWorkload(queryClient, cfg.RetryWorkers, cfg.RetryOps); + std::cout << "\n=== Cleanup ===" << std::endl; + ThrowOnError(queryClient.RetryQuerySync([](NQuery::TSession session) { + return session.ExecuteQuery( + "DROP TABLE otel_demo", NQuery::TTxControl::NoTx()).GetValueSync(); + })); + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; } - std::cout << "\n=== Cleanup ===" << std::endl; - ThrowOnError(queryClient.RetryQuerySync([](NQuery::TSession session) { - return session.ExecuteQuery( - "DROP TABLE otel_demo", NQuery::TTxControl::NoTx()).GetValueSync(); - })); - } catch (const std::exception& e) { - std::cerr << "Error: " << e.what() << std::endl; + driver.Stop(true); } std::cout << "Flushing telemetry..." << std::endl; - driver.Stop(true); + NObservability::FlushBufferedMetricRegistry(ydbMetricRegistry); if (auto* sdkTracerProvider = dynamic_cast(tracerProvider.get())) { sdkTracerProvider->ForceFlush(); @@ -440,7 +533,11 @@ int main(int argc, char** argv) { sdkMeterProvider->ForceFlush(); } - std::this_thread::sleep_for(std::chrono::seconds(3)); + ydbMetricRegistry.reset(); + + if (cfg.TelemetryDrainSleepMs > 0) { + std::this_thread::sleep_for(std::chrono::milliseconds(cfg.TelemetryDrainSleepMs)); + } opentelemetry::trace::Provider::SetTracerProvider( nostd::shared_ptr{}); diff --git a/include/ydb-cpp-sdk/client/iam/common/generic_provider.h b/include/ydb-cpp-sdk/client/iam/common/generic_provider.h index 20029b178de..5df5edd5eda 100644 --- a/include/ydb-cpp-sdk/client/iam/common/generic_provider.h +++ b/include/ydb-cpp-sdk/client/iam/common/generic_provider.h @@ -7,6 +7,7 @@ #include #include +#include #include #include @@ -185,25 +186,43 @@ class TGrpcIamCredentialsProvider : public ICredentialsProvider { TRequest req; - RequestFiller_(req); + try { + RequestFiller_(req); + } catch (...) { + std::lock_guard guard(Lock_); + LastRequestError_ = TStringBuilder() << "Request failed: " << CurrentExceptionMessage(); + ResetContextImpl(); + return; + } Rpc_(Stub_.get(), &*Context_, &req, response.get(), std::move(cb)); } void FillContext(std::unique_lock& guard) { + std::optional authToken; + if (AuthTokenProvider_) { + guard.unlock(); + try { + authToken = AuthTokenProvider_->GetAuthInfo(); + } catch (...) { + guard.lock(); + throw; + } + guard.lock(); + if (NeedStop_) { + return; + } + } + auto& context = Context_.emplace(); - auto deadline = gpr_time_add( + const auto deadline = gpr_time_add( gpr_now(GPR_CLOCK_MONOTONIC), gpr_time_from_micros(IamEndpoint_.RequestTimeout.MicroSeconds(), GPR_TIMESPAN)); context.set_deadline(deadline); - if (AuthTokenProvider_) { - guard.unlock(); - auto token = AuthTokenProvider_->GetAuthInfo(); - guard.lock(); - - context.AddMetadata("authorization", "Bearer " + token); + if (authToken) { + context.AddMetadata("authorization", "Bearer " + *authToken); } } @@ -230,6 +249,7 @@ class TGrpcIamCredentialsProvider : public ICredentialsProvider { } FillContext(guard); if (NeedStop_) { + ResetContextImpl(); return false; } } @@ -320,6 +340,33 @@ class TGrpcIamCredentialsProvider : public ICredentialsProvider { std::shared_ptr Impl_; }; +// Adapter that keeps a self-owned ICoreFacility alive for the lifetime of an inner credentials +// provider. Used by deprecated no-arg ICredentialsProviderFactory::CreateProvider() paths where +// the caller hasn't supplied a facility. +class TOwningFacilityCredentialsProvider : public ICredentialsProvider { +public: + TOwningFacilityCredentialsProvider(std::shared_ptr facility, + TCredentialsProviderPtr inner) + : Facility_(std::move(facility)) + , Inner_(std::move(inner)) + {} + + std::string GetAuthInfo() const override { + return Inner_->GetAuthInfo(); + } + + bool IsValid() const override { + return Inner_->IsValid(); + } + +private: + // Field declaration order matters: Inner_ is destroyed first so that its Stop() can still + // drive the facility's queue (cancel the in-flight gRPC context, drain the response callback), + // and only then is Facility_ destroyed. + std::shared_ptr Facility_; + TCredentialsProviderPtr Inner_; +}; + template class TIamJwtCredentialsProvider : public TGrpcIamCredentialsProvider { public: @@ -349,8 +396,14 @@ class TIamJwtCredentialsProviderFactory : public ICredentialsProviderFactory { public: TIamJwtCredentialsProviderFactory(const TIamJwtParams& params): Params_(params) {} + // Deprecated. Kept for backward compatibility with callers (including out-of-tree mirrors) + // that don't have access to an ICoreFacility. Spins up a private TSimpleCoreFacility and ties + // its lifetime to the returned provider via TOwningFacilityCredentialsProvider. TCredentialsProviderPtr CreateProvider() const final { - ythrow yexception() << "Not supported"; + auto facility = CreateSimpleCoreFacility(); + auto inner = std::make_shared>( + Params_, std::weak_ptr(facility)); + return std::make_shared(std::move(facility), std::move(inner)); } TCredentialsProviderPtr CreateProvider(std::weak_ptr facility) const override { @@ -366,8 +419,12 @@ class TIamOAuthCredentialsProviderFactory : public ICredentialsProviderFactory { public: TIamOAuthCredentialsProviderFactory(const TIamOAuth& params): Params_(params) {} + // Deprecated. Kept for backward compatibility — see comment on TIamJwtCredentialsProviderFactory. TCredentialsProviderPtr CreateProvider() const final { - ythrow yexception() << "Not supported"; + auto facility = CreateSimpleCoreFacility(); + auto inner = std::make_shared>( + Params_, std::weak_ptr(facility)); + return std::make_shared(std::move(facility), std::move(inner)); } TCredentialsProviderPtr CreateProvider(std::weak_ptr facility) const override { diff --git a/include/ydb-cpp-sdk/client/metrics/metric_buffer.h b/include/ydb-cpp-sdk/client/metrics/metric_buffer.h new file mode 100644 index 00000000000..635f141aab9 --- /dev/null +++ b/include/ydb-cpp-sdk/client/metrics/metric_buffer.h @@ -0,0 +1,32 @@ +#pragma once + +#include + +#include +#include +#include + +namespace NYdb::inline V3::NObservability { + +inline constexpr auto kDefaultFlushInterval = std::chrono::milliseconds(100); +inline constexpr std::size_t kDefaultThreadPendingThreshold = 64 * 1024; +inline constexpr std::size_t kDefaultThreadPendingLimit = + 4 * kDefaultThreadPendingThreshold; +inline constexpr std::size_t kDefaultHistogramReserveSamples = 256; + +struct TMetricBufferSettings { + std::chrono::milliseconds FlushInterval = kDefaultFlushInterval; + std::size_t ThreadPendingThreshold = kDefaultThreadPendingThreshold; + std::size_t ThreadPendingLimit = kDefaultThreadPendingLimit; + std::size_t HistogramReserveSamples = kDefaultHistogramReserveSamples; + + std::shared_ptr SelfMetricsRegistry; +}; + +std::shared_ptr CreateBufferedMetricRegistry( + std::shared_ptr underlying, + TMetricBufferSettings settings = {}); + +bool FlushBufferedMetricRegistry(const std::shared_ptr& registry); + +} // namespace NYdb::NObservability diff --git a/include/ydb-cpp-sdk/client/metrics/metrics.h b/include/ydb-cpp-sdk/client/metrics/metrics.h index 5faa930ed50..077187c3996 100644 --- a/include/ydb-cpp-sdk/client/metrics/metrics.h +++ b/include/ydb-cpp-sdk/client/metrics/metrics.h @@ -14,6 +14,12 @@ class ICounter { public: virtual ~ICounter() = default; virtual void Inc() = 0; + + virtual void Add(std::uint64_t delta) { + for (std::uint64_t i = 0; i < delta; ++i) { + Inc(); + } + } }; class IGauge { @@ -27,6 +33,12 @@ class IHistogram { public: virtual ~IHistogram() = default; virtual void Record(double value) = 0; + + virtual void RecordMany(const std::vector& values) { + for (double v : values) { + Record(v); + } + } }; class IMetricRegistry { diff --git a/include/ydb-cpp-sdk/client/table/table.h b/include/ydb-cpp-sdk/client/table/table.h index e3b404ff3b4..9c171202a0c 100644 --- a/include/ydb-cpp-sdk/client/table/table.h +++ b/include/ydb-cpp-sdk/client/table/table.h @@ -43,6 +43,8 @@ class TableIndexDescription; class ValueSinceUnixEpochModeSettings; class EvictionToExternalStorageSettings; class CompactItem; +class LocalBloomFilterIndex; +class LocalBloomNgramFilterIndex; } // namespace Table } // namespace Ydb @@ -416,6 +418,25 @@ struct TFulltextIndexSettings { }; //! Represents index description +// If FalsePositiveProbability is left unset, the server applies its built-in default of 0.1. +struct TLocalBloomFilterSettings { + std::optional FalsePositiveProbability; + + static TLocalBloomFilterSettings FromProto(const Ydb::Table::LocalBloomFilterIndex& proto); + void SerializeTo(Ydb::Table::LocalBloomFilterIndex& proto) const; +}; + +// All fields are optional. If a field is left unset, the server applies its built-in +// default: NgramSize = 3, CaseSensitive = true, FalsePositiveProbability = 0.1. +struct TLocalBloomNgramFilterSettings { + std::optional NgramSize; + std::optional CaseSensitive; + std::optional FalsePositiveProbability; + + static TLocalBloomNgramFilterSettings FromProto(const Ydb::Table::LocalBloomNgramFilterIndex& proto); + void SerializeTo(Ydb::Table::LocalBloomNgramFilterIndex& proto) const; +}; + class TIndexDescription { friend class NYdb::TProtoAccessor; @@ -426,7 +447,7 @@ class TIndexDescription { const std::vector& indexColumns, const std::vector& dataColumns = {}, const std::vector& globalIndexSettings = {}, - const std::variant& specializedIndexSettings = {} + const std::variant& specializedIndexSettings = {} ); TIndexDescription( @@ -440,7 +461,7 @@ class TIndexDescription { EIndexType GetIndexType() const; const std::vector& GetIndexColumns() const; const std::vector& GetDataColumns() const; - const std::variant& GetIndexSettings() const; + const std::variant& GetIndexSettings() const; uint64_t GetSizeBytes() const; void SetParallel(uint32_t parallel); @@ -520,7 +541,7 @@ class TIndexDescription { std::vector IndexColumns_; std::vector DataColumns_; std::vector GlobalIndexSettings_; - std::variant SpecializedIndexSettings_; + std::variant SpecializedIndexSettings_; uint64_t SizeBytes_ = 0; uint32_t Parallel_ = 0; }; diff --git a/include/ydb-cpp-sdk/client/table/table_enum.h b/include/ydb-cpp-sdk/client/table/table_enum.h index 9f156054cc8..dd10eaa274b 100644 --- a/include/ydb-cpp-sdk/client/table/table_enum.h +++ b/include/ydb-cpp-sdk/client/table/table_enum.h @@ -38,6 +38,9 @@ enum class EIndexType { GlobalFulltextPlain, GlobalFulltextRelevance, GlobalJson, + LocalBloomFilter, + LocalBloomNgramFilter, + LocalMinMax, Unknown = std::numeric_limits::max() }; diff --git a/include/ydb-cpp-sdk/client/topic/control_plane.h b/include/ydb-cpp-sdk/client/topic/control_plane.h index e4e42f28064..cb8ef3140ce 100644 --- a/include/ydb-cpp-sdk/client/topic/control_plane.h +++ b/include/ydb-cpp-sdk/client/topic/control_plane.h @@ -356,6 +356,10 @@ class TTopicDescription { uint64_t GetPartitionWriteBurstBytes() const; + uint64_t GetPartitionWriteSpeedMessagesPerSecond() const; + + uint64_t GetPartitionWriteBurstMessages() const; + bool GetContentBasedDeduplication() const; const std::map& GetAttributes() const; @@ -381,6 +385,8 @@ class TTopicDescription { std::optional RetentionStorageMb_; uint64_t PartitionWriteSpeedBytesPerSecond_; uint64_t PartitionWriteBurstBytes_; + uint64_t PartitionWriteSpeedMessagesPerSecond_; + uint64_t PartitionWriteBurstMessages_; EMeteringMode MeteringMode_; std::map Attributes_; std::vector Consumers_; @@ -794,6 +800,8 @@ struct TCreateTopicSettings : public TOperationRequestSettings, Consumers); @@ -937,6 +945,8 @@ struct TAlterTopicSettings : public TOperationRequestSettings { struct TMessageInformation { TMessageInformation(uint64_t offset, - std::string producerId, + std::string_view producerId, uint64_t seqNo, TInstant createTime, TInstant writeTime, diff --git a/include/ydb-cpp-sdk/client/types/core_facility/core_facility.h b/include/ydb-cpp-sdk/client/types/core_facility/core_facility.h index a34f46ac245..6e4d48dba74 100644 --- a/include/ydb-cpp-sdk/client/types/core_facility/core_facility.h +++ b/include/ydb-cpp-sdk/client/types/core_facility/core_facility.h @@ -5,6 +5,8 @@ #include +#include + namespace NYdb::inline V3 { using TPeriodicCb = std::function; @@ -21,4 +23,9 @@ class ICoreFacility { virtual void PostToResponseQueue(TPostTaskCb&& f) = 0; }; +// Self-contained single-threaded ICoreFacility for cases when the SDK core is not available +// (e.g. when a credentials provider factory's deprecated no-arg CreateProvider() is invoked +// directly without an enclosing TDriver). +std::shared_ptr CreateSimpleCoreFacility(); + } // namespace NYdb diff --git a/include/ydb-cpp-sdk/client/types/status/status.h b/include/ydb-cpp-sdk/client/types/status/status.h index 5b3e00dcc94..0e86eea0aa0 100644 --- a/include/ydb-cpp-sdk/client/types/status/status.h +++ b/include/ydb-cpp-sdk/client/types/status/status.h @@ -77,7 +77,6 @@ void ThrowOnError(TStatus status, std::function onSuccess = [](TS void ThrowOnErrorOrPrintIssues(TStatus status); bool StatusContainsIssueWithCode(const TStatus& status, NYdb::NIssue::TIssueCode code); - } } // namespace NYdb diff --git a/include/ydb-cpp-sdk/library/operation_id/operation_id.h b/include/ydb-cpp-sdk/library/operation_id/operation_id.h index 5727a9bf178..8677e198680 100644 --- a/include/ydb-cpp-sdk/library/operation_id/operation_id.h +++ b/include/ydb-cpp-sdk/library/operation_id/operation_id.h @@ -29,6 +29,7 @@ class TOperationId { INCREMENTAL_BACKUP = 11, RESTORE = 12, COMPACTION = 13, + FULL_BACKUP = 14, }; struct TData { diff --git a/plugins/metrics/otel/metrics.cpp b/plugins/metrics/otel/metrics.cpp index e4e03eb6199..5881af9f2f3 100644 --- a/plugins/metrics/otel/metrics.cpp +++ b/plugins/metrics/otel/metrics.cpp @@ -35,6 +35,13 @@ class TOtelCounter : public ICounter { Counter_->Add(1, MakeAttributes(Labels_), context::RuntimeContext::GetCurrent()); } + void Add(std::uint64_t delta) override { + if (delta == 0) { + return; + } + Counter_->Add(delta, MakeAttributes(Labels_), context::RuntimeContext::GetCurrent()); + } + private: nostd::shared_ptr> Counter_; TLabels Labels_; @@ -83,6 +90,17 @@ class TOtelHistogram : public IHistogram { Histogram_->Record(value, MakeAttributes(Labels_), context::RuntimeContext::GetCurrent()); } + void RecordMany(const std::vector& values) override { + if (values.empty()) { + return; + } + auto attrs = MakeAttributes(Labels_); + auto ctx = context::RuntimeContext::GetCurrent(); + for (double v : values) { + Histogram_->Record(v, attrs, ctx); + } + } + private: nostd::shared_ptr> Histogram_; TLabels Labels_; diff --git a/scripts/test_copy_sources_plugins.sh b/scripts/test_copy_sources_plugins.sh index 40724818c94..ddf4804800a 100755 --- a/scripts/test_copy_sources_plugins.sh +++ b/scripts/test_copy_sources_plugins.sh @@ -45,7 +45,8 @@ mkdir -p "$OSS/plugins/trace/otel" mkdir -p "$OSS/plugins/metrics/otel" mkdir -p "$OSS/include/ydb-cpp-sdk" mkdir -p "$OSS/src" -mkdir -p "$OSS/tests/slo_workloads" +mkdir -p "$OSS/tests/slo_workloads/key_value" +mkdir -p "$OSS/tests/deb_package/test_project" echo "OSS_TRACE" > "$OSS/plugins/trace/otel/trace.cpp" echo "OSS_METRICS" > "$OSS/plugins/metrics/otel/metrics.cpp" @@ -58,6 +59,9 @@ echo "# OSS_CMAKE" > "$OSS/plugins/metrics/otel/CMakeLists.txt" touch "$OSS/.gitignore" "$OSS/.gitmodules" "$OSS/CMakePresets.json" "$OSS/CMakeLists.txt" touch "$OSS/LICENSE" "$OSS/README.md" touch "$OSS/tests/slo_workloads/.dockerignore" "$OSS/tests/slo_workloads/Dockerfile" +echo "OSS_SLO_WORKLOAD" > "$OSS/tests/slo_workloads/key_value/main.cpp" +echo "OSS_DEB_PACKAGE" > "$OSS/tests/deb_package/Dockerfile" +echo "OSS_DEB_PACKAGE" > "$OSS/tests/deb_package/test_project/main.cpp" echo "// oss type_switcher" > "$OSS/include/ydb-cpp-sdk/type_switcher.h" echo "// oss version" > "$OSS/src/version.h" @@ -89,6 +93,9 @@ assert_contains() { assert_contains "$OSS/plugins/trace/otel/trace.cpp" "MONOREPO_TRACE" "OSS_TRACE" assert_contains "$OSS/plugins/metrics/otel/metrics.cpp" "MONOREPO_METRICS" "OSS_METRICS" +assert_contains "$OSS/tests/slo_workloads/key_value/main.cpp" "OSS_SLO_WORKLOAD" "" +assert_contains "$OSS/tests/deb_package/Dockerfile" "OSS_DEB_PACKAGE" "" +assert_contains "$OSS/tests/deb_package/test_project/main.cpp" "OSS_DEB_PACKAGE" "" while IFS= read -r cmake_file; do assert_contains "$cmake_file" "OSS_CMAKE" "MONOREPO_PLUGINS_CMAKE" diff --git a/src/api/client/yc_private/iam/reference.proto b/src/api/client/yc_private/iam/reference.proto deleted file mode 100644 index 5cf61ed031c..00000000000 --- a/src/api/client/yc_private/iam/reference.proto +++ /dev/null @@ -1,23 +0,0 @@ -syntax = "proto3"; - -package yandex.cloud.priv.reference; - -message Reference { - enum Type { - TYPE_UNSPECIFIED = 0; - MANAGED_BY = 1; - USED_BY = 2; - } - Referrer referrer = 1; - Type type = 2; -} - -message Referrer { - // * `type = compute.instance, id = ` - // * `type = compute.instanceGroup, id = ` - // * `type = loadbalancer.networkLoadBalancer, id = ` - // * `type = managed-kubernetes.cluster, id = ` - // * `type = managed-mysql.cluster, id = ` - string type = 1; - string id = 2; -} diff --git a/src/api/client/yc_private/iam/service_account_service.proto b/src/api/client/yc_private/iam/service_account_service.proto index aab378611ca..105444cc6f4 100644 --- a/src/api/client/yc_private/iam/service_account_service.proto +++ b/src/api/client/yc_private/iam/service_account_service.proto @@ -8,7 +8,6 @@ import "src/api/client/yc_private/iam/iam_token.proto"; import "src/api/client/yc_private/iam/service_account.proto"; //import "ydb/public/api/client/yc_private/access/access.proto"; import "src/api/client/yc_private/operation/operation.proto"; -import "src/api/client/yc_private/iam/reference.proto"; service ServiceAccountService { rpc Get (GetServiceAccountRequest) returns (ServiceAccount) { @@ -40,10 +39,6 @@ service ServiceAccountService { rpc IssueToken (IssueTokenRequest) returns (IamToken) { option (google.api.http) = { post: "/iam/v1/serviceAccounts/{service_account_id}:issueToken" body: "*" }; } - - rpc ListReferences (ListServiceAccountReferencesRequest) returns (ListServiceAccountReferencesResponse); - - rpc UpdateReferences (UpdateServiceAccountReferencesRequest) returns (.ydb.yc.priv.operation.Operation); } message GetServiceAccountRequest { @@ -68,8 +63,6 @@ message CreateServiceAccountRequest { string description = 3; string id = 4; map labels = 5; - - repeated reference.Reference references = 100; } message CreateServiceAccountMetadata { @@ -112,27 +105,3 @@ message IssueTokenRequest { string service_account_id = 1; string instance_id = 2; } - -message ListServiceAccountReferencesRequest { - string service_account_id = 1; - int64 page_size = 2; - string page_token = 3; -} - -message ListServiceAccountReferencesResponse { - repeated reference.Reference references = 1; - string next_page_token = 2; -} - -message UpdateServiceAccountReferencesRequest { - string service_account_id = 1; - repeated reference.Reference reference_additions = 2; - repeated reference.Reference reference_deletions = 3; -} - -message UpdateServiceAccountReferencesResponse { -} - -message UpdateServiceAccountReferencesMetadata { - string service_account_id = 1; -} diff --git a/src/api/protos/draft/ydb_backup.proto b/src/api/protos/draft/ydb_backup.proto index 43dd1790fa4..03690a459bd 100644 --- a/src/api/protos/draft/ydb_backup.proto +++ b/src/api/protos/draft/ydb_backup.proto @@ -183,6 +183,7 @@ message BackupProgress { PROGRESS_UNSPECIFIED = 0; PROGRESS_PREPARING = 1; PROGRESS_TRANSFER_DATA = 2; + // Terminal, not necessarily successful: check the status field for success vs failure. PROGRESS_DONE = 3; PROGRESS_CANCELLATION = 4; PROGRESS_CANCELLED = 5; @@ -197,6 +198,14 @@ message IncrementalBackupMetadata { message IncrementalBackupResult { } +message BackupMetadata { + BackupProgress.Progress progress = 1; + int32 progress_percent = 2 [(Ydb.value) = "[0; 100]"]; +} + +message BackupResult { +} + message RestoreProgress { enum Progress { PROGRESS_UNSPECIFIED = 0; diff --git a/src/api/protos/ydb_export.proto b/src/api/protos/ydb_export.proto index 262e95a36c6..95822a81daf 100644 --- a/src/api/protos/ydb_export.proto +++ b/src/api/protos/ydb_export.proto @@ -71,6 +71,13 @@ message ExportToYtResponse { Ydb.Operations.Operation operation = 1; } +message YdbDumpFormat { +} + +message ParquetFormat { + uint32 row_group_size = 1; +} + /// S3 message ExportToS3Settings { enum Scheme { @@ -155,6 +162,11 @@ message ExportToS3Settings { // - Patterns are matched against the object path relative to the export's source_path. // - Object is excluded from export operation if it matches any of the specified exclude regexps. repeated string exclude_regexps = 17; + + oneof format { + YdbDumpFormat ydb_dump = 18; + ParquetFormat parquet = 19; + } } message ExportToS3Result { diff --git a/src/api/protos/ydb_table.proto b/src/api/protos/ydb_table.proto index a98e296f751..fac3b770311 100644 --- a/src/api/protos/ydb_table.proto +++ b/src/api/protos/ydb_table.proto @@ -102,6 +102,8 @@ message KMeansTreeSettings { optional uint32 overlap_clusters = 4; optional double overlap_ratio = 5; + + optional bool adaptive_clusters = 6; } message GlobalIndex { diff --git a/src/client/iam_private/common/iam.h b/src/client/iam_private/common/iam.h index 89c03277d00..21e8354abc8 100644 --- a/src/client/iam_private/common/iam.h +++ b/src/client/iam_private/common/iam.h @@ -7,32 +7,64 @@ namespace NYdb::inline V3 { template class TIamServiceCredentialsProviderFactory : public ICredentialsProviderFactory { private: + static auto MakeRequestFiller(TIamServiceParams params) { + return [params = std::move(params)](TRequest& req) { + req.set_service_id(params.ServiceId); + req.set_microservice_id(params.MicroserviceId); + req.set_resource_id(params.ResourceId); + req.set_resource_type(params.ResourceType); + req.set_target_service_account_id(params.TargetServiceAccountId); + }; + } + + static auto MakeRpc() { + return [](typename TService::Stub* stub, grpc::ClientContext* context, const TRequest* request, TResponse* response, std::function cb) { + stub->async()->CreateForService(context, request, response, std::move(cb)); + }; + } + class TCredentialsProvider : public TGrpcIamCredentialsProvider { public: + // TDriver path: a shared facility (TGRpcConnectionsImpl) supports multiple periodic tasks, + // so we can hand the same weak_ptr to the nested auth provider here. TCredentialsProvider(const TIamServiceParams& params, std::weak_ptr responseFacility) : TGrpcIamCredentialsProvider(params, - [¶ms](TRequest& req) { - req.set_service_id(params.ServiceId); - req.set_microservice_id(params.MicroserviceId); - req.set_resource_id(params.ResourceId); - req.set_resource_type(params.ResourceType); - req.set_target_service_account_id(params.TargetServiceAccountId); - }, - [](typename TService::Stub* stub, grpc::ClientContext* context, const TRequest* request, TResponse* response, std::function cb) { - stub->async()->CreateForService(context, request, response, std::move(cb)); - }, + MakeRequestFiller(params), + MakeRpc(), responseFacility, params.SystemServiceAccountCredentials->CreateProvider(responseFacility)) {} + + // Standalone (no-arg) path: the caller has already built a self-owning auth provider + // backed by its OWN facility. We must not share `outerFacility` with the auth provider + // because TSimpleCoreFacility allows only one periodic task. + TCredentialsProvider(const TIamServiceParams& params, + std::weak_ptr outerFacility, + TCredentialsProviderPtr authProvider) + : TGrpcIamCredentialsProvider(params, + MakeRequestFiller(params), + MakeRpc(), + std::move(outerFacility), + std::move(authProvider)) + {} }; public: TIamServiceCredentialsProviderFactory(const TIamServiceParams& params) - : Params_(params) + : Params_(params) {} + // Deprecated. Kept for backward compatibility — see comment on TIamJwtCredentialsProviderFactory. + // The nested auth provider gets its own facility (via a recursive no-arg CreateProvider() that + // returns a TOwningFacilityCredentialsProvider). Sharing a TSimpleCoreFacility between two gRPC + // IAM providers would abort: each one registers a periodic task and the facility allows only one. TCredentialsProviderPtr CreateProvider() const override final { - ythrow yexception() << "Not supported"; + auto authProvider = Params_.SystemServiceAccountCredentials->CreateProvider(); + auto outerFacility = CreateSimpleCoreFacility(); + auto serviceProvider = std::make_shared( + Params_, std::weak_ptr(outerFacility), std::move(authProvider)); + return std::make_shared( + std::move(outerFacility), std::move(serviceProvider)); } TCredentialsProviderPtr CreateProvider(std::weak_ptr facility) const override { diff --git a/src/client/impl/internal/db_driver_state/state.cpp b/src/client/impl/internal/db_driver_state/state.cpp index 9694bb5302d..2c87fd71952 100644 --- a/src/client/impl/internal/db_driver_state/state.cpp +++ b/src/client/impl/internal/db_driver_state/state.cpp @@ -164,8 +164,8 @@ TDbDriverStateTracker::TDbDriverStateTracker(IInternalClient* client) {} TDbDriverStatePtr TDbDriverStateTracker::GetDriverState( - std::string database, - std::string discoveryEndpoint, + const std::string& database, + const std::string& discoveryEndpoint, EDiscoveryMode discoveryMode, const TSslCredentials& sslCredentials, std::shared_ptr credentialsProviderFactory @@ -174,8 +174,9 @@ TDbDriverStatePtr TDbDriverStateTracker::GetDriverState( if (credentialsProviderFactory) { clientIdentity = credentialsProviderFactory->GetClientIdentity(); } - Quote(database); - const TStateKey key{database, discoveryEndpoint, clientIdentity, discoveryMode, sslCredentials}; + std::string quotedDatabase = database; + Quote(quotedDatabase); + const TStateKey key{quotedDatabase, discoveryEndpoint, clientIdentity, discoveryMode, sslCredentials}; { std::shared_lock lock(Lock_); auto state = States_.find(key); @@ -218,7 +219,7 @@ TDbDriverStatePtr TDbDriverStateTracker::GetDriverState( }; strongState = std::shared_ptr( new TDbDriverState( - database, + quotedDatabase, discoveryEndpoint, discoveryMode, sslCredentials, diff --git a/src/client/impl/internal/db_driver_state/state.h b/src/client/impl/internal/db_driver_state/state.h index 1e8b99d6c7e..c975aaaf315 100644 --- a/src/client/impl/internal/db_driver_state/state.h +++ b/src/client/impl/internal/db_driver_state/state.h @@ -99,8 +99,8 @@ class TDbDriverStateTracker { public: TDbDriverStateTracker(IInternalClient* client); TDbDriverState::TPtr GetDriverState( - std::string database, - std::string DiscoveryEndpoint, + const std::string& database, + const std::string& discoveryEndpoint, EDiscoveryMode discoveryMode, const TSslCredentials& sslCredentials, std::shared_ptr credentialsProviderFactory diff --git a/src/client/impl/internal/plain_status/status.cpp b/src/client/impl/internal/plain_status/status.cpp index a0dcfb5fc48..e65aaa31c8d 100644 --- a/src/client/impl/internal/plain_status/status.cpp +++ b/src/client/impl/internal/plain_status/status.cpp @@ -72,8 +72,14 @@ TPlainStatus TPlainStatus::Internal(const std::string& message) { return { EStatus::CLIENT_INTERNAL_ERROR, "Internal client error: " + message }; } +namespace { + +static const std::string ConsumedUnitsHeaderKey{YDB_CONSUMED_UNITS_HEADER}; + +} // namespace + void TPlainStatus::InitCostInfo() { - if (auto metaIt = Metadata.find(YDB_CONSUMED_UNITS_HEADER); metaIt != Metadata.end()) { + if (auto metaIt = Metadata.find(ConsumedUnitsHeaderKey); metaIt != Metadata.end()) { try { CostInfo.set_consumed_units(std::stod(metaIt->second)); } catch (std::exception& e) { diff --git a/src/client/impl/internal/retry/retry.h b/src/client/impl/internal/retry/retry.h index 89f07bf2c65..2a25f0bfad7 100644 --- a/src/client/impl/internal/retry/retry.h +++ b/src/client/impl/internal/retry/retry.h @@ -103,6 +103,9 @@ class TRetryContextBase : TNonCopyable { return NextStep::Finish; } + case EStatus::CLIENT_DEADLINE_EXCEEDED: + Reset(); + [[fallthrough]]; default: return Settings_.RetryUndefined_ ? NextStep::RetrySlowBackoff : NextStep::Finish; } diff --git a/src/client/impl/observability/metric_buffer.cpp b/src/client/impl/observability/metric_buffer.cpp new file mode 100644 index 00000000000..6c2c5ab2ca8 --- /dev/null +++ b/src/client/impl/observability/metric_buffer.cpp @@ -0,0 +1,781 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace NYdb::inline V3::NObservability { + +namespace { + +using NMetrics::ICounter; +using NMetrics::IGauge; +using NMetrics::IHistogram; +using NMetrics::IMetricRegistry; +using NMetrics::TLabels; + +constexpr const char* kFlushDurationMetric = "ydb_sdk_metric_buffer_flush_duration_seconds"; +constexpr const char* kFlushesTotalMetric = "ydb_sdk_metric_buffer_flushes_total"; +constexpr const char* kEventsBufferedTotal = "ydb_sdk_metric_buffer_events_buffered_total"; +constexpr const char* kUnderlyingCallsTotal= "ydb_sdk_metric_buffer_underlying_calls_total"; +constexpr const char* kPendingUpdatesMetric= "ydb_sdk_metric_buffer_pending_updates"; +constexpr const char* kDroppedUpdatesTotal= "ydb_sdk_metric_buffer_dropped_updates_total"; + +const std::vector& FlushDurationBuckets() { + static const std::vector kBuckets = { + 0.00005, 0.0001, 0.0005, 0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0 + }; + return kBuckets; +} + +class TMetricBufferCore : public std::enable_shared_from_this { +public: + enum class EFlushTrigger { + Interval, + Threshold, + Manual, + Shutdown, + }; + + struct TCounterHandleInfo { + std::shared_ptr Underlying; + }; + struct THistogramHandleInfo { + std::shared_ptr Underlying; + }; + + struct TThreadState { + std::mutex Mutex; + std::vector CounterDeltas; + std::vector> HistogramSamples; + std::atomic PendingOps{0}; + std::atomic Active{true}; // becomes false when owning thread exits + }; + + explicit TMetricBufferCore(std::shared_ptr underlying, + TMetricBufferSettings settings) + : Underlying_(std::move(underlying)) + , Settings_(std::move(settings)) + { + if (!Settings_.SelfMetricsRegistry) { + Settings_.SelfMetricsRegistry = Underlying_; + } + InitSelfMetrics(); + } + + void Start() { + FlushThread_ = std::thread([this] { + Run(); + }); + } + + ~TMetricBufferCore() { + Shutdown(); + } + + void Shutdown() noexcept { + bool expected = false; + if (!Stopping_.compare_exchange_strong(expected, true)) { + return; + } + { + std::lock_guard lock(WaitMutex_); + Wakeup_.notify_all(); + } + if (FlushThread_.joinable()) { + try { + FlushThread_.join(); + } catch (...) { + // best-effort + } + } + try { + FlushAll(EFlushTrigger::Shutdown); + } catch (...) { + // best-effort: never let a misbehaving underlying registry crash the + // owning application during teardown. + } + } + + // -- Handle registration -------------------------------------------------- + + std::size_t RegisterCounter(std::shared_ptr underlying) { + std::lock_guard lock(HandlesMutex_); + Counters_.push_back({std::move(underlying)}); + return Counters_.size() - 1; + } + + std::size_t RegisterHistogram(std::shared_ptr underlying) { + std::lock_guard lock(HandlesMutex_); + Histograms_.push_back({std::move(underlying)}); + return Histograms_.size() - 1; + } + + // -- Hot-path entry points ------------------------------------------------ + + void OnCounterAdd(std::size_t handle, std::uint64_t delta) { + if (delta == 0) { + return; + } + if (Stopping_.load(std::memory_order_acquire)) { + std::shared_ptr underlying; + { + std::lock_guard lock(HandlesMutex_); + if (handle < Counters_.size()) { + underlying = Counters_[handle].Underlying; + } + } + if (underlying) { + underlying->Add(delta); + } + return; + } + TThreadState& state = AcquireThreadState(); + if (ShouldDropUpdate(state, 1)) { + ReportDroppedCounter(delta); + TriggerFlush(EFlushTrigger::Threshold); + return; + } + bool overThreshold = false; + { + std::lock_guard lock(state.Mutex); + if (state.CounterDeltas.size() <= handle) { + state.CounterDeltas.resize(handle + 1, 0); + } + state.CounterDeltas[handle] += delta; + } + const auto pending = state.PendingOps.fetch_add(1, std::memory_order_relaxed) + 1; + if (Settings_.ThreadPendingThreshold != 0 + && pending >= Settings_.ThreadPendingThreshold) { + overThreshold = true; + } + if (overThreshold) { + TriggerFlush(EFlushTrigger::Threshold); + } + } + + void OnHistogramRecord(std::size_t handle, double value) { + if (Stopping_.load(std::memory_order_acquire)) { + std::shared_ptr underlying; + { + std::lock_guard lock(HandlesMutex_); + if (handle < Histograms_.size()) { + underlying = Histograms_[handle].Underlying; + } + } + if (underlying) { + underlying->Record(value); + } + return; + } + TThreadState& state = AcquireThreadState(); + if (ShouldDropUpdate(state, 1)) { + ReportDroppedHistogram(1); + TriggerFlush(EFlushTrigger::Threshold); + return; + } + bool overThreshold = false; + { + std::lock_guard lock(state.Mutex); + if (state.HistogramSamples.size() <= handle) { + state.HistogramSamples.resize(handle + 1); + } + auto& bucket = state.HistogramSamples[handle]; + if (bucket.capacity() == 0 && Settings_.HistogramReserveSamples > 0) { + bucket.reserve(Settings_.HistogramReserveSamples); + } + bucket.push_back(value); + } + const auto pending = state.PendingOps.fetch_add(1, std::memory_order_relaxed) + 1; + if (Settings_.ThreadPendingThreshold != 0 + && pending >= Settings_.ThreadPendingThreshold) { + overThreshold = true; + } + if (overThreshold) { + TriggerFlush(EFlushTrigger::Threshold); + } + } + + void OnHistogramRecordMany(std::size_t handle, const std::vector& values) { + if (values.empty()) { + return; + } + if (Stopping_.load(std::memory_order_acquire)) { + std::shared_ptr underlying; + { + std::lock_guard lock(HandlesMutex_); + if (handle < Histograms_.size()) { + underlying = Histograms_[handle].Underlying; + } + } + if (underlying) { + underlying->RecordMany(values); + } + return; + } + TThreadState& state = AcquireThreadState(); + if (ShouldDropUpdate(state, values.size())) { + ReportDroppedHistogram(values.size()); + TriggerFlush(EFlushTrigger::Threshold); + return; + } + bool overThreshold = false; + { + std::lock_guard lock(state.Mutex); + if (state.HistogramSamples.size() <= handle) { + state.HistogramSamples.resize(handle + 1); + } + auto& bucket = state.HistogramSamples[handle]; + bucket.insert(bucket.end(), values.begin(), values.end()); + } + const auto pending = state.PendingOps.fetch_add(values.size(), std::memory_order_relaxed) + values.size(); + if (Settings_.ThreadPendingThreshold != 0 + && pending >= Settings_.ThreadPendingThreshold) { + overThreshold = true; + } + if (overThreshold) { + TriggerFlush(EFlushTrigger::Threshold); + } + } + + // -- Public flush -------------------------------------------------------- + + void Flush() { + FlushAll(EFlushTrigger::Manual); + } + +private: + struct TThreadLocalHolder { + std::shared_ptr State; + std::weak_ptr Owner; + + TThreadLocalHolder() = default; + TThreadLocalHolder(std::shared_ptr state, + std::weak_ptr owner) noexcept + : State(std::move(state)), Owner(std::move(owner)) {} + + TThreadLocalHolder(const TThreadLocalHolder&) = delete; + TThreadLocalHolder& operator=(const TThreadLocalHolder&) = delete; + + TThreadLocalHolder(TThreadLocalHolder&& other) noexcept + : State(std::move(other.State)), Owner(std::move(other.Owner)) {} + + TThreadLocalHolder& operator=(TThreadLocalHolder&& other) noexcept { + if (this != &other) { + if (State) { + State->Active.store(false, std::memory_order_release); + } + State = std::move(other.State); + Owner = std::move(other.Owner); + } + return *this; + } + + ~TThreadLocalHolder() { + if (State) { + State->Active.store(false, std::memory_order_release); + } + if (auto owner = Owner.lock()) { + owner->NudgeOnThreadExit(); + } + } + }; + + TThreadState& AcquireThreadState() { + thread_local std::vector>> + tlsTable; + + for (auto& kv : tlsTable) { + if (kv.first == this) { + return *kv.second; + } + } + + auto state = std::make_shared(); + { + std::lock_guard lock(ThreadsMutex_); + ThreadStates_.push_back(state); + } + tlsTable.emplace_back(this, state); + + thread_local std::vector holders; + holders.emplace_back(state, weak_from_this()); + + return *state; + } + + void NudgeOnThreadExit() noexcept { + std::lock_guard lock(WaitMutex_); + Wakeup_.notify_all(); + } + + bool ShouldDropUpdate(const TThreadState& state, std::size_t incomingOps) const noexcept { + if (Settings_.ThreadPendingLimit == 0) { + return false; + } + const std::size_t pending = state.PendingOps.load(std::memory_order_relaxed); + if (pending >= Settings_.ThreadPendingLimit) { + return true; + } + return incomingOps > (Settings_.ThreadPendingLimit - pending); + } + + void ReportDroppedCounter(std::uint64_t updates) noexcept { + if (DroppedCounterUpdates_ && updates != 0) { + try { + DroppedCounterUpdates_->Add(updates); + } catch (...) { + } + } + } + + void ReportDroppedHistogram(std::size_t updates) noexcept { + if (DroppedHistogramUpdates_ && updates != 0) { + try { + DroppedHistogramUpdates_->Add(static_cast(updates)); + } catch (...) { + } + } + } + + void TriggerFlush(EFlushTrigger trigger) noexcept { + ManualTrigger_.store(true, std::memory_order_release); + LastManualTrigger_.store(static_cast(trigger), std::memory_order_relaxed); + std::lock_guard lock(WaitMutex_); + Wakeup_.notify_all(); + } + + void Run() noexcept { + while (!Stopping_.load(std::memory_order_acquire)) { + EFlushTrigger trigger = EFlushTrigger::Interval; + { + std::unique_lock lock(WaitMutex_); + Wakeup_.wait_for(lock, Settings_.FlushInterval, [this]{ + return Stopping_.load(std::memory_order_acquire) + || ManualTrigger_.load(std::memory_order_acquire); + }); + if (Stopping_.load(std::memory_order_acquire)) { + break; + } + if (ManualTrigger_.exchange(false, std::memory_order_acq_rel)) { + trigger = static_cast( + LastManualTrigger_.load(std::memory_order_relaxed)); + } + } + try { + FlushAll(trigger); + } catch (...) { + } + } + } + + void FlushAll(EFlushTrigger trigger) { + const auto t0 = std::chrono::steady_clock::now(); + + std::vector> snapshot; + { + std::lock_guard lock(ThreadsMutex_); + snapshot = ThreadStates_; + if (!ThreadStates_.empty()) { + ThreadStates_.erase(std::remove_if(ThreadStates_.begin(), ThreadStates_.end(), + [](const std::shared_ptr& st) { + if (st->Active.load(std::memory_order_acquire)) { + return false; + } + std::lock_guard lk(st->Mutex); + for (std::uint64_t delta : st->CounterDeltas) { + if (delta != 0) { + return false; + } + } + for (const auto& samples : st->HistogramSamples) { + if (!samples.empty()) { + return false; + } + } + return true; + }), ThreadStates_.end()); + } + } + + std::vector> counters; + std::vector> histograms; + { + std::lock_guard lock(HandlesMutex_); + counters.reserve(Counters_.size()); + for (auto& c : Counters_) { + counters.push_back(c.Underlying); + } + histograms.reserve(Histograms_.size()); + for (auto& h : Histograms_) { + histograms.push_back(h.Underlying); + } + } + + std::vector totalCounter(counters.size(), 0); + std::vector> totalSamples(histograms.size()); + std::uint64_t totalEvents = 0; + std::uint64_t pendingCounters = 0; + std::uint64_t pendingHistogramSamples = 0; + + for (const auto& state : snapshot) { + std::lock_guard lock(state->Mutex); + for (std::size_t i = 0; i < state->CounterDeltas.size() && i < counters.size(); ++i) { + if (state->CounterDeltas[i] != 0) { + totalCounter[i] += state->CounterDeltas[i]; + totalEvents += state->CounterDeltas[i]; + pendingCounters += state->CounterDeltas[i]; + state->CounterDeltas[i] = 0; + } + } + for (std::size_t i = 0; i < state->HistogramSamples.size() && i < histograms.size(); ++i) { + auto& src = state->HistogramSamples[i]; + if (!src.empty()) { + auto& dst = totalSamples[i]; + pendingHistogramSamples += src.size(); + dst.insert(dst.end(), + std::make_move_iterator(src.begin()), + std::make_move_iterator(src.end())); + totalEvents += src.size(); + src.clear(); + } + } + std::size_t remainingOps = 0; + for (std::uint64_t delta : state->CounterDeltas) { + if (delta != 0) { + ++remainingOps; + } + } + for (const auto& samples : state->HistogramSamples) { + remainingOps += samples.size(); + } + state->PendingOps.store(remainingOps, std::memory_order_release); + } + + std::uint64_t addCalls = 0; + for (std::size_t i = 0; i < counters.size(); ++i) { + if (totalCounter[i] != 0 && counters[i]) { + try { + counters[i]->Add(totalCounter[i]); + ++addCalls; + } catch (...) { + } + } + } + std::uint64_t recordManyCalls = 0; + for (std::size_t i = 0; i < histograms.size(); ++i) { + if (!totalSamples[i].empty() && histograms[i]) { + try { + histograms[i]->RecordMany(totalSamples[i]); + ++recordManyCalls; + } catch (...) { + } + } + } + + const auto elapsed = std::chrono::duration( + std::chrono::steady_clock::now() - t0).count(); + + EmitSelfMetrics(trigger, totalEvents, addCalls, recordManyCalls, elapsed, + pendingCounters, pendingHistogramSamples); + } + + // -- Self-observability of the buffer ----------------------------------- + + void InitSelfMetrics() { + auto& reg = Settings_.SelfMetricsRegistry; + if (!reg) { + return; + } + FlushDurationHist_ = reg->Histogram( + kFlushDurationMetric, FlushDurationBuckets(), + {}, + "Wall-clock time spent in a single TMetricBuffer drain pass.", + "s"); + EventsBufferedCounter_ = reg->Counter( + kEventsBufferedTotal, {}, + "Total number of logical metric updates that passed through the buffer.", + "1"); + PendingCounterGauge_ = reg->Gauge( + kPendingUpdatesMetric, {{"instrument", "counter"}}, + "Pending updates aggregated across all threads at flush start.", + "1"); + PendingHistogramGauge_ = reg->Gauge( + kPendingUpdatesMetric, {{"instrument", "histogram"}}, + "Pending updates aggregated across all threads at flush start.", + "1"); + DroppedCounterUpdates_ = reg->Counter( + kDroppedUpdatesTotal, {{"instrument", "counter"}}, + "Total dropped metric updates due to ThreadPendingLimit overflow.", + "1"); + DroppedHistogramUpdates_ = reg->Counter( + kDroppedUpdatesTotal, {{"instrument", "histogram"}}, + "Total dropped metric updates due to ThreadPendingLimit overflow.", + "1"); + } + + std::shared_ptr FlushesTotal(EFlushTrigger trigger) { + auto& reg = Settings_.SelfMetricsRegistry; + if (!reg) { + return nullptr; + } + const char* trig = "interval"; + switch (trigger) { + case EFlushTrigger::Interval: trig = "interval"; break; + case EFlushTrigger::Threshold: trig = "threshold"; break; + case EFlushTrigger::Manual: trig = "manual"; break; + case EFlushTrigger::Shutdown: trig = "shutdown"; break; + } + TLabels labels = {{"trigger", trig}}; + return reg->Counter(kFlushesTotalMetric, labels, + "Total number of TMetricBuffer flush passes, by trigger.", "1"); + } + + std::shared_ptr UnderlyingCalls(const char* kind) { + auto& reg = Settings_.SelfMetricsRegistry; + if (!reg) { + return nullptr; + } + TLabels labels = {{"kind", kind}}; + return reg->Counter(kUnderlyingCallsTotal, labels, + "Total number of batched calls TMetricBuffer made into the underlying IMetricRegistry.", + "1"); + } + + void EmitSelfMetrics(EFlushTrigger trigger, + std::uint64_t totalEvents, + std::uint64_t addCalls, + std::uint64_t recordManyCalls, + double durationSeconds, + std::uint64_t pendingCounters, + std::uint64_t pendingHistogramSamples) { + auto safe = [](auto&& fn) noexcept { + try { + fn(); + } catch (...) { + } + }; + + if (FlushDurationHist_) { + safe([&]{ FlushDurationHist_->Record(durationSeconds); }); + } + if (auto c = FlushesTotal(trigger)) { + safe([&]{ c->Inc(); }); + } + if (EventsBufferedCounter_ && totalEvents != 0) { + safe([&]{ EventsBufferedCounter_->Add(totalEvents); }); + } + if (addCalls != 0) { + if (auto c = UnderlyingCalls("add")) { + safe([&]{ c->Add(addCalls); }); + } + } + if (recordManyCalls != 0) { + if (auto c = UnderlyingCalls("record_many")) { + safe([&]{ c->Add(recordManyCalls); }); + } + } + if (PendingCounterGauge_) { + safe([&]{ PendingCounterGauge_->Set(static_cast(pendingCounters)); }); + } + if (PendingHistogramGauge_) { + safe([&]{ PendingHistogramGauge_->Set(static_cast(pendingHistogramSamples)); }); + } + } + + std::shared_ptr Underlying_; + TMetricBufferSettings Settings_; + + mutable std::mutex HandlesMutex_; + std::vector Counters_; + std::vector Histograms_; + + mutable std::mutex ThreadsMutex_; + std::vector> ThreadStates_; + + std::mutex WaitMutex_; + std::condition_variable Wakeup_; + std::atomic Stopping_{false}; + std::atomic ManualTrigger_{false}; + std::atomic LastManualTrigger_{static_cast(EFlushTrigger::Manual)}; + + std::thread FlushThread_; + + std::shared_ptr FlushDurationHist_; + std::shared_ptr EventsBufferedCounter_; + std::shared_ptr DroppedCounterUpdates_; + std::shared_ptr DroppedHistogramUpdates_; + std::shared_ptr PendingCounterGauge_; + std::shared_ptr PendingHistogramGauge_; + +public: + const std::shared_ptr& Underlying() const { return Underlying_; } +}; + +class TBufferedCounter : public ICounter { +public: + TBufferedCounter(std::shared_ptr core, std::size_t id) + : Core_(std::move(core)), Id_(id) {} + + void Inc() override { + Core_->OnCounterAdd(Id_, 1); + } + void Add(std::uint64_t delta) override { + Core_->OnCounterAdd(Id_, delta); + } + +private: + std::shared_ptr Core_; + std::size_t Id_; +}; + +class TBufferedHistogram : public IHistogram { +public: + TBufferedHistogram(std::shared_ptr core, std::size_t id) + : Core_(std::move(core)), Id_(id) {} + + void Record(double value) override { + Core_->OnHistogramRecord(Id_, value); + } + void RecordMany(const std::vector& values) override { + Core_->OnHistogramRecordMany(Id_, values); + } + +private: + std::shared_ptr Core_; + std::size_t Id_; +}; + +class TPassthroughGauge : public IGauge { +public: + explicit TPassthroughGauge(std::shared_ptr underlying) + : Underlying_(std::move(underlying)) {} + + void Add(double delta) override { if (Underlying_) Underlying_->Add(delta); } + void Set(double value) override { if (Underlying_) Underlying_->Set(value); } + +private: + std::shared_ptr Underlying_; +}; + +class TBufferedMetricRegistry : public IMetricRegistry { +public: + TBufferedMetricRegistry(std::shared_ptr core) + : Core_(std::move(core)) {} + + ~TBufferedMetricRegistry() override { + if (Core_) { + Core_->Shutdown(); + } + } + + void FlushBufferedData() { + if (Core_) { + Core_->Flush(); + } + } + + std::shared_ptr Counter(const std::string& name, + const TLabels& labels, + const std::string& description, + const std::string& unit) override { + const auto key = MakeKey(name, labels); + std::lock_guard lock(WrappersLock_); + auto& slot = CounterWrappers_[key]; + if (!slot) { + auto underlying = Core_->Underlying()->Counter(name, labels, description, unit); + const auto id = Core_->RegisterCounter(underlying); + slot = std::make_shared(Core_, id); + } + return slot; + } + + std::shared_ptr Gauge(const std::string& name, + const TLabels& labels, + const std::string& description, + const std::string& unit) override { + const auto key = MakeKey(name, labels); + std::lock_guard lock(WrappersLock_); + auto& slot = GaugeWrappers_[key]; + if (!slot) { + auto underlying = Core_->Underlying()->Gauge(name, labels, description, unit); + slot = std::make_shared(std::move(underlying)); + } + return slot; + } + + std::shared_ptr Histogram(const std::string& name, + const std::vector& buckets, + const TLabels& labels, + const std::string& description, + const std::string& unit) override { + const auto key = MakeKey(name, labels); + std::lock_guard lock(WrappersLock_); + auto& slot = HistogramWrappers_[key]; + if (!slot) { + auto underlying = Core_->Underlying()->Histogram( + name, buckets, labels, description, unit); + const auto id = Core_->RegisterHistogram(underlying); + slot = std::make_shared(Core_, id); + } + return slot; + } + +private: + static std::string MakeKey(const std::string& name, const TLabels& labels) { + std::string key; + key.reserve(name.size() + labels.size() * 24); + key.append(name); + key.push_back('\x1f'); + for (const auto& [k, v] : labels) { + key.append(k); + key.push_back('\x1e'); + key.append(v); + key.push_back('\x1f'); + } + return key; + } + + std::shared_ptr Core_; + std::mutex WrappersLock_; + std::unordered_map> CounterWrappers_; + std::unordered_map> GaugeWrappers_; + std::unordered_map> HistogramWrappers_; +}; + +} // anonymous namespace + +std::shared_ptr CreateBufferedMetricRegistry( + std::shared_ptr underlying, + TMetricBufferSettings settings) +{ + if (!underlying) { + return nullptr; + } + auto core = std::make_shared(std::move(underlying), + std::move(settings)); + core->Start(); + return std::make_shared(std::move(core)); +} + +bool FlushBufferedMetricRegistry(const std::shared_ptr& registry) { + auto buffered = std::dynamic_pointer_cast(registry); + if (!buffered) { + return false; + } + buffered->FlushBufferedData(); + return true; +} + +} // namespace NYdb::NObservability diff --git a/src/client/impl/session/session_pool.cpp b/src/client/impl/session/session_pool.cpp index b5d0bdfb396..18a5979c75f 100644 --- a/src/client/impl/session/session_pool.cpp +++ b/src/client/impl/session/session_pool.cpp @@ -41,10 +41,16 @@ TDuration RandomizeThreshold(TDuration duration) { return TDuration::FromValue(value); } +namespace { + +static const std::string ServerHintsKey{NYdb::YDB_SERVER_HINTS}; + +} // namespace + bool IsSessionCloseRequested(const TStatus& status) { const auto& meta = status.GetResponseMetadata(); - auto hints = meta.equal_range(NYdb::YDB_SERVER_HINTS); - for(auto it = hints.first; it != hints.second; ++it) { + auto hints = meta.equal_range(ServerHintsKey); + for (auto it = hints.first; it != hints.second; ++it) { if (it->second == NYdb::YDB_SESSION_CLOSE) { return true; } diff --git a/src/client/persqueue_public/impl/read_session_messages.cpp b/src/client/persqueue_public/impl/read_session_messages.cpp index 4d7a82631d4..dcb38d1f9b8 100644 --- a/src/client/persqueue_public/impl/read_session_messages.cpp +++ b/src/client/persqueue_public/impl/read_session_messages.cpp @@ -8,11 +8,11 @@ namespace NYdb::inline V3::NPersQueue { TReadSessionEvent::TDataReceivedEvent::TMessageInformation::TMessageInformation( ui64 offset, - std::string messageGroupId, + std::string_view messageGroupId, ui64 seqNo, TInstant createTime, TInstant writeTime, - std::string ip, + std::string_view ip, TWriteSessionMeta::TPtr meta, ui64 uncompressedSize ) diff --git a/src/client/persqueue_public/include/read_events.h b/src/client/persqueue_public/include/read_events.h index 7152be32513..18a06b75441 100644 --- a/src/client/persqueue_public/include/read_events.h +++ b/src/client/persqueue_public/include/read_events.h @@ -71,11 +71,11 @@ struct TReadSessionEvent { struct TMessageInformation { TMessageInformation(ui64 offset, - std::string messageGroupId, + std::string_view messageGroupId, ui64 seqNo, TInstant createTime, TInstant writeTime, - std::string ip, + std::string_view ip, TWriteSessionMeta::TPtr meta, ui64 uncompressedSize); ui64 Offset; diff --git a/src/client/query/impl/client_session.h b/src/client/query/impl/client_session.h index d36d75c8a63..5bda823a0de 100644 --- a/src/client/query/impl/client_session.h +++ b/src/client/query/impl/client_session.h @@ -18,8 +18,8 @@ class TSession::TImpl : public TKqpSessionCommon { std::shared_ptr client, std::weak_ptr sessionClient) : Promise(promise) - , SessionId(sessionId) - , Endpoint(endpoint) + , SessionId(std::move(sessionId)) + , Endpoint(std::move(endpoint)) , Client(client) , SessionClient(sessionClient) { } diff --git a/src/client/table/table.cpp b/src/client/table/table.cpp index d7bb46d4ed3..63619124137 100644 --- a/src/client/table/table.cpp +++ b/src/client/table/table.cpp @@ -2460,7 +2460,7 @@ TIndexDescription::TIndexDescription( const std::vector& indexColumns, const std::vector& dataColumns, const std::vector& globalIndexSettings, - const std::variant& specializedIndexSettings + const std::variant& specializedIndexSettings ) : IndexName_(name) , IndexType_(type) , IndexColumns_(indexColumns) @@ -2501,7 +2501,7 @@ const std::vector& TIndexDescription::GetDataColumns() const { return DataColumns_; } -const std::variant& TIndexDescription::GetIndexSettings() const { +const std::variant& TIndexDescription::GetIndexSettings() const { return SpecializedIndexSettings_; } @@ -2810,6 +2810,46 @@ void TVectorIndexSettings::Out(IOutputStream& o) const { o << *this; } +TLocalBloomFilterSettings TLocalBloomFilterSettings::FromProto(const Ydb::Table::LocalBloomFilterIndex& proto) { + TLocalBloomFilterSettings settings; + if (proto.has_false_positive_probability()) { + settings.FalsePositiveProbability = proto.false_positive_probability(); + } + return settings; +} + +void TLocalBloomFilterSettings::SerializeTo(Ydb::Table::LocalBloomFilterIndex& proto) const { + if (FalsePositiveProbability) { + proto.set_false_positive_probability(*FalsePositiveProbability); + } +} + +TLocalBloomNgramFilterSettings TLocalBloomNgramFilterSettings::FromProto(const Ydb::Table::LocalBloomNgramFilterIndex& proto) { + TLocalBloomNgramFilterSettings settings; + if (proto.ngram_size() != 0) { + settings.NgramSize = proto.ngram_size(); + } + if (proto.has_case_sensitive()) { + settings.CaseSensitive = proto.case_sensitive(); + } + if (proto.has_false_positive_probability()) { + settings.FalsePositiveProbability = proto.false_positive_probability(); + } + return settings; +} + +void TLocalBloomNgramFilterSettings::SerializeTo(Ydb::Table::LocalBloomNgramFilterIndex& proto) const { + if (NgramSize) { + proto.set_ngram_size(*NgramSize); + } + if (CaseSensitive) { + proto.set_case_sensitive(*CaseSensitive); + } + if (FalsePositiveProbability) { + proto.set_false_positive_probability(*FalsePositiveProbability); + } +} + TKMeansTreeSettings TKMeansTreeSettings::FromProto(const Ydb::Table::KMeansTreeSettings& proto) { return { .Settings = TVectorIndexSettings::FromProto(proto.settings()), @@ -2987,7 +3027,7 @@ TIndexDescription TIndexDescription::FromProto(const TProto& proto) { std::vector indexColumns; std::vector dataColumns; std::vector globalIndexSettings; - std::variant specializedIndexSettings = std::monostate{}; + std::variant specializedIndexSettings = std::monostate{}; indexColumns.assign(proto.index_columns().begin(), proto.index_columns().end()); dataColumns.assign(proto.data_columns().begin(), proto.data_columns().end()); @@ -3047,7 +3087,19 @@ TIndexDescription TIndexDescription::FromProto(const TProto& proto) { type = EIndexType::GlobalJson; globalIndexSettings.emplace_back(TGlobalIndexSettings::FromProto(proto.global_json_index().settings())); break; - default: // fallback to global sync + case TProto::kLocalBloomFilterIndex: + type = EIndexType::LocalBloomFilter; + specializedIndexSettings = TLocalBloomFilterSettings::FromProto(proto.local_bloom_filter_index()); + break; + case TProto::kLocalBloomNgramFilterIndex: + type = EIndexType::LocalBloomNgramFilter; + specializedIndexSettings = TLocalBloomNgramFilterSettings::FromProto(proto.local_bloom_ngram_filter_index()); + break; + case TProto::kLocalMinMaxIndex: + type = EIndexType::LocalMinMax; + specializedIndexSettings = std::monostate{}; + break; + case TProto::TYPE_NOT_SET: type = EIndexType::GlobalSync; globalIndexSettings.resize(1); break; @@ -3149,6 +3201,24 @@ void TIndexDescription::SerializeTo(Ydb::Table::TableIndex& proto) const { } break; } + case EIndexType::LocalBloomFilter: { + auto* indexProto = proto.mutable_local_bloom_filter_index(); + if (const auto* settings = std::get_if(&SpecializedIndexSettings_)) { + settings->SerializeTo(*indexProto); + } + break; + } + case EIndexType::LocalBloomNgramFilter: { + auto* indexProto = proto.mutable_local_bloom_ngram_filter_index(); + if (const auto* settings = std::get_if(&SpecializedIndexSettings_)) { + settings->SerializeTo(*indexProto); + } + break; + } + case EIndexType::LocalMinMax: { + proto.mutable_local_min_max_index(); + break; + } case EIndexType::Unknown: break; } @@ -3175,6 +3245,9 @@ void TIndexDescription::Out(IOutputStream& o) const { case EIndexType::GlobalAsync: case EIndexType::GlobalUnique: case EIndexType::GlobalJson: + case EIndexType::LocalBloomFilter: + case EIndexType::LocalBloomNgramFilter: + case EIndexType::LocalMinMax: case EIndexType::Unknown: break; case EIndexType::GlobalVectorKMeansTree: diff --git a/src/client/topic/impl/common.h b/src/client/topic/impl/common.h index b429750aeaa..f3430c9020c 100644 --- a/src/client/topic/impl/common.h +++ b/src/client/topic/impl/common.h @@ -399,6 +399,8 @@ class TBaseSessionEventsQueue : public ISignalable { public: NThreading::TFuture WaitEvent() { + bool needSelfCheck = false; + NThreading::TFuture res; { std::lock_guard guard (Mutex); if (HasEventsImpl()) { @@ -406,14 +408,17 @@ class TBaseSessionEventsQueue : public ISignalable { } else { if (const auto now = TInstant::Now(); now - LastSelfCheckAt > TDuration::Seconds(10)) { LastSelfCheckAt = now; - SelfCheck(); + needSelfCheck = true; } Y_ABORT_UNLESS(Waiter.Valid()); - auto res = Waiter.GetFuture(); - return res; + res = Waiter.GetFuture(); } } + if (needSelfCheck) { + SelfCheck(); + } + return res; } bool IsClosed() { diff --git a/src/client/topic/impl/direct_reader.cpp b/src/client/topic/impl/direct_reader.cpp index 8db1fd20515..4cd2388ea56 100644 --- a/src/client/topic/impl/direct_reader.cpp +++ b/src/client/topic/impl/direct_reader.cpp @@ -159,7 +159,7 @@ TDirectReadSessionManager::TDirectReadSessionManager( TLog log ) : ReadSessionSettings(settings) - , ServerSessionId(serverSessionId) + , ServerSessionId(std::move(serverSessionId)) , ClientContext(clientContext) , ProcessorFactory(processorFactory) , ControlCallbacks(controlCallbacks) @@ -332,7 +332,7 @@ TDirectReadSession::TDirectReadSession( ) : ClientContext(clientContext) , ReadSessionSettings(settings) - , ServerSessionId(serverSessionId) + , ServerSessionId(std::move(serverSessionId)) , ProcessorFactory(processorFactory) , NodeId(nodeId) , IncomingMessagesForControlSession(std::make_shared>()) diff --git a/src/client/topic/impl/read_session_event.cpp b/src/client/topic/impl/read_session_event.cpp index c832ce1b023..7c1871728ac 100644 --- a/src/client/topic/impl/read_session_event.cpp +++ b/src/client/topic/impl/read_session_event.cpp @@ -37,7 +37,7 @@ std::pair GetMessageOffsetRange(const TDataReceivedEvent& da TMessageInformation::TMessageInformation( uint64_t offset, - std::string producerId, + std::string_view producerId, uint64_t seqNo, TInstant createTime, TInstant writeTime, @@ -47,7 +47,7 @@ TMessageInformation::TMessageInformation( std::string messageGroupId ) : Offset(offset) - , ProducerId(std::move(producerId)) + , ProducerId(producerId) , SeqNo(seqNo) , CreateTime(createTime) , WriteTime(writeTime) diff --git a/src/client/topic/impl/read_session_impl.ipp b/src/client/topic/impl/read_session_impl.ipp index 6c8bf8eafab..8a3f7b0eb45 100644 --- a/src/client/topic/impl/read_session_impl.ipp +++ b/src/client/topic/impl/read_session_impl.ipp @@ -2198,6 +2198,7 @@ bool TSingleClusterReadSessionImpl::AllParentSessionsHasBe template void TSingleClusterReadSessionImpl::SelfCheck() { + std::lock_guard guard(Lock); const auto delta = TInstant::Now() - LastActiveTime; if (delta < TDuration::Seconds(30)) { // Session ok, we got at least one event from server since last 1 minute diff --git a/src/client/topic/impl/topic.cpp b/src/client/topic/impl/topic.cpp index dafbccdf9e0..c22116dcf84 100644 --- a/src/client/topic/impl/topic.cpp +++ b/src/client/topic/impl/topic.cpp @@ -52,6 +52,8 @@ TTopicDescription::TTopicDescription(Ydb::Topic::DescribeTopicResult&& result) , RetentionStorageMb_(Proto_.retention_storage_mb() > 0 ? std::optional(Proto_.retention_storage_mb()) : std::nullopt) , PartitionWriteSpeedBytesPerSecond_(Proto_.partition_write_speed_bytes_per_second()) , PartitionWriteBurstBytes_(Proto_.partition_write_burst_bytes()) + , PartitionWriteSpeedMessagesPerSecond_(Proto_.partition_write_speed_messages_per_second()) + , PartitionWriteBurstMessages_(Proto_.partition_write_burst_messages()) , MeteringMode_(TProtoAccessor::FromProto(Proto_.metering_mode())) , TopicStats_(Proto_.topic_stats()) , MetricsLevel_(Proto_.has_metrics_level() ? std::optional(static_cast(Proto_.metrics_level())) : std::optional()) @@ -211,6 +213,14 @@ uint64_t TTopicDescription::GetPartitionWriteBurstBytes() const { return PartitionWriteBurstBytes_; } +uint64_t TTopicDescription::GetPartitionWriteSpeedMessagesPerSecond() const { + return PartitionWriteSpeedMessagesPerSecond_; +} + +uint64_t TTopicDescription::GetPartitionWriteBurstMessages() const { + return PartitionWriteBurstMessages_; +} + EMeteringMode TTopicDescription::GetMeteringMode() const { return MeteringMode_; } @@ -892,6 +902,8 @@ TCreateTopicSettings::TCreateTopicSettings(const Ydb::Topic::CreateTopicRequest& , MeteringMode_(TProtoAccessor::FromProto(proto.metering_mode())) , PartitionWriteSpeedBytesPerSecond_(proto.partition_write_speed_bytes_per_second()) , PartitionWriteBurstBytes_(proto.partition_write_burst_bytes()) + , PartitionWriteSpeedMessagesPerSecond_(proto.partition_write_speed_messages_per_second()) + , PartitionWriteBurstMessages_(proto.partition_write_burst_messages()) , Attributes_(DeserializeAttributes(proto.attributes())) , MetricsLevel_(proto.has_metrics_level() ? std::optional(static_cast(proto.metrics_level())) : std::nullopt) { @@ -906,6 +918,8 @@ void TCreateTopicSettings::SerializeTo(Ydb::Topic::CreateTopicRequest& request) request.set_metering_mode(TProtoAccessor::GetProto(MeteringMode_)); request.set_partition_write_speed_bytes_per_second(PartitionWriteSpeedBytesPerSecond_); request.set_partition_write_burst_bytes(PartitionWriteBurstBytes_); + request.set_partition_write_speed_messages_per_second(PartitionWriteSpeedMessagesPerSecond_); + request.set_partition_write_burst_messages(PartitionWriteBurstMessages_); *request.mutable_consumers() = SerializeConsumers(Consumers_); *request.mutable_attributes() = SerializeAttributes(Attributes_); if (MetricsLevel_) { diff --git a/src/client/topic/impl/topic_impl.h b/src/client/topic/impl/topic_impl.h index cc4c5ecc6da..406b9c5901c 100644 --- a/src/client/topic/impl/topic_impl.h +++ b/src/client/topic/impl/topic_impl.h @@ -103,6 +103,12 @@ class TTopicClient::TImpl : public TClientImplCommon { if (settings.SetPartitionWriteBurstBytes_) { request.set_set_partition_write_burst_bytes(*settings.SetPartitionWriteBurstBytes_); } + if (settings.SetPartitionWriteSpeedMessagesPerSecond_) { + request.set_set_partition_write_speed_messages_per_second(*settings.SetPartitionWriteSpeedMessagesPerSecond_); + } + if (settings.SetPartitionWriteBurstMessages_) { + request.set_set_partition_write_burst_messages(*settings.SetPartitionWriteBurstMessages_); + } if (settings.SetRetentionStorageMb_) { request.set_set_retention_storage_mb(*settings.SetRetentionStorageMb_); } diff --git a/src/client/topic/ut/ut_utils/topic_sdk_test_setup.cpp b/src/client/topic/ut/ut_utils/topic_sdk_test_setup.cpp index 71608ae13ae..890356e19f0 100644 --- a/src/client/topic/ut/ut_utils/topic_sdk_test_setup.cpp +++ b/src/client/topic/ut/ut_utils/topic_sdk_test_setup.cpp @@ -67,13 +67,15 @@ TConsumerDescription TTopicSdkTestSetup::DescribeConsumer(const std::string& nam void TTopicSdkTestSetup::Write(const std::string& message, std::uint32_t partitionId, const std::optional producer, - std::optional seqNo) { - Write(GetTopicPath(), message, partitionId, producer, seqNo); + std::optional seqNo, + NYdb::NTopic::ECodec codec) { + Write(GetTopicPath(), message, partitionId, producer, seqNo, codec); } void TTopicSdkTestSetup::Write(const std::string& topic, const std::string& message, std::uint32_t partitionId, const std::optional producer, - std::optional seqNo) { + std::optional seqNo, + NYdb::NTopic::ECodec codec) { TTopicClient client(*Driver); TWriteSessionSettings settings; @@ -84,6 +86,7 @@ void TTopicSdkTestSetup::Write(const std::string& topic, const std::string& mess settings.ProducerId(producer.value()) .MessageGroupId(producer.value()); } + settings.Codec(codec); auto session = client.CreateSimpleBlockingWriteSession(settings); UNIT_ASSERT(session->Write(message, seqNo)); diff --git a/src/client/topic/ut/ut_utils/topic_sdk_test_setup.h b/src/client/topic/ut/ut_utils/topic_sdk_test_setup.h index e4381554e10..7d70cb3948c 100644 --- a/src/client/topic/ut/ut_utils/topic_sdk_test_setup.h +++ b/src/client/topic/ut/ut_utils/topic_sdk_test_setup.h @@ -32,11 +32,13 @@ class TTopicSdkTestSetup : public ITopicTestSetup { void Write(const std::string& message, std::uint32_t partitionId = 0, const std::optional producer = std::nullopt, - std::optional seqNo = std::nullopt); + std::optional seqNo = std::nullopt, + NYdb::NTopic::ECodec codec = NYdb::NTopic::ECodec::RAW); void Write(const std::string& topic, const std::string& message, std::uint32_t partitionId = 0, const std::optional producer = std::nullopt, - std::optional seqNo = std::nullopt); + std::optional seqNo = std::nullopt, + NYdb::NTopic::ECodec codec = NYdb::NTopic::ECodec::RAW); struct TReadResult { TReadResult(TDriver& driver); diff --git a/src/client/types/core_facility/simple_core_facility.cpp b/src/client/types/core_facility/simple_core_facility.cpp index a54e41871ef..4356582b2a1 100644 --- a/src/client/types/core_facility/simple_core_facility.cpp +++ b/src/client/types/core_facility/simple_core_facility.cpp @@ -1,5 +1,7 @@ #include "simple_core_facility.h" +#include +#include #include namespace NYdb::inline V3 { @@ -35,23 +37,24 @@ void TSimpleCoreFacility::AddPeriodicTask(TPeriodicCb&& cb, TDeadline::Duration EnqueueTaskNoLock( TClock::now(), [this, periodicCb, period] { - NYdb::NIssue::TIssues issues; - const bool cont = (*periodicCb)(std::move(issues), EStatus::SUCCESS); - if (cont) { - SchedulePeriodic(periodicCb, period); - } + RunPeriodicTask(periodicCb, period); }); Cv_.notify_one(); } void TSimpleCoreFacility::PostToResponseQueue(TPostTaskCb&& f) { - std::lock_guard lock(Mutex_); - if (Stop_) { + if (!f) { return; } - EnqueueTaskNoLock(TClock::now(), std::move(f)); - - Cv_.notify_one(); + { + std::lock_guard lock(Mutex_); + if (!Stop_) { + EnqueueTaskNoLock(TClock::now(), std::move(f)); + Cv_.notify_one(); + return; + } + } + f(); } void TSimpleCoreFacility::EnqueueTaskNoLock(TTimePoint executeAt, TPostTaskCb&& task) { @@ -62,6 +65,20 @@ void TSimpleCoreFacility::EnqueueTaskNoLock(TTimePoint executeAt, TPostTaskCb&& }); } +void TSimpleCoreFacility::RunPeriodicTask(std::shared_ptr periodicCb, TDeadline::Duration period) { + bool cont = false; + try { + NYdb::NIssue::TIssues issues; + cont = (*periodicCb)(std::move(issues), EStatus::SUCCESS); + } catch (...) { + Cerr << "TSimpleCoreFacility periodic task failed: " << CurrentExceptionMessage() << Endl; + cont = true; + } + if (cont) { + SchedulePeriodic(periodicCb, period); + } +} + void TSimpleCoreFacility::SchedulePeriodic(std::shared_ptr periodicCb, TDeadline::Duration period) { std::lock_guard lock(Mutex_); if (Stop_) { @@ -70,11 +87,7 @@ void TSimpleCoreFacility::SchedulePeriodic(std::shared_ptr periodic EnqueueTaskNoLock( TClock::now() + period, [this, periodicCb, period] { - NYdb::NIssue::TIssues issues; - const bool cont = (*periodicCb)(std::move(issues), EStatus::SUCCESS); - if (cont) { - SchedulePeriodic(periodicCb, period); - } + RunPeriodicTask(periodicCb, period); }); Cv_.notify_one(); @@ -117,7 +130,11 @@ void TSimpleCoreFacility::WorkerLoop() { for (auto& task : ready) { if (task) { - task(); + try { + task(); + } catch (...) { + Cerr << "TSimpleCoreFacility task failed: " << CurrentExceptionMessage() << Endl; + } } } } diff --git a/src/client/types/core_facility/simple_core_facility.h b/src/client/types/core_facility/simple_core_facility.h index 371440be744..5444774dfc6 100644 --- a/src/client/types/core_facility/simple_core_facility.h +++ b/src/client/types/core_facility/simple_core_facility.h @@ -42,6 +42,7 @@ class TSimpleCoreFacility final : public ICoreFacility { }; void EnqueueTaskNoLock(TTimePoint executeAt, TPostTaskCb&& task); + void RunPeriodicTask(std::shared_ptr periodicCb, TDeadline::Duration period); void SchedulePeriodic(std::shared_ptr periodicCb, TDeadline::Duration period); std::optional DrainReadyTasks(std::vector& ready); void WorkerLoop(); diff --git a/src/client/types/status/status.cpp b/src/client/types/status/status.cpp index 319113ce84c..16ac9da9062 100644 --- a/src/client/types/status/status.cpp +++ b/src/client/types/status/status.cpp @@ -104,7 +104,6 @@ bool TStreamPartStatus::EOS() const { //////////////////////////////////////////////////////////////////////////////// namespace { - template bool StatusContainsIssueIf(const TStatus& status, TIssuePredicate&& pred) { for (const auto& top : status.GetIssues()) { @@ -121,7 +120,6 @@ bool StatusContainsIssueIf(const TStatus& status, TIssuePredicate&& pred) { } return false; } - } // anonymous namespace namespace NStatusHelpers { diff --git a/src/client/value/value.cpp b/src/client/value/value.cpp index 31359e825d1..934aaeb9a32 100644 --- a/src/client/value/value.cpp +++ b/src/client/value/value.cpp @@ -324,7 +324,7 @@ class TTypeParser::TImpl { NYdb::CheckKind(GetKind(), kind, method); } - void CheckPreviousKind(ETypeKind kind, const std::string method) const { + void CheckPreviousKind(ETypeKind kind, const std::string& method) const { if (Path_.size() < 2) { FatalError("Expected container type."); return; diff --git a/src/library/issue/yql_issue.cpp b/src/library/issue/yql_issue.cpp index 7057f488934..82e33597294 100644 --- a/src/library/issue/yql_issue.cpp +++ b/src/library/issue/yql_issue.cpp @@ -182,7 +182,7 @@ void ProgramLinesWithErrors( } } -} // namspace +} // namespace void TIssues::PrintTo(IOutputStream& out, bool oneLine) const { diff --git a/src/library/operation_id/operation_id.cpp b/src/library/operation_id/operation_id.cpp index b9c0f56d484..f2f0e4f6280 100644 --- a/src/library/operation_id/operation_id.cpp +++ b/src/library/operation_id/operation_id.cpp @@ -16,6 +16,7 @@ namespace NOperationId { using namespace NUri; static const std::string QueryIdPrefix = "ydb://preparedqueryid/4?id="; +static const std::string KindKey = "kind"; std::string FormatPreparedQueryIdCompat(const std::string& in) { return QueryIdPrefix + in; @@ -75,6 +76,9 @@ std::string ProtoToString(const Ydb::TOperationId& proto) { case Ydb::TOperationId::COMPACTION: res << "ydb://compaction"; break; + case Ydb::TOperationId::FULL_BACKUP: + res << "ydb://fullbackup"; + break; default: Y_ABORT_UNLESS(false, "unexpected kind"); } @@ -198,13 +202,13 @@ class TOperationId::TImpl { } std::string GetSubKind() const { - auto it = Index.find("kind"); + auto it = Index.find(KindKey); if (it == Index.end()) { return std::string(); } if (it->second.size() != 1) { - ythrow yexception() << "Unable to retreive sub-kind"; + ythrow yexception() << "Unable to retrieve sub-kind"; } return *it->second.at(0); @@ -330,6 +334,10 @@ TOperationId::EKind ParseKind(const std::string_view value) { return TOperationId::COMPACTION; } + if (value.starts_with("fullbackup")) { + return TOperationId::FULL_BACKUP; + } + return TOperationId::UNUSED; } diff --git a/src/library/operation_id/protos/operation_id.proto b/src/library/operation_id/protos/operation_id.proto index c85d5645b94..f9c64e04e5b 100644 --- a/src/library/operation_id/protos/operation_id.proto +++ b/src/library/operation_id/protos/operation_id.proto @@ -18,6 +18,7 @@ message TOperationId { INCREMENTAL_BACKUP = 11; RESTORE = 12; COMPACTION = 13; + FULL_BACKUP = 14; } message TData { diff --git a/src/version.h b/src/version.h index ffff92e1131..be835d99f57 100644 --- a/src/version.h +++ b/src/version.h @@ -2,7 +2,7 @@ namespace NYdb { -inline const char* YDB_SDK_VERSION = "3.18.0"; +inline const char* YDB_SDK_VERSION = "3.19.0"; inline const char* YDB_CERTIFICATE_FILE_KEY = "ydb_root_ca_v3.pem"; } // namespace NYdb diff --git a/tests/common/fake_metric_registry.h b/tests/common/fake_metric_registry.h index 82a9b70a342..f706cd0fd5e 100644 --- a/tests/common/fake_metric_registry.h +++ b/tests/common/fake_metric_registry.h @@ -13,14 +13,33 @@ class TFakeCounter : public NMetrics::ICounter { public: void Inc() override { Count_.fetch_add(1, std::memory_order_relaxed); + IncCalls_.fetch_add(1, std::memory_order_relaxed); + } + + void Add(std::uint64_t delta) override { + if (delta == 0) { + return; + } + Count_.fetch_add(static_cast(delta), std::memory_order_relaxed); + AddCalls_.fetch_add(1, std::memory_order_relaxed); } int64_t Get() const { return Count_.load(std::memory_order_relaxed); } + std::uint64_t IncCalls() const { + return IncCalls_.load(std::memory_order_relaxed); + } + + std::uint64_t AddCalls() const { + return AddCalls_.load(std::memory_order_relaxed); + } + private: std::atomic Count_{0}; + std::atomic IncCalls_{0}; + std::atomic AddCalls_{0}; }; class TFakeHistogram : public NMetrics::IHistogram { @@ -28,6 +47,16 @@ class TFakeHistogram : public NMetrics::IHistogram { void Record(double value) override { std::lock_guard lock(Mutex_); Values_.push_back(value); + ++RecordCalls_; + } + + void RecordMany(const std::vector& values) override { + if (values.empty()) { + return; + } + std::lock_guard lock(Mutex_); + Values_.insert(Values_.end(), values.begin(), values.end()); + ++RecordManyCalls_; } std::vector GetValues() const { @@ -40,9 +69,21 @@ class TFakeHistogram : public NMetrics::IHistogram { return Values_.size(); } + std::uint64_t RecordCalls() const { + std::lock_guard lock(Mutex_); + return RecordCalls_; + } + + std::uint64_t RecordManyCalls() const { + std::lock_guard lock(Mutex_); + return RecordManyCalls_; + } + private: mutable std::mutex Mutex_; std::vector Values_; + std::uint64_t RecordCalls_ = 0; + std::uint64_t RecordManyCalls_ = 0; }; class TFakeGauge : public NMetrics::IGauge { diff --git a/tests/integration/key_conflict/main.cpp b/tests/integration/key_conflict/main.cpp index c22666ace1a..f3d1c934413 100644 --- a/tests/integration/key_conflict/main.cpp +++ b/tests/integration/key_conflict/main.cpp @@ -2,11 +2,8 @@ #include #include #include - #include - #include - #include using namespace NYdb; @@ -32,6 +29,7 @@ TRunArgs GetRunArgs() { TDriver driver(driverConfig); std::string tablePath = std::string(database) + "/" + testRoot + "/pk_conflict_it"; + return {std::move(driver), std::move(tablePath)}; } @@ -55,6 +53,7 @@ TStatus CreatePkTable(TTableClient& client, const std::string& tablePath) { .AddNullableColumn("payload", EPrimitiveType::Utf8) .SetPrimaryKeyColumn("id") .Build(); + return session.CreateTable(tablePath, std::move(desc)).ExtractValueSync(); }); } @@ -67,10 +66,8 @@ TStatus InsertRow(TSession& session, const std::string& tablePath, uint64_t id, const auto q = std::format(R"( --!syntax_v1 PRAGMA TablePathPrefix("{}"); - DECLARE $id AS Uint64; DECLARE $payload AS Utf8; - INSERT INTO {} (id, payload) VALUES ($id, $payload); )", parent, tableName); @@ -97,6 +94,7 @@ TEST(KeyConflict, DuplicatePrimaryKeyInsertIsDetectableViaIssueCode) { ASSERT_NE(std::getenv("YDB_TEST_ROOT"), nullptr); auto [driver, tablePath] = GetRunArgs(); + TTableClient client(driver); DropTableIfExists(client, tablePath); @@ -119,4 +117,4 @@ TEST(KeyConflict, DuplicatePrimaryKeyInsertIsDetectableViaIssueCode) { DropTableIfExists(client, tablePath); driver.Stop(true); -} +} \ No newline at end of file diff --git a/tests/unit/client/iam/grpc_iam_ut.cpp b/tests/unit/client/iam/grpc_iam_ut.cpp index 553ddf91398..a3c51f96194 100644 --- a/tests/unit/client/iam/grpc_iam_ut.cpp +++ b/tests/unit/client/iam/grpc_iam_ut.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -153,3 +154,261 @@ TEST(GrpcIamCredentialsProvider, TeardownWhileIamCreatePendingCompletes) { iamService.Release(); server.Stop(); } + +TEST(GrpcIamCredentialsProvider, TeardownWhileIamCreatePendingCompletesViaFactoryWrapper) { + TBlockingIamTokenService iamService; + TIamGrpcServer server(&iamService); + ASSERT_TRUE(server.Start()); + + TIamOAuth params; + params.Endpoint = server.Endpoint(); + params.OAuthToken = "unit-test-oauth-token"; + params.EnableSsl = false; + params.RefreshPeriod = TDuration::Hours(1); + params.RequestTimeout = TDuration::MilliSeconds(400); + + auto factory = std::make_shared>(params); + + auto work = [&factory] { + auto provider = factory->CreateProvider(); + (void)provider; + }; + + std::future done = std::async(std::launch::async, work); + + ASSERT_TRUE(iamService.WaitUntilRpcEntered(std::chrono::seconds(5))) + << "server should have accepted the IAM Create call"; + + ASSERT_EQ(done.wait_for(std::chrono::seconds(20)), std::future_status::ready) + << "factory wrapper teardown must finish while an IAM Create is still blocked on the server"; + done.get(); + + iamService.Release(); + server.Stop(); +} + +// Regression test for backward compatibility of the deprecated no-arg +// ICredentialsProviderFactory::CreateProvider() entry point on IAM factories. After commit +// 0140ad8 ("fix sdk: fixed self thread join in iam cred provider") this entry point was +// throwing "Not supported" and broke all out-of-tree callers that didn't yet plumb an +// ICoreFacility. The factory must spin up a private facility transparently. +TEST(GrpcIamCredentialsProvider, NoArgCreateProviderBackwardCompat) { + TBlockingIamTokenService iamService; + TIamGrpcServer server(&iamService); + ASSERT_TRUE(server.Start()); + + iamService.Release(); + + TIamOAuth params; + params.Endpoint = server.Endpoint(); + params.OAuthToken = "unit-test-oauth-token"; + params.EnableSsl = false; + params.RefreshPeriod = TDuration::Hours(1); + params.RequestTimeout = TDuration::Seconds(5); + + auto factory = std::make_shared>(params); + + auto work = [&factory]() -> std::string { + auto provider = factory->CreateProvider(); + return provider->GetAuthInfo(); + }; + + std::future done = std::async(std::launch::async, work); + ASSERT_EQ(done.wait_for(std::chrono::seconds(20)), std::future_status::ready) + << "no-arg CreateProvider() path must produce a token and tear down cleanly"; + EXPECT_EQ(done.get(), "released-token"); + + server.Stop(); +} + +namespace { + +// PS256-compatible RSA test keypair (also used in jwt_token_source_ut.cpp). +constexpr const char* TestRSAPrivateKey = + "-----BEGIN PRIVATE KEY-----\n" + "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC75/JS3rMcLJxv\n" + "FgpOzF5+2gH+Yig3RE2MTl9uwC0BZKAv6foYr7xywQyWIK+W1cBhz8R4LfFmZo2j\n" + "M0aCvdRmNBdW0EDSTnHLxCsFhoQWLVq+bI5f5jzkcoiioUtaEpADPqwgVULVtN/n\n" + "nPJiZ6/dU30C3jmR6+LUgEntUtWt3eq3xQIn5lG3zC1klBY/HxtfH5Hu8xBvwRQT\n" + "Jnh3UpPLj8XwSmriDgdrhR7o6umWyVuGrMKlLHmeivlfzjYtfzO1MOIMG8t2/zxG\n" + "R+xb4Vwks73sH1KruH/0/JMXU97npwpe+Um+uXhpldPygGErEia7abyZB2gMpXqr\n" + "WYKMo02NAgMBAAECggEAO0BpC5OYw/4XN/optu4/r91bupTGHKNHlsIR2rDzoBhU\n" + "YLd1evpTQJY6O07EP5pYZx9mUwUdtU4KRJeDGO/1/WJYp7HUdtxwirHpZP0lQn77\n" + "uccuX/QQaHLrPekBgz4ONk+5ZBqukAfQgM7fKYOLk41jgpeDbM2Ggb6QUSsJISEp\n" + "zrwpI/nNT/wn+Hvx4DxrzWU6wF+P8kl77UwPYlTA7GsT+T7eKGVH8xsxmK8pt6lg\n" + "svlBA5XosWBWUCGLgcBkAY5e4ZWbkdd183o+oMo78id6C+PQPE66PLDtHWfpRRmN\n" + "m6XC03x6NVhnfvfozoWnmS4+e4qj4F/emCHvn0GMywKBgQDLXlj7YPFVXxZpUvg/\n" + "rheVcCTGbNmQJ+4cZXx87huqwqKgkmtOyeWsRc7zYInYgraDrtCuDBCfP//ZzOh0\n" + "LxepYLTPk5eNn/GT+VVrqsy35Ccr60g7Lp/bzb1WxyhcLbo0KX7/6jl0lP+VKtdv\n" + "mto+4mbSBXSM1Y5BVVoVgJ3T/wKBgQDsiSvPRzVi5TTj13x67PFymTMx3HCe2WzH\n" + "JUyepCmVhTm482zW95pv6raDr5CTO6OYpHtc5sTTRhVYEZoEYFTM9Vw8faBtluWG\n" + "BjkRh4cIpoIARMn74YZKj0C/0vdX7SHdyBOU3bgRPHg08Hwu3xReqT1kEPSI/B2V\n" + "4pe5fVrucwKBgQCNFgUxUA3dJjyMES18MDDYUZaRug4tfiYouRdmLGIxUxozv6CG\n" + "ZnbZzwxFt+GpvPUV4f+P33rgoCvFU+yoPctyjE6j+0aW0DFucPmb2kBwCu5J/856\n" + "kFwCx3blbwFHAco+SdN7g2kcwgmV2MTg/lMOcU7XwUUcN0Obe7UlWbckzQKBgQDQ\n" + "nXaXHL24GGFaZe4y2JFmujmNy1dEsoye44W9ERpf9h1fwsoGmmCKPp90az5+rIXw\n" + "FXl8CUgk8lXW08db/r4r+ma8Lyx0GzcZyplAnaB5/6j+pazjSxfO4KOBy4Y89Tb+\n" + "TP0AOcCi6ws13bgY+sUTa/5qKA4UVw+c5zlb7nRpgwKBgGXAXhenFw1666482iiN\n" + "cHSgwc4ZHa1oL6aNJR1XWH+aboBSwR+feKHUPeT4jHgzRGo/aCNHD2FE5I8eBv33\n" + "of1kWYjAO0YdzeKrW0rTwfvt9gGg+CS397aWu4cy+mTI+MNfBgeDAIVBeJOJXLlX\n" + "hL8bFAuNNVrCOp79TNnNIsh7\n" + "-----END PRIVATE KEY-----\n"; + +constexpr const char* TestRSAPublicKey = + "-----BEGIN PUBLIC KEY-----\n" + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu+fyUt6zHCycbxYKTsxe\n" + "ftoB/mIoN0RNjE5fbsAtAWSgL+n6GK+8csEMliCvltXAYc/EeC3xZmaNozNGgr3U\n" + "ZjQXVtBA0k5xy8QrBYaEFi1avmyOX+Y85HKIoqFLWhKQAz6sIFVC1bTf55zyYmev\n" + "3VN9At45kevi1IBJ7VLVrd3qt8UCJ+ZRt8wtZJQWPx8bXx+R7vMQb8EUEyZ4d1KT\n" + "y4/F8Epq4g4Ha4Ue6OrplslbhqzCpSx5nor5X842LX8ztTDiDBvLdv88RkfsW+Fc\n" + "JLO97B9Sq7h/9PyTF1Pe56cKXvlJvrl4aZXT8oBhKxImu2m8mQdoDKV6q1mCjKNN\n" + "jQIDAQAB\n" + "-----END PUBLIC KEY-----\n"; + +} // namespace + +// Regression test for the deprecated no-arg CreateProvider() on the JWT factory. Mirrors the +// OAuth counterpart above. The standalone path spins up a private TSimpleCoreFacility behind +// TOwningFacilityCredentialsProvider; this verifies the JWT specialization wires it up +// correctly and tears down without aborting. +TEST(GrpcIamCredentialsProvider, NoArgCreateProviderBackwardCompatJwt) { + TBlockingIamTokenService iamService; + TIamGrpcServer server(&iamService); + ASSERT_TRUE(server.Start()); + + iamService.Release(); + + TIamJwtParams params; + params.Endpoint = server.Endpoint(); + params.EnableSsl = false; + params.RefreshPeriod = TDuration::Hours(1); + params.RequestTimeout = TDuration::Seconds(5); + params.JwtParams.AccountId = "unit-test-account"; + params.JwtParams.KeyId = "unit-test-key"; + params.JwtParams.PrivKey = TestRSAPrivateKey; + params.JwtParams.PubKey = TestRSAPublicKey; + + auto factory = std::make_shared>(params); + + auto work = [&factory]() -> std::string { + auto provider = factory->CreateProvider(); + return provider->GetAuthInfo(); + }; + + std::future done = std::async(std::launch::async, work); + ASSERT_EQ(done.wait_for(std::chrono::seconds(20)), std::future_status::ready) + << "no-arg CreateProvider() path must produce a token and tear down cleanly"; + EXPECT_EQ(done.get(), "released-token"); + + server.Stop(); +} + +namespace { + +class TSlowBlockingAuthProvider final : public ICredentialsProvider { +public: + std::string GetAuthInfo() const override { + std::unique_lock lock(Mutex_); + if (++CallCount_ > 1) { + BlockedCv_.wait(lock, [this] { return Released_; }); + } + return "slow-auth-token"; + } + + bool IsValid() const override { + return true; + } + + void Release() { + { + std::lock_guard lock(Mutex_); + Released_ = true; + } + BlockedCv_.notify_all(); + } + + bool WaitUntilBlocked(std::chrono::milliseconds timeout) const { + const auto deadline = std::chrono::steady_clock::now() + timeout; + while (std::chrono::steady_clock::now() < deadline) { + std::lock_guard lock(Mutex_); + if (CallCount_ > 1 && !Released_) { + return true; + } + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + } + std::lock_guard lock(Mutex_); + return CallCount_ > 1 && !Released_; + } + +private: + mutable std::mutex Mutex_; + mutable std::condition_variable BlockedCv_; + mutable int CallCount_ = 0; + bool Released_ = false; +}; + +} // namespace + +// Regression: Stop() must not hang when FillContext is blocked inside AuthTokenProvider_->GetAuthInfo(). +TEST(GrpcIamCredentialsProvider, StopDuringFillContextDoesNotHang) { + TBlockingIamTokenService iamService; + TIamGrpcServer server(&iamService); + ASSERT_TRUE(server.Start()); + + iamService.Release(); + + auto authProvider = std::make_shared(); + + TIamOAuth params; + params.Endpoint = server.Endpoint(); + params.OAuthToken = "unit-test-oauth-token"; + params.EnableSsl = false; + params.RefreshPeriod = TDuration::MilliSeconds(50); + params.RequestTimeout = TDuration::Seconds(5); + + std::shared_ptr> provider; + auto facility = std::make_shared(); + + provider = std::make_shared>( + params, + [token = params.OAuthToken](CreateIamTokenRequest& req) { + req.set_yandex_passport_oauth_token(TStringType{token}); + }, + [](IamTokenService::Stub* stub, grpc::ClientContext* context, const CreateIamTokenRequest* request, + CreateIamTokenResponse* response, std::function cb) { + stub->async()->Create(context, request, response, std::move(cb)); + }, + facility, + authProvider); + + ASSERT_TRUE(authProvider->WaitUntilBlocked(std::chrono::seconds(10))) + << "FillContext should block inside slow AuthTokenProvider during refresh"; + + // Move-capture provider so provider.reset() is the last owner and triggers Stop(). + // facility stays here: its destructor joins the worker thread (still blocked in + // GetAuthInfo()), so it must be destroyed after authProvider->Release(). + // + // Mirrors production (TOwningFacilityCredentialsProvider): Inner_ dies first (Stop()), + // then Facility_ dies (joins worker). No self-join: TImpl holds facility as weak_ptr, + // callbacks capture only weak_ptr. If a callback locks TImpl temporarily, TImpl's + // destructor is trivial (no joins), and it never strong-refs the facility. + std::future stopDone = std::async(std::launch::async, + [provider = std::move(provider)]() mutable { + // Last owner: triggers ~TGrpcIamCredentialsProvider -> Stop(). + // Stop() must return even though the worker thread is still blocked in GetAuthInfo(). + provider.reset(); + }); + + ASSERT_EQ(stopDone.wait_for(std::chrono::seconds(20)), std::future_status::ready) + << "provider destructor (Stop()) must complete while FillContext is blocked in GetAuthInfo()"; + stopDone.get(); + + // Now unblock the worker thread so facility can be destroyed cleanly. + authProvider->Release(); + facility.reset(); // joins the worker thread (now unblocked) + + server.Stop(); +} diff --git a/tests/unit/client/iam_private/grpc_iam_service_ut.cpp b/tests/unit/client/iam_private/grpc_iam_service_ut.cpp new file mode 100644 index 00000000000..1478a9997d1 --- /dev/null +++ b/tests/unit/client/iam_private/grpc_iam_service_ut.cpp @@ -0,0 +1,220 @@ +#include +#include + +#include +#include + +#include +#include + +#include + +#include +#include +#include +#include +#include + +using namespace NYdb; +using namespace yandex::cloud::priv::iam::v1; + +namespace { + +constexpr const char* TestRSAPrivateKey = + "-----BEGIN PRIVATE KEY-----\n" + "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC75/JS3rMcLJxv\n" + "FgpOzF5+2gH+Yig3RE2MTl9uwC0BZKAv6foYr7xywQyWIK+W1cBhz8R4LfFmZo2j\n" + "M0aCvdRmNBdW0EDSTnHLxCsFhoQWLVq+bI5f5jzkcoiioUtaEpADPqwgVULVtN/n\n" + "nPJiZ6/dU30C3jmR6+LUgEntUtWt3eq3xQIn5lG3zC1klBY/HxtfH5Hu8xBvwRQT\n" + "Jnh3UpPLj8XwSmriDgdrhR7o6umWyVuGrMKlLHmeivlfzjYtfzO1MOIMG8t2/zxG\n" + "R+xb4Vwks73sH1KruH/0/JMXU97npwpe+Um+uXhpldPygGErEia7abyZB2gMpXqr\n" + "WYKMo02NAgMBAAECggEAO0BpC5OYw/4XN/optu4/r91bupTGHKNHlsIR2rDzoBhU\n" + "YLd1evpTQJY6O07EP5pYZx9mUwUdtU4KRJeDGO/1/WJYp7HUdtxwirHpZP0lQn77\n" + "uccuX/QQaHLrPekBgz4ONk+5ZBqukAfQgM7fKYOLk41jgpeDbM2Ggb6QUSsJISEp\n" + "zrwpI/nNT/wn+Hvx4DxrzWU6wF+P8kl77UwPYlTA7GsT+T7eKGVH8xsxmK8pt6lg\n" + "svlBA5XosWBWUCGLgcBkAY5e4ZWbkdd183o+oMo78id6C+PQPE66PLDtHWfpRRmN\n" + "m6XC03x6NVhnfvfozoWnmS4+e4qj4F/emCHvn0GMywKBgQDLXlj7YPFVXxZpUvg/\n" + "rheVcCTGbNmQJ+4cZXx87huqwqKgkmtOyeWsRc7zYInYgraDrtCuDBCfP//ZzOh0\n" + "LxepYLTPk5eNn/GT+VVrqsy35Ccr60g7Lp/bzb1WxyhcLbo0KX7/6jl0lP+VKtdv\n" + "mto+4mbSBXSM1Y5BVVoVgJ3T/wKBgQDsiSvPRzVi5TTj13x67PFymTMx3HCe2WzH\n" + "JUyepCmVhTm482zW95pv6raDr5CTO6OYpHtc5sTTRhVYEZoEYFTM9Vw8faBtluWG\n" + "BjkRh4cIpoIARMn74YZKj0C/0vdX7SHdyBOU3bgRPHg08Hwu3xReqT1kEPSI/B2V\n" + "4pe5fVrucwKBgQCNFgUxUA3dJjyMES18MDDYUZaRug4tfiYouRdmLGIxUxozv6CG\n" + "ZnbZzwxFt+GpvPUV4f+P33rgoCvFU+yoPctyjE6j+0aW0DFucPmb2kBwCu5J/856\n" + "kFwCx3blbwFHAco+SdN7g2kcwgmV2MTg/lMOcU7XwUUcN0Obe7UlWbckzQKBgQDQ\n" + "nXaXHL24GGFaZe4y2JFmujmNy1dEsoye44W9ERpf9h1fwsoGmmCKPp90az5+rIXw\n" + "FXl8CUgk8lXW08db/r4r+ma8Lyx0GzcZyplAnaB5/6j+pazjSxfO4KOBy4Y89Tb+\n" + "TP0AOcCi6ws13bgY+sUTa/5qKA4UVw+c5zlb7nRpgwKBgGXAXhenFw1666482iiN\n" + "cHSgwc4ZHa1oL6aNJR1XWH+aboBSwR+feKHUPeT4jHgzRGo/aCNHD2FE5I8eBv33\n" + "of1kWYjAO0YdzeKrW0rTwfvt9gGg+CS397aWu4cy+mTI+MNfBgeDAIVBeJOJXLlX\n" + "hL8bFAuNNVrCOp79TNnNIsh7\n" + "-----END PRIVATE KEY-----\n"; + +constexpr const char* TestRSAPublicKey = + "-----BEGIN PUBLIC KEY-----\n" + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu+fyUt6zHCycbxYKTsxe\n" + "ftoB/mIoN0RNjE5fbsAtAWSgL+n6GK+8csEMliCvltXAYc/EeC3xZmaNozNGgr3U\n" + "ZjQXVtBA0k5xy8QrBYaEFi1avmyOX+Y85HKIoqFLWhKQAz6sIFVC1bTf55zyYmev\n" + "3VN9At45kevi1IBJ7VLVrd3qt8UCJ+ZRt8wtZJQWPx8bXx+R7vMQb8EUEyZ4d1KT\n" + "y4/F8Epq4g4Ha4Ue6OrplslbhqzCpSx5nor5X842LX8ztTDiDBvLdv88RkfsW+Fc\n" + "JLO97B9Sq7h/9PyTF1Pe56cKXvlJvrl4aZXT8oBhKxImu2m8mQdoDKV6q1mCjKNN\n" + "jQIDAQAB\n" + "-----END PUBLIC KEY-----\n"; + +// Implements both Create (used by the nested JWT provider) and CreateForService (used by the +// outer service provider). Returns distinct tokens so the outer one is observable via GetAuthInfo(). +class TIamServiceStub final : public IamTokenService::Service { +public: + grpc::Status Create( + grpc::ServerContext*, + const CreateIamTokenRequest*, + CreateIamTokenResponse* response) override + { + response->set_iam_token("inner-jwt-token"); + response->mutable_expires_at()->set_seconds(4102444800); + return grpc::Status::OK; + } + + grpc::Status CreateForService( + grpc::ServerContext*, + const CreateIamTokenForServiceRequest*, + CreateIamTokenResponse* response) override + { + response->set_iam_token("outer-service-token"); + response->mutable_expires_at()->set_seconds(4102444800); + return grpc::Status::OK; + } +}; + +class TIamGrpcServer { +public: + explicit TIamGrpcServer(TIamServiceStub* service) : Service_(service) {} + + bool Start() { + grpc::ServerBuilder builder; + int boundPort = 0; + builder.AddListeningPort("127.0.0.1:0", grpc::InsecureServerCredentials(), &boundPort); + builder.RegisterService(Service_); + Server_ = builder.BuildAndStart(); + if (!Server_ || boundPort <= 0) { + return false; + } + Port_ = boundPort; + WaitThread_ = std::thread([this] { Server_->Wait(); }); + return true; + } + + std::string Endpoint() const { return "127.0.0.1:" + std::to_string(Port_); } + + void Stop() { + if (Server_) { + Server_->Shutdown(); + } + if (WaitThread_.joinable()) { + WaitThread_.join(); + } + Server_.reset(); + } + + ~TIamGrpcServer() { Stop(); } + +private: + TIamServiceStub* Service_ = nullptr; + std::unique_ptr Server_; + int Port_ = 0; + std::thread WaitThread_; +}; + +} // namespace + +// Regression test for the deprecated no-arg CreateProvider() on the IAM service-account +// factory with a nested gRPC JWT auth provider. Before the fix, both providers shared a single +// TSimpleCoreFacility, each registered a periodic refresh task, and TSimpleCoreFacility's +// single-task invariant tripped Y_ABORT_UNLESS and killed the process. The fix gives the +// nested auth provider its own facility (via a recursive no-arg CreateProvider()), so the two +// periodic tasks land on separate facilities. +TEST(IamServiceCredentialsProvider, NoArgCreateProviderWithGrpcInnerCreds) { + TIamServiceStub stub; + TIamGrpcServer server(&stub); + ASSERT_TRUE(server.Start()); + + TIamJwtParams jwtParams; + jwtParams.Endpoint = server.Endpoint(); + jwtParams.EnableSsl = false; + jwtParams.RefreshPeriod = TDuration::Hours(1); + jwtParams.RequestTimeout = TDuration::Seconds(5); + jwtParams.JwtParams.AccountId = "unit-test-account"; + jwtParams.JwtParams.KeyId = "unit-test-key"; + jwtParams.JwtParams.PrivKey = TestRSAPrivateKey; + jwtParams.JwtParams.PubKey = TestRSAPublicKey; + + auto jwtFactory = std::make_shared>(jwtParams); + + TIamServiceParams serviceParams; + serviceParams.Endpoint = server.Endpoint(); + serviceParams.EnableSsl = false; + serviceParams.RefreshPeriod = TDuration::Hours(1); + serviceParams.RequestTimeout = TDuration::Seconds(5); + serviceParams.ServiceId = "unit-test-service"; + serviceParams.MicroserviceId = "unit-test-microservice"; + serviceParams.ResourceId = "unit-test-resource"; + serviceParams.ResourceType = "unit-test-resource-type"; + serviceParams.TargetServiceAccountId = "unit-test-target"; + serviceParams.SystemServiceAccountCredentials = jwtFactory; + + auto serviceFactory = CreateIamServiceCredentialsProviderFactory(serviceParams); + + auto work = [&serviceFactory]() -> std::string { + auto provider = serviceFactory->CreateProvider(); + return provider->GetAuthInfo(); + }; + + std::future done = std::async(std::launch::async, work); + ASSERT_EQ(done.wait_for(std::chrono::seconds(20)), std::future_status::ready) + << "no-arg CreateProvider() with nested gRPC JWT auth must not abort and must produce a token"; + EXPECT_EQ(done.get(), "outer-service-token"); + + server.Stop(); +} + +// Regression: MakeRequestFiller must capture TIamServiceParams by value so a temporary factory +// can be destroyed while the provider still handles periodic refresh ticks. +TEST(IamServiceCredentialsProvider, NoArgCreateProviderTemporaryFactory) { + TIamServiceStub stub; + TIamGrpcServer server(&stub); + ASSERT_TRUE(server.Start()); + + TIamJwtParams jwtParams; + jwtParams.Endpoint = server.Endpoint(); + jwtParams.EnableSsl = false; + jwtParams.RefreshPeriod = TDuration::Hours(1); + jwtParams.RequestTimeout = TDuration::Seconds(5); + jwtParams.JwtParams.AccountId = "unit-test-account"; + jwtParams.JwtParams.KeyId = "unit-test-key"; + jwtParams.JwtParams.PrivKey = TestRSAPrivateKey; + jwtParams.JwtParams.PubKey = TestRSAPublicKey; + + auto jwtFactory = std::make_shared>(jwtParams); + + TIamServiceParams serviceParams; + serviceParams.Endpoint = server.Endpoint(); + serviceParams.EnableSsl = false; + serviceParams.RefreshPeriod = TDuration::MilliSeconds(100); + serviceParams.RequestTimeout = TDuration::Seconds(5); + serviceParams.ServiceId = "unit-test-service"; + serviceParams.MicroserviceId = "unit-test-microservice"; + serviceParams.ResourceId = "unit-test-resource"; + serviceParams.ResourceType = "unit-test-resource-type"; + serviceParams.TargetServiceAccountId = "unit-test-target"; + serviceParams.SystemServiceAccountCredentials = jwtFactory; + + auto provider = CreateIamServiceCredentialsProviderFactory(serviceParams)->CreateProvider(); + EXPECT_EQ(provider->GetAuthInfo(), "outer-service-token"); + + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + EXPECT_EQ(provider->GetAuthInfo(), "outer-service-token"); + + server.Stop(); +} diff --git a/tests/unit/client/observability/metric_buffer_ut.cpp b/tests/unit/client/observability/metric_buffer_ut.cpp new file mode 100644 index 00000000000..0ec0176c86b --- /dev/null +++ b/tests/unit/client/observability/metric_buffer_ut.cpp @@ -0,0 +1,577 @@ +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace NYdb; +using namespace NYdb::NObservability; +using namespace NYdb::NMetrics; +using namespace NYdb::NTests; + +namespace { + +constexpr const char kCounter[] = "test.counter"; +constexpr const char kHistogram[] = "test.histogram"; + +struct TFixture { + std::shared_ptr Fake; + std::shared_ptr Buffered; +}; + +TFixture MakeFixture(std::chrono::milliseconds flushInterval, + std::size_t threshold = 0) { + auto fake = std::make_shared(); + TMetricBufferSettings settings; + settings.FlushInterval = flushInterval; + settings.ThreadPendingThreshold = threshold; + auto buffered = CreateBufferedMetricRegistry(fake, settings); + return {std::move(fake), std::move(buffered)}; +} + +TFixture MakeFixture(TMetricBufferSettings settings) { + auto fake = std::make_shared(); + auto buffered = CreateBufferedMetricRegistry(fake, std::move(settings)); + return {std::move(fake), std::move(buffered)}; +} + +void SpinUntil(std::function pred, + std::chrono::milliseconds deadline = std::chrono::milliseconds(2000)) { + const auto t0 = std::chrono::steady_clock::now(); + while (!pred()) { + if (std::chrono::steady_clock::now() - t0 > deadline) { + return; + } + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + } +} + +} // namespace + +// --------------------------------------------------------------------------- +// Coalescing on a single thread. +// --------------------------------------------------------------------------- + +TEST(MetricBufferTest, CounterIncsAreCoalescedIntoOneAddPerFlush) { + auto fix = MakeFixture(std::chrono::seconds(10)); + + auto counter = fix.Buffered->Counter(kCounter, {}, "", ""); + for (int i = 0; i < 1000; ++i) { + counter->Inc(); + } + + auto fakeCounter = fix.Fake->GetCounter(kCounter, {}); + ASSERT_NE(fakeCounter, nullptr); + EXPECT_EQ(fakeCounter->Get(), 0); + EXPECT_EQ(fakeCounter->IncCalls(), 0u); + EXPECT_EQ(fakeCounter->AddCalls(), 0u); + + fix.Buffered.reset(); + + EXPECT_EQ(fakeCounter->Get(), 1000); + EXPECT_EQ(fakeCounter->IncCalls(), 0u); + EXPECT_EQ(fakeCounter->AddCalls(), 1u); +} + +TEST(MetricBufferTest, HistogramRecordsAreCoalescedIntoOneRecordMany) { + auto fix = MakeFixture(std::chrono::seconds(10)); + + auto hist = fix.Buffered->Histogram(kHistogram, {0.1, 0.5, 1, 5}, {}, "", ""); + for (int i = 0; i < 200; ++i) { + hist->Record(static_cast(i) / 100.0); + } + + auto fakeHist = fix.Fake->GetHistogram(kHistogram, {}); + ASSERT_NE(fakeHist, nullptr); + EXPECT_EQ(fakeHist->Count(), 0u); + EXPECT_EQ(fakeHist->RecordCalls(), 0u); + EXPECT_EQ(fakeHist->RecordManyCalls(), 0u); + + fix.Buffered.reset(); + + EXPECT_EQ(fakeHist->Count(), 200u); + EXPECT_EQ(fakeHist->RecordCalls(), 0u); + EXPECT_EQ(fakeHist->RecordManyCalls(), 1u); +} + +// --------------------------------------------------------------------------- +// Interval-triggered flush. +// --------------------------------------------------------------------------- + +TEST(MetricBufferTest, IntervalFlushDrainsPendingUpdates) { + auto fix = MakeFixture(std::chrono::milliseconds(20)); + + auto counter = fix.Buffered->Counter(kCounter, {}, "", ""); + counter->Add(7); + counter->Inc(); + counter->Inc(); + + auto fakeCounter = fix.Fake->GetCounter(kCounter, {}); + ASSERT_NE(fakeCounter, nullptr); + + SpinUntil([&]{ return fakeCounter->Get() == 9; }); + + EXPECT_EQ(fakeCounter->Get(), 9); + EXPECT_EQ(fakeCounter->IncCalls(), 0u); + EXPECT_GE(fakeCounter->AddCalls(), 1u); +} + +// --------------------------------------------------------------------------- +// Multi-threaded fan-in. +// --------------------------------------------------------------------------- + +TEST(MetricBufferTest, MultiThreadedIncsAreLosslessAndAggregated) { + constexpr int kThreads = 8; + constexpr int kIncsPerThread = 25'000; + + auto fix = MakeFixture(std::chrono::milliseconds(10)); + auto counter = fix.Buffered->Counter(kCounter, {}, "", ""); + + std::vector workers; + workers.reserve(kThreads); + for (int t = 0; t < kThreads; ++t) { + workers.emplace_back([counter] { + for (int i = 0; i < kIncsPerThread; ++i) { + counter->Inc(); + } + }); + } + for (auto& w : workers) { + w.join(); + } + + fix.Buffered.reset(); + + auto fakeCounter = fix.Fake->GetCounter(kCounter, {}); + ASSERT_NE(fakeCounter, nullptr); + EXPECT_EQ(fakeCounter->Get(), static_cast(kThreads) * kIncsPerThread); + EXPECT_EQ(fakeCounter->IncCalls(), 0u); + + EXPECT_GE(fakeCounter->AddCalls(), 1u); + EXPECT_LE(fakeCounter->AddCalls(), static_cast(kThreads * 100)); +} + +TEST(MetricBufferTest, MultiThreadedHistogramSamplesAreLosslessAndAggregated) { + constexpr int kThreads = 4; + constexpr int kRecordsPerThread = 10'000; + + auto fix = MakeFixture(std::chrono::milliseconds(10)); + auto hist = fix.Buffered->Histogram(kHistogram, {0.1, 1, 10}, {}, "", ""); + + std::vector workers; + for (int t = 0; t < kThreads; ++t) { + workers.emplace_back([hist, t] { + for (int i = 0; i < kRecordsPerThread; ++i) { + hist->Record(static_cast(t * 1000 + i)); + } + }); + } + for (auto& w : workers) { + w.join(); + } + + fix.Buffered.reset(); + + auto fakeHist = fix.Fake->GetHistogram(kHistogram, {}); + ASSERT_NE(fakeHist, nullptr); + EXPECT_EQ(fakeHist->Count(), + static_cast(kThreads) * kRecordsPerThread); + EXPECT_EQ(fakeHist->RecordCalls(), 0u); + EXPECT_GE(fakeHist->RecordManyCalls(), 1u); +} + +// --------------------------------------------------------------------------- +// Shutdown drain. +// --------------------------------------------------------------------------- + +TEST(MetricBufferTest, ShutdownDrainsLeftoverData) { + auto fix = MakeFixture(std::chrono::seconds(60)); // never fire by timer + auto counter = fix.Buffered->Counter(kCounter, {}, "", ""); + auto hist = fix.Buffered->Histogram(kHistogram, {}, {}, "", ""); + + counter->Add(100); + hist->Record(0.5); + hist->Record(1.5); + + auto fakeCounter = fix.Fake->GetCounter(kCounter, {}); + auto fakeHist = fix.Fake->GetHistogram(kHistogram, {}); + ASSERT_NE(fakeCounter, nullptr); + ASSERT_NE(fakeHist, nullptr); + + EXPECT_EQ(fakeCounter->Get(), 0); + EXPECT_EQ(fakeHist->Count(), 0u); + + fix.Buffered.reset(); + + EXPECT_EQ(fakeCounter->Get(), 100); + EXPECT_EQ(fakeHist->Count(), 2u); +} + +// --------------------------------------------------------------------------- +// Threshold-triggered flush. +// --------------------------------------------------------------------------- + +TEST(MetricBufferTest, ThresholdTriggersImmediateFlush) { + auto fix = MakeFixture(std::chrono::seconds(60), /*threshold=*/128); + auto counter = fix.Buffered->Counter(kCounter, {}, "", ""); + auto fakeCounter = fix.Fake->GetCounter(kCounter, {}); + ASSERT_NE(fakeCounter, nullptr); + + for (int i = 0; i < 100; ++i) { + counter->Inc(); + } + + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + EXPECT_EQ(fakeCounter->Get(), 0); + + for (int i = 0; i < 50; ++i) { + counter->Inc(); + } + SpinUntil([&]{ return fakeCounter->Get() == 150; }); + EXPECT_EQ(fakeCounter->Get(), 150); +} + +// --------------------------------------------------------------------------- +// Repeat lookup returns the same wrapper instance. +// --------------------------------------------------------------------------- + +TEST(MetricBufferTest, RepeatedLookupReturnsSameHandle) { + auto fix = MakeFixture(std::chrono::seconds(60)); + + auto a = fix.Buffered->Counter(kCounter, {}, "", ""); + auto b = fix.Buffered->Counter(kCounter, {}, "", ""); + EXPECT_EQ(a.get(), b.get()); + + auto h1 = fix.Buffered->Histogram(kHistogram, {1.0}, {}, "", ""); + auto h2 = fix.Buffered->Histogram(kHistogram, {1.0}, {}, "", ""); + EXPECT_EQ(h1.get(), h2.get()); +} + +// --------------------------------------------------------------------------- +// Gauge passthrough semantics. +// --------------------------------------------------------------------------- + +TEST(MetricBufferTest, GaugeIsPassThrough) { + auto fix = MakeFixture(std::chrono::seconds(60)); + + auto g = fix.Buffered->Gauge("test.gauge", {}, "", ""); + g->Set(42.0); + g->Add(8.0); + + auto fakeGauge = fix.Fake->GetGauge("test.gauge", {}); + ASSERT_NE(fakeGauge, nullptr); + EXPECT_DOUBLE_EQ(fakeGauge->Get(), 50.0); +} + +// --------------------------------------------------------------------------- +// Self-observability metrics are wired up. +// --------------------------------------------------------------------------- + +TEST(MetricBufferTest, SelfObservabilityMetricsAreEmitted) { + auto fix = MakeFixture(std::chrono::milliseconds(10)); + + auto counter = fix.Buffered->Counter(kCounter, {}, "", ""); + auto hist = fix.Buffered->Histogram(kHistogram, {1.0}, {}, "", ""); + counter->Add(50); + for (int i = 0; i < 20; ++i) { + hist->Record(0.1); + } + + fix.Buffered.reset(); + + auto buffered = fix.Fake->GetCounter("ydb_sdk_metric_buffer_events_buffered_total", {}); + ASSERT_NE(buffered, nullptr); + EXPECT_EQ(buffered->Get(), 70); + + auto addCalls = fix.Fake->GetCounter( + "ydb_sdk_metric_buffer_underlying_calls_total", {{"kind", "add"}}); + ASSERT_NE(addCalls, nullptr); + EXPECT_GE(addCalls->Get(), 1); + + auto recordManyCalls = fix.Fake->GetCounter( + "ydb_sdk_metric_buffer_underlying_calls_total", {{"kind", "record_many"}}); + ASSERT_NE(recordManyCalls, nullptr); + EXPECT_GE(recordManyCalls->Get(), 1); + + auto shutdownFlushes = fix.Fake->GetCounter( + "ydb_sdk_metric_buffer_flushes_total", {{"trigger", "shutdown"}}); + ASSERT_NE(shutdownFlushes, nullptr); + EXPECT_GE(shutdownFlushes->Get(), 1); +} + +// --------------------------------------------------------------------------- +// Best-effort overflow handling and dropped updates accounting. +// --------------------------------------------------------------------------- + +TEST(MetricBufferTest, OverflowDropsUpdatesAndReportsDroppedMetrics) { + TMetricBufferSettings settings; + settings.FlushInterval = std::chrono::seconds(60); // rely on shutdown drain + settings.ThreadPendingThreshold = 0; + settings.ThreadPendingLimit = 8; + auto fix = MakeFixture(settings); + + auto counter = fix.Buffered->Counter(kCounter, {}, "", ""); + auto hist = fix.Buffered->Histogram(kHistogram, {1.0}, {}, "", ""); + + for (int i = 0; i < 50; ++i) { + counter->Inc(); + hist->Record(0.01 * i); + } + + fix.Buffered.reset(); + + auto fakeCounter = fix.Fake->GetCounter(kCounter, {}); + auto fakeHist = fix.Fake->GetHistogram(kHistogram, {}); + ASSERT_NE(fakeCounter, nullptr); + ASSERT_NE(fakeHist, nullptr); + EXPECT_LE(fakeCounter->Get(), 8); + EXPECT_LE(fakeHist->Count(), 8u); + + auto droppedCounter = fix.Fake->GetCounter( + "ydb_sdk_metric_buffer_dropped_updates_total", {{"instrument", "counter"}}); + auto droppedHistogram = fix.Fake->GetCounter( + "ydb_sdk_metric_buffer_dropped_updates_total", {{"instrument", "histogram"}}); + ASSERT_NE(droppedCounter, nullptr); + ASSERT_NE(droppedHistogram, nullptr); + EXPECT_GT(droppedCounter->Get(), 0); + EXPECT_GT(droppedHistogram->Get(), 0); +} + +TEST(MetricBufferTest, ZeroPendingLimitDisablesDropping) { + TMetricBufferSettings settings; + settings.FlushInterval = std::chrono::seconds(60); + settings.ThreadPendingThreshold = 0; + settings.ThreadPendingLimit = 0; // unlimited + auto fix = MakeFixture(settings); + + auto counter = fix.Buffered->Counter(kCounter, {}, "", ""); + for (int i = 0; i < 1000; ++i) { + counter->Inc(); + } + fix.Buffered.reset(); + + auto fakeCounter = fix.Fake->GetCounter(kCounter, {}); + ASSERT_NE(fakeCounter, nullptr); + EXPECT_EQ(fakeCounter->Get(), 1000); + + auto droppedCounter = fix.Fake->GetCounter( + "ydb_sdk_metric_buffer_dropped_updates_total", {{"instrument", "counter"}}); + if (droppedCounter) { + EXPECT_EQ(droppedCounter->Get(), 0); + } +} + +// --------------------------------------------------------------------------- +// Post-shutdown handle behavior: pass-through mode for still-live handles. +// --------------------------------------------------------------------------- + +TEST(MetricBufferTest, LiveHandlesBecomePassThroughAfterBufferedRegistryDestruction) { + auto fix = MakeFixture(std::chrono::seconds(60)); // do not auto-flush + auto counter = fix.Buffered->Counter(kCounter, {}, "", ""); + auto hist = fix.Buffered->Histogram(kHistogram, {1.0}, {}, "", ""); + + counter->Inc(); // buffered + hist->Record(0.5); // buffered + + // Destroy buffered registry first; handles remain alive. + fix.Buffered.reset(); + + // After shutdown handles should bypass the buffer. + counter->Add(3); + hist->Record(1.5); + + auto fakeCounter = fix.Fake->GetCounter(kCounter, {}); + auto fakeHist = fix.Fake->GetHistogram(kHistogram, {}); + ASSERT_NE(fakeCounter, nullptr); + ASSERT_NE(fakeHist, nullptr); + EXPECT_EQ(fakeCounter->Get(), 4); // 1 buffered + 3 pass-through + EXPECT_EQ(fakeHist->Count(), 2u); // 1 buffered + 1 pass-through +} + +// --------------------------------------------------------------------------- +// Race-ish test: concurrent registration and periodic flushing. +// --------------------------------------------------------------------------- + +TEST(MetricBufferTest, ConcurrentMetricRegistrationAndFlushIsLossless) { + auto fix = MakeFixture(std::chrono::milliseconds(5)); + + constexpr int kThreads = 4; + constexpr int kMetricsPerThread = 50; + constexpr int kIncsPerMetric = 200; + + std::vector workers; + workers.reserve(kThreads); + for (int t = 0; t < kThreads; ++t) { + workers.emplace_back([&, t] { + for (int m = 0; m < kMetricsPerThread; ++m) { + auto name = std::string("race.counter.") + std::to_string(t) + "." + std::to_string(m); + auto c = fix.Buffered->Counter(name, {}, "", ""); + for (int i = 0; i < kIncsPerMetric; ++i) { + c->Inc(); + } + } + }); + } + for (auto& w : workers) { + w.join(); + } + fix.Buffered.reset(); + + const int64_t expected = static_cast(kIncsPerMetric); + for (int t = 0; t < kThreads; ++t) { + for (int m = 0; m < kMetricsPerThread; ++m) { + auto name = std::string("race.counter.") + std::to_string(t) + "." + std::to_string(m); + auto c = fix.Fake->GetCounter(name, {}); + ASSERT_NE(c, nullptr) << name; + EXPECT_EQ(c->Get(), expected) << name; + } + } +} + +// --------------------------------------------------------------------------- +// Flush worker resiliency: underlying metric calls may throw. +// --------------------------------------------------------------------------- + +namespace { + +class TThrowingCounter final : public ICounter { +public: + explicit TThrowingCounter(std::atomic* calls) : Calls_(calls) {} + void Inc() override { throw std::runtime_error("inc throw"); } + void Add(std::uint64_t) override { + Calls_->fetch_add(1, std::memory_order_relaxed); + throw std::runtime_error("add throw"); + } +private: + std::atomic* Calls_; +}; + +class TThrowingHistogram final : public IHistogram { +public: + explicit TThrowingHistogram(std::atomic* calls) : Calls_(calls) {} + void Record(double) override { throw std::runtime_error("record throw"); } + void RecordMany(const std::vector&) override { + Calls_->fetch_add(1, std::memory_order_relaxed); + throw std::runtime_error("record many throw"); + } +private: + std::atomic* Calls_; +}; + +class TThrowingRegistry final : public IMetricRegistry { +public: + std::shared_ptr Counter(const std::string&, const TLabels&, const std::string&, const std::string&) override { + if (!Counter_) { + Counter_ = std::make_shared(&CounterCalls_); + } + return Counter_; + } + std::shared_ptr Gauge(const std::string&, const TLabels&, const std::string&, const std::string&) override { + return nullptr; + } + std::shared_ptr Histogram(const std::string&, const std::vector&, const TLabels&, const std::string&, const std::string&) override { + if (!Histogram_) { + Histogram_ = std::make_shared(&HistogramCalls_); + } + return Histogram_; + } + std::uint64_t CounterCalls() const { return CounterCalls_.load(std::memory_order_relaxed); } + std::uint64_t HistogramCalls() const { return HistogramCalls_.load(std::memory_order_relaxed); } +private: + std::shared_ptr Counter_; + std::shared_ptr Histogram_; + std::atomic CounterCalls_{0}; + std::atomic HistogramCalls_{0}; +}; + +} // namespace + +TEST(MetricBufferTest, FlushThreadSurvivesUnderlyingExceptions) { + auto throwing = std::make_shared(); + TMetricBufferSettings settings; + settings.FlushInterval = std::chrono::milliseconds(2); + settings.ThreadPendingThreshold = 4; + auto buffered = CreateBufferedMetricRegistry(throwing, settings); + + auto c = buffered->Counter("throw.counter", {}, "", ""); + auto h = buffered->Histogram("throw.hist", {1.0}, {}, "", ""); + for (int i = 0; i < 100; ++i) { + c->Inc(); + h->Record(0.1 * i); + } + + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + EXPECT_GT(throwing->CounterCalls(), 0u); + EXPECT_GT(throwing->HistogramCalls(), 0u); + + EXPECT_NO_THROW(buffered.reset()); +} + +// --------------------------------------------------------------------------- +// Property-style invariant test for deterministic random operations. +// --------------------------------------------------------------------------- + +TEST(MetricBufferTest, RandomOperationSequencePreservesTotalsWithoutDropping) { + TMetricBufferSettings settings; + settings.FlushInterval = std::chrono::milliseconds(3); + settings.ThreadPendingThreshold = 128; + settings.ThreadPendingLimit = 0; + auto fix = MakeFixture(settings); + + auto counter = fix.Buffered->Counter(kCounter, {}, "", ""); + auto hist = fix.Buffered->Histogram(kHistogram, {1.0}, {}, "", ""); + + std::mt19937 rng(42); + std::uniform_int_distribution opDist(0, 3); + std::uniform_int_distribution addDist(1, 5); + + int64_t expectedCounter = 0; + std::size_t expectedHist = 0; + for (int i = 0; i < 5000; ++i) { + const int op = opDist(rng); + switch (op) { + case 0: + counter->Inc(); + ++expectedCounter; + break; + case 1: { + const int d = addDist(rng); + counter->Add(d); + expectedCounter += d; + break; + } + case 2: + hist->Record(static_cast(i) * 0.01); + ++expectedHist; + break; + default: { + std::vector batch; + const int n = addDist(rng); + batch.reserve(n); + for (int j = 0; j < n; ++j) { + batch.push_back(static_cast(i + j) * 0.001); + } + hist->RecordMany(batch); + expectedHist += batch.size(); + break; + } + } + } + + fix.Buffered.reset(); + + auto fakeCounter = fix.Fake->GetCounter(kCounter, {}); + auto fakeHist = fix.Fake->GetHistogram(kHistogram, {}); + ASSERT_NE(fakeCounter, nullptr); + ASSERT_NE(fakeHist, nullptr); + EXPECT_EQ(fakeCounter->Get(), expectedCounter); + EXPECT_EQ(fakeHist->Count(), expectedHist); +}