diff --git a/.github/workflows/slo-test.yml b/.github/workflows/slo-test.yml new file mode 100644 index 0000000..f0c5bde --- /dev/null +++ b/.github/workflows/slo-test.yml @@ -0,0 +1,110 @@ +name: SLO JDBC + +on: + push: + branches: [master, main, add-slo-workload] + paths: + - 'slo-workload/**' + - '.github/workflows/slo-test.yml' + pull_request: + paths: + - 'slo-workload/**' + - '.github/workflows/slo-test.yml' + +jobs: + slo-workload-test: + if: (!contains(github.event.pull_request.labels.*.name, 'no slo')) + name: Test SLO Workload + runs-on: ubuntu-latest + + strategy: + matrix: + workload: + - simple-jdbc + include: + - workload: simple-jdbc + test_duration: 60 + read_rps: 1000 + write_rps: 100 + read_timeout: 1000 + write_timeout: 1000 + + concurrency: + group: slo-${{ github.ref }}-${{ matrix.workload }} + cancel-in-progress: true + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Initialize YDB SLO + uses: ydb-platform/ydb-slo-action/init@53e02500d4a98a6b67d9009bc46e839236f15f81 + with: + github_pull_request_number: ${{ github.event.pull_request.number }} + github_token: ${{ secrets.GITHUB_TOKEN }} + workload_name: ${{ matrix.workload }} + ydb_database_node_count: 5 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 21 + cache: maven + + - name: Build project + run: mvn clean install -DskipTests -q + + - name: Wait for YDB + run: | + echo "Waiting for YDB to start..." + for i in {1..30}; do + if nc -zv localhost 2135 2>&1 | grep -q "succeeded"; then + echo "YDB is ready!" + break + fi + echo "Attempt $i/30..." + sleep 2 + done + + - name: Run SLO test + env: + WORKLOAD_NAME: ${{ matrix.workload }} + YDB_JDBC_URL: jdbc:ydb:grpc://localhost:2135/Root/testdb + PROM_PGW: http://localhost:9091 + TEST_DURATION: ${{ matrix.test_duration }} + READ_RPS: ${{ matrix.read_rps }} + WRITE_RPS: ${{ matrix.write_rps }} + READ_TIMEOUT: ${{ matrix.read_timeout }} + WRITE_TIMEOUT: ${{ matrix.write_timeout }} + REPORT_PERIOD: 1000 + run: | + mvn test -pl slo-workload/${{ matrix.workload }} \ + -Dskip.jdbc.tests=false \ + -Dtest=JdbcSloTest + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: slo-test-results-${{ matrix.workload }} + path: | + slo-workload/**/target/surefire-reports/ + slo-workload/**/target/*.log + retention-days: 3 + + publish-slo-report: + name: Publish SLO Report + runs-on: ubuntu-latest + needs: slo-workload-test + if: success() + permissions: + contents: read + pull-requests: write + actions: read + steps: + - name: Publish report + uses: ydb-platform/ydb-slo-action/report@53e02500d4a98a6b67d9009bc46e839236f15f81 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + github_run_id: ${{ github.run_id }} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 8978897..8b23a80 100644 --- a/pom.xml +++ b/pom.xml @@ -30,6 +30,7 @@ url-shortener-demo jdbc project-course + slo-workload diff --git a/slo-workload/README.md b/slo-workload/README.md new file mode 100644 index 0000000..d48ea9e --- /dev/null +++ b/slo-workload/README.md @@ -0,0 +1,508 @@ +# YDB SLO Workload Tests + +Набор SLO (Service Level Objectives) тестов для проверки производительности и надежности YDB клиентов на Java. + +## Назначение + +SLO тесты измеряют соответствие реальной производительности заявленным целевым метрикам: +- **Latency**: время отклика операций (P50, P95, P99) +- **Availability**: процент успешных операций +- **Throughput**: количество операций в секунду +- **Stability**: стабильность под длительной нагрузкой + +## Стратегия тестирования + +### Фазы выполнения + +#### 1. Инициализация (Setup Phase) +``` +Действия: +- Создание таблицы slo_table +- Проверка подключения к YDB +- Инициализация метрик + +Длительность: ~5 секунд +``` + +#### 2. Прогрев (Warmup Phase) +``` +Зачем нужен: +- JIT компиляция "горячих" методов +- Инициализация connection pool +- Стабилизация JVM (GC, memory allocation) +- Прогрев кэшей YDB + +Параметры: +- Потоки: 10 (read: 8, write: 2) +- Длительность: 10 секунд +- RPS: 50% от целевой нагрузки + +Критерий готовности: +- Latency P95 стабилизировалась (не растет) +- Success rate > 95% +``` + +**Почему 10 потоков?** +- Достаточно для JIT компиляции критичных методов +- Не создает избыточную нагрузку на YDB +- Позволяет обнаружить проблемы до основного теста + +#### 3. Рабочая нагрузка (Load Phase) +``` +Параметры: +- Потоки: 30 (read: 24, write: 6) +- Длительность: TEST_DURATION секунд (default: 60) +- Read RPS: READ_RPS (default: 1000) +- Write RPS: WRITE_RPS (default: 100) + +Соотношение read/write = 10:1 (типичное для OLTP) +``` + +**Почему 30 потоков?** +- Имитирует реальную многопользовательскую нагрузку +- Достаточно для насыщения connection pool +- Выявляет проблемы конкурентного доступа +- Соответствует типичной нагрузке веб-приложений (20-50 одновременных запросов) +- **Рассчитано на кластер из 5 нод** (текущее значение `ydb_database_node_count`) + +**Распределение потоков (24 read / 6 write):** +- Отражает реальное соотношение операций в OLTP системах +- Read-heavy workload типичен для большинства приложений +- Write потоки создают достаточную конкуренцию за ресурсы + +> ⚠️ **Важно:** Количество потоков следует масштабировать в зависимости от размера кластера: +> - **3 ноды:** 18-20 потоков (6 потоков на ноду) +> - **5 нод:** 30 потоков (6 потоков на ноду) ← текущая конфигурация +> - **10 нод:** 50-60 потоков (5-6 потоков на ноду) +> +> Эмпирическое правило: **~6 потоков на ноду** обеспечивает оптимальную утилизацию без перегрузки. +> В будущих версиях планируется автоматический расчет количества потоков на основе `ydb_database_node_count`. + +#### 4. Валидация результатов (Validation Phase) +``` +Проверяемые SLO: +✓ P50 latency < 10 ms +✓ P95 latency < 50 ms +✓ P99 latency < 100 ms +✓ Success rate > 99.9% + +Если хотя бы один порог не пройден → тест FAILED +``` + +#### 5. Экспорт метрик (Export Phase) +``` +Действия: +- Отправка метрик в Prometheus Push Gateway +- Сохранение результатов в файл +- Генерация отчета для GitHub +``` + +## Метрики + +### Основные метрики Prometheus +``` +sdk_operations_total{operation_type, sdk, sdk_version} + Counter - общее количество операций + Labels: read, write, upsert + +sdk_operations_success_total{operation_type, sdk, sdk_version} + Counter - успешные операции + Используется для расчета Success Rate + +sdk_operation_latency_seconds{operation_type, operation_status} + Histogram - распределение latency + Buckets: 1ms, 2ms, 3ms, 4ms, 5ms, 7.5ms, 10ms, 20ms, 50ms, 100ms, 200ms, 500ms, 1s + Позволяет вычислить P50, P95, P99 + +sdk_pending_operations{operation_type} + Gauge - количество операций в процессе выполнения + Индикатор перегрузки системы +``` + +### Вычисляемые метрики + +**Success Rate:** +``` +success_rate = (sdk_operations_success_total / sdk_operations_total) * 100% +``` + +**Error Rate:** +``` +error_rate = 100% - success_rate +``` + +**Throughput (RPS):** +``` +actual_rps = sdk_operations_total / test_duration_seconds +``` + +**Percentiles (P50, P95, P99):** +``` +Вычисляются из histogram buckets в Prometheus/Grafana +``` + +## Retry механизм + +Все SLO тесты используют единую стратегию retry: +``` +Стратегия: Exponential Backoff +Максимум попыток: 5 +Backoff delays: 100ms → 200ms → 400ms → 800ms → 1600ms + +Повторяемые ошибки (transient): +- Connection timeout +- Network errors +- Session expired +- Overload (503) +- Unavailable (503) + +Не повторяемые ошибки (permanent): +- Schema errors (table not found, column mismatch) +- Constraint violations (unique, foreign key) +- Syntax errors +- Permission denied +``` + +**Почему именно такая стратегия?** +- Exponential backoff снижает нагрузку на перегруженную систему +- 5 попыток достаточно для восстановления после кратковременных сбоев +- Максимальная задержка 1.6s не блокирует тест надолго + +## Структура проекта +``` +slo-workload/ +├── pom.xml # Родительский POM (Java 21, зависимости) +├── README.md # Этот файл +│ +├── simple-jdbc/ # SLO тест для базового JDBC +│ ├── src/ +│ │ ├── main/java/tech/ydb/slo/ +│ │ │ ├── JdbcSloTableContext.java # Управление таблицей +│ │ │ ├── MetricsReporter.java # Экспорт метрик +│ │ │ ├── SloTableRow.java # DTO +│ │ │ └── SimpleJdbcConfig.java # Spring конфиг +│ │ └── test/java/tech/ydb/slo/ +│ │ └── JdbcSloTest.java # Основной тест +│ ├── pom.xml +│ └── README.md +│ +└── [future workloads]/ + ├── spring-jdbc/ # Spring JdbcTemplate + ├── spring-data-jdbc/ # Spring Data JDBC + └── spring-data-jpa/ # Spring Data JPA +``` + +## Текущие реализации + +### ✅ simple-jdbc +Базовый JDBC драйвер без фреймворков. + +**Статус:** Готов +**Технологии:** YDB JDBC Driver, Spring Boot (только DI) +**Документация:** [simple-jdbc/README.md](simple-jdbc/README.md) + +## Планируемые реализации + +### 🔄 spring-jdbc +Использование Spring JdbcTemplate. + +**Цель:** Проверить overhead Spring JdbcTemplate +**Ожидаемый результат:** Latency +2-3ms по сравнению с plain JDBC + +### 🔄 spring-data-jdbc +Spring Data JDBC (без JPA). + +**Цель:** Измерить производительность Spring Data абстракции +**Ожидаемый результат:** Latency +5-10ms, упрощение кода + +### 🔄 spring-data-jpa +Полноценный JPA/Hibernate stack. + +**Цель:** Оценить overhead JPA +**Ожидаемый результат:** Latency +10-20ms, максимальное удобство разработки + +### 🔄 webflux-r2dbc +Реактивный подход (WebFlux + R2DBC). + +**Цель:** Сравнить reactive vs blocking под высокой нагрузкой +**Ожидаемый результат:** Лучшая throughput при >100 одновременных запросах + +## Планируемые улучшения + +### Инфраструктура + +#### 🔄 Динамическое масштабирование потоков +**Проблема:** Фиксированное количество потоков (30) не оптимально для кластеров разного размера. + +**Решение:** Автоматический расчет на основе `ydb_database_node_count`: +```java +int optimalThreads = ydbNodeCount * THREADS_PER_NODE; // 6 потоков на ноду +int readThreads = (int) (optimalThreads * 0.8); // 80% read +int writeThreads = optimalThreads - readThreads; // 20% write +``` + +**Преимущества:** +- Оптимальная утилизация кластера любого размера +- Предсказуемая нагрузка на каждую ноду +- Лучшее распределение запросов + +**Статус:** Планируется после стабилизации текущих тестов + +#### 🔄 Адаптивная стратегия warmup +Длительность прогрева также зависит от размера кластера: +- 3 ноды: 5-7 секунд +- 5 нод: 10 секунд (текущее) +- 10+ нод: 15-20 секунд + +#### 🔄 Тесты масштабируемости +Автоматическое тестирование на кластерах разного размера: +```yaml +matrix: + ydb_nodes: [3, 5, 10] + # Автоматически: threads = nodes * 6 +``` + +**Ожидаемые результаты:** +- Linear scaling throughput: 3 ноды → 5 нод = +67% RPS +- Latency остается стабильной при правильном масштабировании потоков + +### Тестовые сценарии + +#### 🔄 Chaos Testing +- Рестарты нод во время теста +- Network partitions +- Увеличение latency сети + +#### 🔄 Soak Tests +- Длительные тесты (24h+) +- Детекция memory leaks +- Проверка стабильности под постоянной нагрузкой + +#### 🔄 Различные размеры payload +- Small (100 bytes) +- Medium (1 KB) ← текущий +- Large (10 KB) +- Extra Large (100 KB) + +## Использование в CI/CD + +### В ydb-java-examples (текущий репозиторий) + +Workflow: `.github/workflows/slo-test.yml` +```yaml +Триггеры: +- Push в master/main +- Pull Request с изменениями в slo-workload/ + +Что делает: +1. Поднимает YDB через ydb-slo-action +2. Собирает проект (Maven) +3. Запускает SLO тесты +4. Публикует графики в PR +``` + +### В ydb-jdbc-driver / ydb-java-sdk (будущее) + +Workflow: `.github/workflows/slo.yml` +```yaml +Что будет делать: +1. Checkout текущего SDK/JDBC драйвера +2. Checkout ydb-java-examples +3. Собрать текущую версию драйвера +4. Обновить версию в SLO проекте +5. Запустить SLO тесты с новой версией +6. Публиковать результаты + +Цель: +- Проверять каждый PR на регрессию производительности +- Блокировать merge при нарушении SLO +``` + +## Добавление нового workload + +### Шаг 1: Создайте модуль +```bash +cd slo-workload +cp -r simple-jdbc your-workload +cd your-workload +``` + +### Шаг 2: Модифицируйте код +```java +// YourSloTest.java +@SpringBootTest +public class YourSloTest { + + @Test + public void runSloTest() { + // 1. Setup + // 2. Warmup (10 threads, 10s) + // 3. Load test (30 threads, TEST_DURATION) + // 4. Validate SLO + // 5. Export metrics + } +} +``` + +### Шаг 3: Обновите pom.xml +```xml + + + simple-jdbc + your-workload + +``` + +### Шаг 4: Обновите workflow +```yaml +# .github/workflows/slo-test.yml +matrix: + workload: + - simple-jdbc + - your-workload +``` + +### Шаг 5: Документация + +Создайте `your-workload/README.md` с описанием специфики вашего теста. + +## Интерпретация результатов + +### ✅ Успешный тест +``` +✓ P50: 5.2 ms (порог: 10 ms) +✓ P95: 23.1 ms (порог: 50 ms) +✓ P99: 45.8 ms (порог: 100 ms) +✓ Success Rate: 99.95% (порог: 99.9%) +``` + +**Интерпретация:** Производительность в норме, можно мержить PR. + +### ⚠️ Warning: близко к порогу +``` +✓ P50: 8.7 ms (порог: 10 ms) ⚠️ +✓ P95: 47.3 ms (порог: 50 ms) ⚠️ +✓ P99: 89.2 ms (порог: 100 ms) +✓ Success Rate: 99.91% (порог: 99.9%) +``` + +**Интерпретация:** Тест прошел, но метрики близки к порогам. Рекомендуется: +- Проверить код на возможные оптимизации +- Запустить тест повторно для подтверждения +- Рассмотреть профилирование + +### ❌ Failed: нарушение SLO +``` +✓ P50: 12.4 ms (порог: 10 ms) ❌ +✓ P95: 78.9 ms (порог: 50 ms) ❌ +✓ P99: 156.3 ms (порог: 100 ms) ❌ +✓ Success Rate: 99.45% (порог: 99.9%) ❌ +``` + +**Интерпретация:** Регрессия производительности. Действия: +1. Сравнить с предыдущим успешным прогоном +2. Найти изменения, которые могли повлиять +3. Профилировать код (JProfiler, async-profiler) +4. Проверить конфигурацию (connection pool, timeouts) + +## Best Practices + +### При разработке нового workload + +✅ **DO:** +- Используйте единую стратегию прогрева (10 потоков, 10с) +- Следуйте паттерну 30 потоков для основной нагрузки (на 5 нод) +- Реализуйте retry с exponential backoff +- Экспортируйте стандартные метрики +- Документируйте специфику вашего теста + +❌ **DON'T:** +- Не меняйте SLO пороги без обоснования +- Не пропускайте фазу warmup +- Не используйте фиксированные задержки вместо exponential backoff +- Не игнорируйте ошибки (всегда логируйте и считайте) + +### При настройке нагрузки + +✅ **DO:** +- Учитывайте размер кластера при выборе количества потоков +- Используйте формулу: `threads ≈ nodes * 6` как отправную точку +- Мониторьте утилизацию каждой ноды (должна быть равномерной) +- Начинайте с меньшей нагрузки и постепенно увеличивайте + +❌ **DON'T:** +- Не используйте 30 потоков для кластера из 3 нод (перегрузка) +- Не используйте 30 потоков для кластера из 20 нод (недогрузка) +- Не сравнивайте результаты тестов на кластерах разного размера +- Не запускайте 100+ потоков без обоснования + +### При анализе результатов + +✅ **DO:** +- Смотрите на тренды (несколько прогонов) +- Сравнивайте с baseline (предыдущие успешные тесты) +- Учитывайте вариативность (±5% норма) +- Проверяйте все метрики, не только latency + +❌ **DON'T:** +- Не принимайте решения по одному прогону +- Не игнорируйте Success Rate ради latency +- Не сравнивайте разные конфигурации YDB + +## Troubleshooting + +### Высокая latency только в начале теста + +**Причина:** Недостаточный warmup +**Решение:** Увеличить длительность warmup до 15-20 секунд + +### Success Rate < 99.9% из-за timeout + +**Причина:** Таймауты слишком короткие для текущей нагрузки +**Решение:** Увеличить `READ_TIMEOUT` / `WRITE_TIMEOUT` в workflow + +### Нестабильные результаты (разброс >10%) + +**Причина:** Конкуренция за ресурсы в GitHub Actions runner +**Решение:** Запустить несколько раз, усреднить результаты + +### "Too many retries" ошибки + +**Причина:** YDB перегружен или недостаточно нод +**Решение:** Увеличить `ydb_database_node_count` в workflow + +### Неравномерная нагрузка на ноды кластера + +**Причина:** Количество потоков не соответствует размеру кластера +**Диагностика:** +```bash +# Проверить утилизацию нод в YDB Monitoring +# Если разброс > 20% между нодами → проблема +``` + +**Решение:** +- Для 3 нод: уменьшить до 18-20 потоков +- Для 10 нод: увеличить до 50-60 потоков +- Проверить балансировку connection pool + +### Высокая latency при увеличении размера кластера + +**Причина:** Фиксированное количество потоков (30) недостаточно для утилизации большого кластера +**Решение:** Масштабировать количество потоков пропорционально количеству нод + +**Пример:** +```yaml +# Для 10 нод (требует доработки кода) +env: + THREAD_COUNT: 60 +``` + +**Temporary workaround:** Запускать несколько инстансов теста параллельно + +## Ссылки + +- [YDB JDBC Driver](https://github.com/ydb-platform/ydb-jdbc-driver) +- [YDB Java SDK](https://github.com/ydb-platform/ydb-java-sdk) +- [YDB SLO Action](https://github.com/ydb-platform/ydb-slo-action) +- [YDB Documentation](https://ydb.tech/docs/) +- [Prometheus Client Java](https://github.com/prometheus/client_java) \ No newline at end of file diff --git a/slo-workload/pom.xml b/slo-workload/pom.xml new file mode 100644 index 0000000..c7e9f8c --- /dev/null +++ b/slo-workload/pom.xml @@ -0,0 +1,83 @@ + + 4.0.0 + + tech.ydb.examples + ydb-sdk-examples + 1.1.0-SNAPSHOT + ../pom.xml + + + slo-workload + pom + + YDB SLO Workload Tests + SLO testing applications for YDB performance validation + + + 21 + 21 + 21 + UTF-8 + + 3.5.9 + 2.3.20 + 0.16.0 + 5.12.2 + true + + + + simple-jdbc + + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + tech.ydb.jdbc + ydb-jdbc-driver + ${ydb.jdbc.version} + + + + + io.prometheus + simpleclient + ${prometheus.version} + + + io.prometheus + simpleclient_pushgateway + ${prometheus.version} + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 21 + 21 + 21 + + + + + + \ No newline at end of file diff --git a/slo-workload/simple-jdbc/README.md b/slo-workload/simple-jdbc/README.md new file mode 100644 index 0000000..ef31f60 --- /dev/null +++ b/slo-workload/simple-jdbc/README.md @@ -0,0 +1,137 @@ +# Simple JDBC SLO Test + +Базовый SLO тест для YDB JDBC Driver без использования фреймворков (plain JDBC). + +## Описание + +Тестирует производительность и надежность YDB JDBC Driver под нагрузкой: +- **Latency**: P50 < 10ms, P95 < 50ms, P99 < 100ms +- **Success Rate**: > 99.9% +- **Технологии**: YDB JDBC Driver, Spring Boot (только для DI) + +> 📖 **Подробнее о стратегии тестирования, метриках и архитектуре:** [slo-workload/README.md](../README.md) + +## Быстрый старт + +### Запуск в CI/CD + +Тест автоматически запускается через GitHub Actions при изменениях в `slo-workload/**`. + +**Workflow:** `.github/workflows/slo-test.yml` + +Пропустить тест: добавьте label `no slo` к PR. + +### Параметры + +| Параметр | Описание | Значение по умолчанию | +|----------|----------|----------------------| +| `TEST_DURATION` | Длительность теста (секунды) | 60 | +| `READ_RPS` | Read операций в секунду | 1000 | +| `WRITE_RPS` | Write операций в секунду | 100 | +| `READ_TIMEOUT` | Timeout для read (ms) | 1000 | +| `WRITE_TIMEOUT` | Timeout для write (ms) | 1000 | + +## Компоненты + +### JdbcSloTableContext +Сервисный класс для работы с таблицей `slo_table`. + +**Основные методы:** +```java +createTable(timeout) // Создание таблицы +save(row, timeout) // UPSERT с retry +select(guid, id, timeout) // SELECT с retry +``` + +**Особенности:** +- Retry с exponential backoff (5 попыток) +- Автоматическое восстановление после временных ошибок +- Подробности: см. [родительский README](../README.md#retry-механизм) + +### SloTableRow +DTO для строки таблицы (~1KB payload). +```java +SloTableRow row = SloTableRow.generate(id); +// Поля: guid, id, payloadStr, payloadDouble, payloadTimestamp +``` + +### JdbcSloTest +Основной тест JUnit, выполняющий полный цикл SLO тестирования. + +**Фазы:** +1. Инициализация таблицы +2. Warmup (10 потоков, 10s) +3. Load test (30 потоков, TEST_DURATION) +4. Валидация SLO +5. Экспорт метрик + +### MetricsReporter +Экспорт метрик в Prometheus Push Gateway. + +**Метрики:** +- `sdk_operations_total` - всего операций +- `sdk_operations_success_total` - успешных операций +- `sdk_operation_latency_seconds` - histogram latency +- `sdk_pending_operations` - активных операций + +Подробнее о метриках: [родительский README](../README.md#метрики) + +## Локальная разработка + +Разработка кода возможна локально, но **полноценный запуск теста только в CI/CD** (требуется YDB): +```bash +# Компиляция +mvn clean compile -pl slo-workload/simple-jdbc + +# Тесты (требует YDB) +mvn test -pl slo-workload/simple-jdbc -Dskip.jdbc.tests=false +``` + +## Troubleshooting + +### JDBC-специфичные проблемы + +**Connection pool exhausted** +``` +Симптом: Много ошибок "Cannot get connection from pool" +Решение: Увеличить pool size в SimpleJdbcConfig +``` + +**Prepared statement cache issues** +``` +Симптом: OutOfMemoryError: Metaspace +Решение: Ограничить кэш prepared statements +``` + +**Long GC pauses** +``` +Симптом: Периодические всплески latency P99 +Решение: Настроить JVM параметры (-XX:+UseG1GC) +``` + +### Общие проблемы + +Для общих проблем (compilation, YDB connection, метрики) см. [Troubleshooting в родительском README](../README.md#troubleshooting). + +## Развитие + +### Сравнение с другими реализациями + +После появления других workload'ов (Spring JdbcTemplate, Spring Data JDBC, JPA) можно будет сравнить: +- Overhead каждого слоя абстракции +- Trade-off между производительностью и удобством +- Рекомендации по выбору стека + +### Оптимизации + +Потенциальные улучшения этого теста: +- [ ] Batch operations для write +- [ ] Использование prepared statements +- [ ] Connection pool tuning +- [ ] Асинхронное логирование + +## Ссылки + +- [Родительский README (стратегия, метрики, архитектура)](../README.md) +- [YDB JDBC Driver](https://github.com/ydb-platform/ydb-jdbc-driver) +- [YDB Documentation](https://ydb.tech/docs/) \ No newline at end of file diff --git a/slo-workload/simple-jdbc/pom.xml b/slo-workload/simple-jdbc/pom.xml new file mode 100644 index 0000000..2d05435 --- /dev/null +++ b/slo-workload/simple-jdbc/pom.xml @@ -0,0 +1,74 @@ + + 4.0.0 + + + tech.ydb.examples + slo-workload + 1.1.0-SNAPSHOT + ../pom.xml + + + simple-jdbc + jar + Simple JDBC SLO Test + + + + + org.springframework.boot + spring-boot-starter-jdbc + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + tech.ydb.jdbc + ydb-jdbc-driver + + + + + io.prometheus + simpleclient + + + io.prometheus + simpleclient_pushgateway + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 21 + 21 + 21 + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + ${skip.jdbc.tests} + + + + + + \ No newline at end of file diff --git a/slo-workload/simple-jdbc/src/main/java/tech/ydb/slo/JdbcSloTableContext.java b/slo-workload/simple-jdbc/src/main/java/tech/ydb/slo/JdbcSloTableContext.java new file mode 100644 index 0000000..a424499 --- /dev/null +++ b/slo-workload/simple-jdbc/src/main/java/tech/ydb/slo/JdbcSloTableContext.java @@ -0,0 +1,196 @@ +package tech.ydb.slo; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Timestamp; +import java.util.UUID; + +public class JdbcSloTableContext { + + private static final String TABLE_NAME = "slo_table"; + private static final int MAX_RETRY_ATTEMPTS = 5; + private static final int INITIAL_BACKOFF_MS = 100; + + private final DataSource dataSource; + + public JdbcSloTableContext(DataSource dataSource) { + this.dataSource = dataSource; + } + + public void createTable(int operationTimeoutMs) throws SQLException { + String createTableSql = String.format(""" + CREATE TABLE `%s` ( + Guid Utf8, + Id Int32, + PayloadStr Utf8, + PayloadDouble Double, + PayloadTimestamp Timestamp, + PRIMARY KEY (Guid, Id) + ) + """, TABLE_NAME); + + try (Connection conn = dataSource.getConnection(); + Statement stmt = conn.createStatement()) { + stmt.setQueryTimeout(operationTimeoutMs / 1000); + stmt.execute(createTableSql); + } + } + + public int save(SloTableRow row, int writeTimeoutMs) throws SQLException { + String upsertSql = String.format(""" + UPSERT INTO `%s` (Guid, Id, PayloadStr, PayloadDouble, PayloadTimestamp) + VALUES (?, ?, ?, ?, ?) + """, TABLE_NAME); + + int attempts = 0; + SQLException lastException = null; + + while (attempts < MAX_RETRY_ATTEMPTS) { + attempts++; + + try (Connection conn = dataSource.getConnection(); + PreparedStatement stmt = conn.prepareStatement(upsertSql)) { + + stmt.setQueryTimeout(writeTimeoutMs / 1000); + stmt.setString(1, row.guid.toString()); + stmt.setInt(2, row.id); + stmt.setString(3, row.payloadStr); + stmt.setDouble(4, row.payloadDouble); + stmt.setTimestamp(5, new Timestamp(row.payloadTimestamp.getTime())); + + stmt.executeUpdate(); + return attempts; + + } catch (SQLException e) { + lastException = e; + + if (!isRetryableError(e) || attempts >= MAX_RETRY_ATTEMPTS) { + throw new SQLException("Failed to save after " + attempts + " attempts", e); + } + + try { + long backoffMs = INITIAL_BACKOFF_MS * (1L << (attempts - 1)); + Thread.sleep(backoffMs); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + throw new SQLException("Interrupted during retry", ie); + } + } + } + + throw new SQLException("Failed to save after " + attempts + " attempts", lastException); + } + + public SloTableRow select(UUID guid, int id, int readTimeoutMs) throws SQLException { + String selectSql = String.format(""" + SELECT Guid, Id, PayloadStr, PayloadDouble, PayloadTimestamp + FROM `%s` WHERE Guid = ? AND Id = ? + """, TABLE_NAME); + + int attempts = 0; + SQLException lastException = null; + + while (attempts < MAX_RETRY_ATTEMPTS) { + attempts++; + + try (Connection conn = dataSource.getConnection(); + PreparedStatement stmt = conn.prepareStatement(selectSql)) { + + stmt.setQueryTimeout(readTimeoutMs / 1000); + stmt.setString(1, guid.toString()); + stmt.setInt(2, id); + + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + SloTableRow row = new SloTableRow(); + row.guid = UUID.fromString(rs.getString("Guid")); + row.id = rs.getInt("Id"); + row.payloadStr = rs.getString("PayloadStr"); + row.payloadDouble = rs.getDouble("PayloadDouble"); + row.payloadTimestamp = rs.getTimestamp("PayloadTimestamp"); + return row; + } else { + throw new SQLException("Row not found: guid=" + guid + ", id=" + id); + } + } + + } catch (SQLException e) { + lastException = e; + + if (!isRetryableError(e) || attempts >= MAX_RETRY_ATTEMPTS) { + throw new SQLException("Failed to select after " + attempts + " attempts", e); + } + + try { + long backoffMs = INITIAL_BACKOFF_MS * (1L << (attempts - 1)); + Thread.sleep(backoffMs); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + throw new SQLException("Interrupted during retry", ie); + } + } + } + + throw new SQLException("Failed to select after " + attempts + " attempts", lastException); + } + + public int selectCount() throws SQLException { + String selectSql = String.format("SELECT COUNT(*) as cnt FROM `%s`", TABLE_NAME); + + try (Connection conn = dataSource.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(selectSql)) { + + return rs.next() ? rs.getInt("cnt") : 0; + } + } + + public boolean tableExists() { + String checkSql = String.format("SELECT 1 FROM `%s` WHERE 1=0", TABLE_NAME); + + try (Connection conn = dataSource.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(checkSql)) { + return true; + } catch (SQLException e) { + return false; + } + } + + private boolean isRetryableError(SQLException e) { + String message = e.getMessage().toLowerCase(); + String sqlState = e.getSQLState(); + + if (message.contains("timeout") || + message.contains("connection") || + message.contains("network") || + message.contains("unavailable") || + message.contains("overload") || + message.contains("too many requests") || + message.contains("throttle")) { + return true; + } + + if (message.contains("session") && message.contains("expired")) { + return true; + } + + if (sqlState != null && sqlState.startsWith("YDB")) { + return true; + } + + if (message.contains("already exists") || + message.contains("not found") || + message.contains("syntax error") || + message.contains("constraint") || + message.contains("duplicate key")) { + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/slo-workload/simple-jdbc/src/main/java/tech/ydb/slo/SloTableRow.java b/slo-workload/simple-jdbc/src/main/java/tech/ydb/slo/SloTableRow.java new file mode 100644 index 0000000..0b37a23 --- /dev/null +++ b/slo-workload/simple-jdbc/src/main/java/tech/ydb/slo/SloTableRow.java @@ -0,0 +1,57 @@ +package tech.ydb.slo; + +import java.sql.Timestamp; +import java.util.UUID; + +/** + * Представление строки таблицы для SLO тестов + */ +public class SloTableRow { + public UUID guid; + public int id; + public String payloadStr; + public double payloadDouble; + public Timestamp payloadTimestamp; + + public SloTableRow() { + } + + /** + * Создание строки с заданными значениями + */ + public SloTableRow(UUID guid, int id, String payloadStr, double payloadDouble, Timestamp payloadTimestamp) { + this.guid = guid; + this.id = id; + this.payloadStr = payloadStr; + this.payloadDouble = payloadDouble; + this.payloadTimestamp = payloadTimestamp; + } + + /** + * Генерация случайной строки для тестирования + */ + public static SloTableRow generate(int id) { + return new SloTableRow( + UUID.randomUUID(), + id, + generatePayloadString(1024), // 1KB payload + Math.random() * 1000.0, + new Timestamp(System.currentTimeMillis()) + ); + } + + /** + * Генерация строки заданного размера + */ + private static String generatePayloadString(int sizeBytes) { + StringBuilder sb = new StringBuilder(sizeBytes); + String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + for (int i = 0; i < sizeBytes; i++) { + int index = (int) (Math.random() * chars.length()); + sb.append(chars.charAt(index)); + } + + return sb.toString(); + } +} \ No newline at end of file diff --git a/slo-workload/simple-jdbc/src/test/java/tech/ydb/slo/JdbcSloTest.java b/slo-workload/simple-jdbc/src/test/java/tech/ydb/slo/JdbcSloTest.java new file mode 100644 index 0000000..52002cc --- /dev/null +++ b/slo-workload/simple-jdbc/src/test/java/tech/ydb/slo/JdbcSloTest.java @@ -0,0 +1,292 @@ +package tech.ydb.slo; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import javax.sql.DataSource; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest(classes = SimpleJdbcConfig.class) +public class JdbcSloTest { + + @Autowired + DataSource dataSource; + + private static int testDuration; + private static int readRps; + private static int writeRps; + private static int readTimeout; + private static int writeTimeout; + private static String promPgw; + private static int initialDataCount; + private static int reportPeriod; + + private static final double MAX_P50_LATENCY_MS = 10.0; + private static final double MAX_P95_LATENCY_MS = 50.0; + private static final double MAX_P99_LATENCY_MS = 100.0; + private static final double MIN_SUCCESS_RATE = 99.9; + + @BeforeAll + static void setup() { + testDuration = Integer.parseInt(System.getenv().getOrDefault("TEST_DURATION", "60")); + readRps = Integer.parseInt(System.getenv().getOrDefault("READ_RPS", "100")); + writeRps = Integer.parseInt(System.getenv().getOrDefault("WRITE_RPS", "10")); + readTimeout = Integer.parseInt(System.getenv().getOrDefault("READ_TIMEOUT", "1000")); + writeTimeout = Integer.parseInt(System.getenv().getOrDefault("WRITE_TIMEOUT", "1000")); + promPgw = System.getenv().getOrDefault("PROM_PGW", "http://localhost:9091"); + reportPeriod = Integer.parseInt(System.getenv().getOrDefault("REPORT_PERIOD", "10000")); + initialDataCount = Math.max(100, writeRps * testDuration / 10); + } + + @Test + void sloFullTest() throws Exception { + JdbcSloTableContext context = new JdbcSloTableContext(dataSource); + String jobName = System.getenv().getOrDefault("WORKLOAD_NAME", "jdbc-slo-test"); + MetricsReporter metrics = new MetricsReporter(promPgw, jobName); + + context.createTable(writeTimeout); + assertTrue(context.tableExists()); + + List testData = new ArrayList<>(); + for (int i = 0; i < initialDataCount; i++) { + testData.add(SloTableRow.generate(i)); + } + + writeInitialData(context, testData, writeTimeout, metrics); + assertTrue(context.selectCount() >= testData.size() * 0.99); + + SloTestResult result = runSloTest(context, testData, metrics); + + validateSloResults(result); + + int finalCount = context.selectCount(); + int expectedMinCount = initialDataCount + (int)(writeRps * testDuration * 0.95); + assertTrue(finalCount >= expectedMinCount); + + metrics.push(); + metrics.saveToFile("target/test-metrics.txt", result.avgLatencySeconds); + } + + private void writeInitialData( + JdbcSloTableContext context, + List data, + int timeout, + MetricsReporter metrics + ) throws InterruptedException, ExecutionException { + + ExecutorService executor = Executors.newFixedThreadPool(10); + List> futures = new ArrayList<>(); + AtomicInteger errors = new AtomicInteger(0); + + for (SloTableRow row : data) { + futures.add(executor.submit(() -> { + long start = System.nanoTime(); + try { + context.save(row, timeout); + metrics.recordSuccess("write_initial", (System.nanoTime() - start) / 1_000_000_000.0); + } catch (SQLException e) { + errors.incrementAndGet(); + metrics.recordError("write_initial", e.getClass().getSimpleName()); + } + return null; + })); + } + + for (Future future : futures) { + future.get(); + } + + executor.shutdown(); + executor.awaitTermination(1, TimeUnit.MINUTES); + + if (errors.get() > data.size() * 0.01) { + throw new RuntimeException("Too many errors: " + errors.get()); + } + } + + private SloTestResult runSloTest( + JdbcSloTableContext context, + List testData, + MetricsReporter metrics + ) throws InterruptedException, ExecutionException { + + ExecutorService executor = Executors.newFixedThreadPool(30); + + AtomicInteger successCount = new AtomicInteger(0); + AtomicInteger errorCount = new AtomicInteger(0); + AtomicLong totalLatencyNanos = new AtomicLong(0); + AtomicInteger totalAttempts = new AtomicInteger(0); + List latencies = new CopyOnWriteArrayList<>(); + + long testStartTime = System.currentTimeMillis(); + long testEndTime = testStartTime + (testDuration * 1000L); + long lastReportTime = testStartTime; + + int nextWriteId = testData.size(); + List> activeFutures = new ArrayList<>(); + + while (System.currentTimeMillis() < testEndTime) { + long iterationStart = System.currentTimeMillis(); + + for (int i = 0; i < readRps && System.currentTimeMillis() < testEndTime; i++) { + SloTableRow row = testData.get(ThreadLocalRandom.current().nextInt(testData.size())); + + activeFutures.add(executor.submit(() -> { + long opStart = System.nanoTime(); + try { + context.select(row.guid, row.id, readTimeout); + long opEnd = System.nanoTime(); + double latency = (opEnd - opStart) / 1_000_000_000.0; + + successCount.incrementAndGet(); + totalLatencyNanos.addAndGet(opEnd - opStart); + latencies.add(latency); + metrics.recordSuccess("read", latency); + } catch (SQLException e) { + errorCount.incrementAndGet(); + metrics.recordError("read", e.getClass().getSimpleName()); + } + })); + } + + for (int i = 0; i < writeRps && System.currentTimeMillis() < testEndTime; i++) { + final int writeId = nextWriteId++; + SloTableRow row = SloTableRow.generate(writeId); + + activeFutures.add(executor.submit(() -> { + long opStart = System.nanoTime(); + try { + int attempts = context.save(row, writeTimeout); + long opEnd = System.nanoTime(); + double latency = (opEnd - opStart) / 1_000_000_000.0; + + successCount.incrementAndGet(); + totalLatencyNanos.addAndGet(opEnd - opStart); + totalAttempts.addAndGet(attempts); + latencies.add(latency); + metrics.recordSuccess("write", latency); + } catch (SQLException e) { + errorCount.incrementAndGet(); + metrics.recordError("write", e.getClass().getSimpleName()); + } + })); + } + + long now = System.currentTimeMillis(); + if (now - lastReportTime >= reportPeriod) { + metrics.push(); + lastReportTime = now; + } + + long iterationDuration = System.currentTimeMillis() - iterationStart; + if (iterationDuration < 1000) { + Thread.sleep(1000 - iterationDuration); + } + } + + for (Future future : activeFutures) { + try { + future.get(writeTimeout * 2L, TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + future.cancel(true); + } catch (Exception ignored) { + } + } + + executor.shutdown(); + executor.awaitTermination(30, TimeUnit.SECONDS); + + return calculateMetrics(successCount.get(), errorCount.get(), + totalLatencyNanos.get(), totalAttempts.get(), latencies); + } + + private SloTestResult calculateMetrics( + int successCount, + int errorCount, + long totalLatencyNanos, + int totalAttempts, + List latencies + ) { + List sortedLatencies = new ArrayList<>(latencies); + sortedLatencies.sort(Double::compareTo); + + int totalRequests = successCount + errorCount; + double avgLatency = totalRequests > 0 ? + totalLatencyNanos / 1_000_000_000.0 / totalRequests : 0.0; + double avgAttempts = successCount > 0 ? + (double)totalAttempts / successCount : 0.0; + + return new SloTestResult( + successCount, + errorCount, + totalRequests, + avgLatency, + avgAttempts, + getPercentile(sortedLatencies, 0.50), + getPercentile(sortedLatencies, 0.95), + getPercentile(sortedLatencies, 0.99), + totalRequests > 0 ? (double)successCount / totalRequests * 100.0 : 0.0 + ); + } + + private double getPercentile(List sortedValues, double percentile) { + if (sortedValues.isEmpty()) return 0.0; + int index = (int) Math.ceil(sortedValues.size() * percentile) - 1; + index = Math.max(0, Math.min(index, sortedValues.size() - 1)); + return sortedValues.get(index); + } + + private void validateSloResults(SloTestResult result) { + assertTrue(result.p50Latency * 1000 <= MAX_P50_LATENCY_MS, + String.format("P50 latency %.2fms exceeds threshold %.0fms", + result.p50Latency * 1000, MAX_P50_LATENCY_MS)); + + assertTrue(result.p95Latency * 1000 <= MAX_P95_LATENCY_MS, + String.format("P95 latency %.2fms exceeds threshold %.0fms", + result.p95Latency * 1000, MAX_P95_LATENCY_MS)); + + assertTrue(result.p99Latency * 1000 <= MAX_P99_LATENCY_MS, + String.format("P99 latency %.2fms exceeds threshold %.0fms", + result.p99Latency * 1000, MAX_P99_LATENCY_MS)); + + assertTrue(result.successRate >= MIN_SUCCESS_RATE, + String.format("Success rate %.2f%% below threshold %.1f%%", + result.successRate, MIN_SUCCESS_RATE)); + } + + static class SloTestResult { + final int successCount; + final int errorCount; + final int totalRequests; + final double avgLatencySeconds; + final double avgAttempts; + final double p50Latency; + final double p95Latency; + final double p99Latency; + final double successRate; + + SloTestResult(int successCount, int errorCount, int totalRequests, + double avgLatencySeconds, double avgAttempts, + double p50, double p95, double p99, double successRate) { + this.successCount = successCount; + this.errorCount = errorCount; + this.totalRequests = totalRequests; + this.avgLatencySeconds = avgLatencySeconds; + this.avgAttempts = avgAttempts; + this.p50Latency = p50; + this.p95Latency = p95; + this.p99Latency = p99; + this.successRate = successRate; + } + } +} \ No newline at end of file diff --git a/slo-workload/simple-jdbc/src/test/java/tech/ydb/slo/MetricsReporter.java b/slo-workload/simple-jdbc/src/test/java/tech/ydb/slo/MetricsReporter.java new file mode 100644 index 0000000..26ced2c --- /dev/null +++ b/slo-workload/simple-jdbc/src/test/java/tech/ydb/slo/MetricsReporter.java @@ -0,0 +1,155 @@ +package tech.ydb.slo; + +import io.prometheus.client.CollectorRegistry; +import io.prometheus.client.Counter; +import io.prometheus.client.Gauge; +import io.prometheus.client.Histogram; +import io.prometheus.client.exporter.PushGateway; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.net.URI; +import java.net.URL; +import java.util.Map; + +public class MetricsReporter { + private static final Logger log = LoggerFactory.getLogger(MetricsReporter.class); + + private final CollectorRegistry registry = new CollectorRegistry(); + private final PushGateway pushGateway; + private final String jobName; + private final String javaVersion; + + private final Counter operationsTotal; + private final Counter operationsSuccessTotal; + private final Histogram operationLatencySeconds; + private final Gauge pendingOperations; + + private int totalSuccess = 0; + private int totalErrors = 0; + + public MetricsReporter(String promPgwUrl, String jobName) { + this.jobName = jobName; + this.javaVersion = System.getProperty("java.version"); + + try { + URL url = URI.create(promPgwUrl).toURL(); + this.pushGateway = new PushGateway(url); + log.info("Initialized PushGateway: {}", promPgwUrl); + } catch (Exception e) { + throw new RuntimeException("Failed to initialize PushGateway: " + promPgwUrl, e); + } + + // Total operations (including errors) + this.operationsTotal = Counter.build() + .name("sdk_operations_total") + .labelNames("operation_type", "sdk", "sdk_version", "workload", "workload_version") + .help("Total number of operations performed by the SDK") + .register(registry); + + // Successful operations only + this.operationsSuccessTotal = Counter.build() + .name("sdk_operations_success_total") + .labelNames("operation_type", "sdk", "sdk_version", "workload", "workload_version") + .help("Total number of successful operations") + .register(registry); + + // Operation latency + this.operationLatencySeconds = Histogram.build() + .name("sdk_operation_latency_seconds") + .labelNames("operation_type", "operation_status", "sdk", "sdk_version", "workload", "workload_version") + .help("Operation latency in seconds") + .buckets(0.001, 0.002, 0.003, 0.004, 0.005, 0.0075, 0.010, 0.020, 0.050, 0.100, 0.200, 0.500, 1.000) + .register(registry); + + // Pending operations gauge + this.pendingOperations = Gauge.build() + .name("sdk_pending_operations") + .labelNames("operation_type", "sdk", "sdk_version", "workload", "workload_version") + .help("Current number of pending operations") + .register(registry); + } + + /** + * Record successful operation + */ + public void recordSuccess(String operation, double latencySeconds) { + operationsTotal.labels(operation, "java", javaVersion, jobName, "0.0.0").inc(); + operationsSuccessTotal.labels(operation, "java", javaVersion, jobName, "0.0.0").inc(); + operationLatencySeconds.labels(operation, "success", "java", javaVersion, jobName, "0.0.0") + .observe(latencySeconds); + + totalSuccess++; + + // Log only at milestones + if (totalSuccess % 1000 == 0) { + log.info("{} operations completed successfully (avg latency: {:.2f} ms)", + totalSuccess, latencySeconds * 1000); + } + } + + /** + * Record failed operation + */ + public void recordError(String operation, String errorType) { + operationsTotal.labels(operation, "java", javaVersion, jobName, "0.0.0").inc(); + totalErrors++; + + log.warn("Operation failed: {} (type: {}, total errors: {})", operation, errorType, totalErrors); + } + + /** + * Set pending operations count + */ + public void setPendingOperations(String operationType, int count) { + pendingOperations.labels(operationType, "java", javaVersion, jobName, "0.0.0").set(count); + } + + /** + * Push metrics to Prometheus Push Gateway + */ + public void push() { + try { + pushGateway.pushAdd( + registry, + jobName, + Map.of( + "workload", jobName, + "instance", "jdbc" + ) + ); + log.info("Metrics pushed to Prometheus (success: {}, errors: {})", totalSuccess, totalErrors); + } catch (IOException e) { + log.error("Failed to push metrics to Prometheus", e); + } + } + + /** + * Save metrics summary to file + */ + public void saveToFile(String filename, double latencySeconds) { + try (PrintWriter writer = new PrintWriter(new FileWriter(filename))) { + writer.println("SUCCESS_COUNT=" + totalSuccess); + writer.println("ERROR_COUNT=" + totalErrors); + writer.println("LATENCY_MS=" + String.format("%.2f", latencySeconds * 1000)); + writer.println("PENDING_OPERATIONS=0"); + + log.info("Metrics saved to {} (success: {}, errors: {}, latency: {:.2f} ms)", + filename, totalSuccess, totalErrors, latencySeconds * 1000); + } catch (IOException e) { + log.error("Failed to save metrics to file: {}", filename, e); + } + } + + /** + * Get summary statistics + */ + public String getSummary() { + return String.format("Success: %d, Errors: %d, Error rate: %.2f%%", + totalSuccess, totalErrors, + totalSuccess > 0 ? (totalErrors * 100.0 / (totalSuccess + totalErrors)) : 0.0); + } +} \ No newline at end of file diff --git a/slo-workload/simple-jdbc/src/test/java/tech/ydb/slo/SimpleJdbcConfig.java b/slo-workload/simple-jdbc/src/test/java/tech/ydb/slo/SimpleJdbcConfig.java new file mode 100644 index 0000000..4f64923 --- /dev/null +++ b/slo-workload/simple-jdbc/src/test/java/tech/ydb/slo/SimpleJdbcConfig.java @@ -0,0 +1,32 @@ +package tech.ydb.slo; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DriverManagerDataSource; + +import javax.sql.DataSource; + +@Configuration +public class SimpleJdbcConfig { + + @Value("${spring.datasource.url}") + private String jdbcUrl; + + @Value("${spring.datasource.driver-class-name}") + private String driverClassName; + + @Bean + public DataSource dataSource() { + DriverManagerDataSource ds = new DriverManagerDataSource(); + ds.setDriverClassName(driverClassName); + ds.setUrl(jdbcUrl); + return ds; + } + + @Bean + public JdbcTemplate jdbcTemplate(DataSource ds) { + return new JdbcTemplate(ds); + } +} diff --git a/slo-workload/simple-jdbc/src/test/resources/application.yaml b/slo-workload/simple-jdbc/src/test/resources/application.yaml new file mode 100644 index 0000000..df03a3b --- /dev/null +++ b/slo-workload/simple-jdbc/src/test/resources/application.yaml @@ -0,0 +1,4 @@ +spring: + datasource: + driver-class-name: tech.ydb.jdbc.YdbDriver + url: ${YDB_JDBC_URL:jdbc:ydb:grpc://localhost:2136/Root/testdb}