From 785eceab5eb55382d439abb6232bde13999ed72b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B2=D0=B0=D0=BD=20=D0=92=D0=B0=D1=81=D0=B8=D0=BB?= =?UTF-8?q?=D1=8C=D0=B5=D0=B2?= Date: Tue, 13 Jan 2026 10:17:40 +0800 Subject: [PATCH 01/13] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=B1=D0=B0?= =?UTF-8?q?=D0=B7=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 1 + slo/README.md | 19 ++ slo/pom.xml | 30 ++ slo/simple-jdbc-test/README.md | 85 +++++ slo/simple-jdbc-test/pom.xml | 100 ++++++ .../tech/ydb/slo/JdbcSloTableContext.java | 196 ++++++++++++ .../main/java/tech/ydb/slo/SloTableRow.java | 57 ++++ .../test/java/tech/ydb/slo/JdbcSloTest.java | 292 ++++++++++++++++++ .../java/tech/ydb/slo/MetricsReporter.java | 167 ++++++++++ .../java/tech/ydb/slo/SimpleJdbcConfig.java | 32 ++ .../src/test/resources/application.yaml | 4 + 11 files changed, 983 insertions(+) create mode 100644 slo/README.md create mode 100644 slo/pom.xml create mode 100644 slo/simple-jdbc-test/README.md create mode 100644 slo/simple-jdbc-test/pom.xml create mode 100644 slo/simple-jdbc-test/src/main/java/tech/ydb/slo/JdbcSloTableContext.java create mode 100644 slo/simple-jdbc-test/src/main/java/tech/ydb/slo/SloTableRow.java create mode 100644 slo/simple-jdbc-test/src/test/java/tech/ydb/slo/JdbcSloTest.java create mode 100644 slo/simple-jdbc-test/src/test/java/tech/ydb/slo/MetricsReporter.java create mode 100644 slo/simple-jdbc-test/src/test/java/tech/ydb/slo/SimpleJdbcConfig.java create mode 100644 slo/simple-jdbc-test/src/test/resources/application.yaml diff --git a/pom.xml b/pom.xml index 8978897..a725e61 100644 --- a/pom.xml +++ b/pom.xml @@ -30,6 +30,7 @@ url-shortener-demo jdbc project-course + slo diff --git a/slo/README.md b/slo/README.md new file mode 100644 index 0000000..e1f8fd2 --- /dev/null +++ b/slo/README.md @@ -0,0 +1,19 @@ + +# Пробная реализация SLO + +Проект представляет собой пробный подход к решению задачи проверки SLO для SDK. +В состав модулей SDK добавлен модуль **slo**. +Модуль должен быть подключен в числе дочерних модулей ydb-java-sdk + - slo + +Запуск модулей slo осуществляется в задаче [slo.yml](../.github/workflows/slo.yml) + +В настоящее время реализован только один тест .. [simple-jdbc-test](simple-jdbc-test) +по аналогии с AdoNet проверяющий базовое JDBC подключение. + +В развитие просятся модули slo для подключений c использованием: + - JDBCTemplate + - Spring Data JDBC + - Spring Data JPA + - +Думаю, ими можно заняться после согласования simple-jdbc-test \ No newline at end of file diff --git a/slo/pom.xml b/slo/pom.xml new file mode 100644 index 0000000..5ae1e8f --- /dev/null +++ b/slo/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + + tech.ydb + ydb-sdk-parent + 2.3.30-SNAPSHOT + + + + 21 + + + pom + + tech.ydb.slo + ydb-sdk-slo + 2.3.30-SNAPSHOT + YDB SLO Tests + + + simple-jdbc-test + + + diff --git a/slo/simple-jdbc-test/README.md b/slo/simple-jdbc-test/README.md new file mode 100644 index 0000000..4e8cd8b --- /dev/null +++ b/slo/simple-jdbc-test/README.md @@ -0,0 +1,85 @@ +# simple-jdbc-test + +Модуль, содержащий полноценный SLO тест с использованием JDBC подключения к YDB. + +## Компоненты + +### JdbcSloTableContext +Сервисный класс для работы с таблицей `slo_table`. Реализует retry logic с exponential backoff. + +**Методы:** +- `createTable(timeout)` - создание таблицы с составным ключом (Guid, Id) +- `save(row, timeout)` - UPSERT запись с автоматическим retry (до 5 попыток) +- `select(guid, id, timeout)` - чтение записи по ключу с retry +- `selectCount()` - подсчёт общего количества записей +- `tableExists()` - проверка существования таблицы +- `isRetryableError(exception)` - определение временных ошибок (timeout, network, overload) + +**Retry стратегия:** +- Максимум попыток: 5 +- Backoff: 100ms → 200ms → 400ms → 800ms → 1600ms +- Повторяет: timeout, connection, network, overload, session expired +- Не повторяет: schema errors, constraint violations, syntax errors + +### SloTableRow +Data Transfer Object для строки таблицы. Используется для тестирования и будущих workload'ов. + +**Поля:** +- `guid: UUID` - уникальный идентификатор +- `id: int` - порядковый номер +- `payloadStr: String` - строковая нагрузка (~1KB) +- `payloadDouble: double` - числовая нагрузка +- `payloadTimestamp: Timestamp` - время создания + +**Методы:** +- `generate(id)` - генерация случайной строки с заданным id +- `generatePayloadString(size)` - создание payload заданного размера + +### JdbcSloTest +Основной SLO тест. Выполняет полный цикл нагрузочного тестирования. + +**Фазы выполнения:** +1. **Table Initialization** - создание таблицы +2. **Data Preparation** - генерация и запись начальных данных +3. **SLO Test Execution** - параллельная нагрузка (read + write) в течение заданного времени +4. **Results Validation** - проверка соответствия SLO порогам +5. **Metrics Export** - отправка метрик в Prometheus и сохранение в файл + +**SLO пороги:** +- P50 Latency: < 10ms +- P95 Latency: < 50ms +- P99 Latency: < 100ms +- Success Rate: > 99.9% + +**Параметры окружения:** +- `TEST_DURATION` - длительность теста в секундах (default: 60) +- `READ_RPS` - read операций в секунду (default: 100) +- `WRITE_RPS` - write операций в секунду (default: 10) +- `READ_TIMEOUT` - timeout для read в ms (default: 1000) +- `WRITE_TIMEOUT` - timeout для write в ms (default: 1000) +- `PROM_PGW` - URL Prometheus Push Gateway (default: http://localhost:9091) +- `REPORT_PERIOD` - период отправки метрик в ms (default: 10000) +- `YDB_JDBC_URL` - строка подключения к YDB + +### MetricsReporter +Класс для сбора и отправки метрик в Prometheus Push Gateway. + +**Метрики:** +- `jdbc_test_success_total` - Counter успешных операций (labels: operation) +- `jdbc_test_errors_total` - Counter ошибок (labels: operation, error_type) +- `jdbc_test_latency_seconds` - Histogram latency (labels: operation) +- `jdbc_test_active_connections` - Gauge активных подключений + +**Методы:** +- `recordSuccess(operation, latency)` - запись успешной операции +- `recordError(operation, errorType)` - запись ошибки +- `push()` - отправка метрик в Prometheus (полная замена) +- `pushAdd()` - инкрементальное обновление метрик +- `saveToFile(filename, latency)` - сохранение в файл для GitHub Summary + +### SimpleJdbcConfig +Spring конфигурация для DataSource и JdbcTemplate. + +**Beans:** +- `dataSource()` - DriverManagerDataSource для YDB JDBC Driver +- `jdbcTemplate(dataSource)` - Spring JdbcTemplate (для будущего использования) \ No newline at end of file diff --git a/slo/simple-jdbc-test/pom.xml b/slo/simple-jdbc-test/pom.xml new file mode 100644 index 0000000..901dbef --- /dev/null +++ b/slo/simple-jdbc-test/pom.xml @@ -0,0 +1,100 @@ + + + 4.0.0 + + tech.ydb.slo + simple-jdbc-test + 1.0-SNAPSHOT + jar + Simple JDBC Test + + + 21 + 21 + 21 + UTF-8 + + 3.5.9 + 5.12.2 + true + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-starter-jdbc + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + tech.ydb.jdbc + ydb-jdbc-driver + 2.3.20 + + + + + io.prometheus + simpleclient + 0.16.0 + + + + + io.prometheus + simpleclient_pushgateway + 0.16.0 + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 21 + 21 + 21 + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.1.2 + + ${skip.jdbc.tests} + + + + + + \ No newline at end of file diff --git a/slo/simple-jdbc-test/src/main/java/tech/ydb/slo/JdbcSloTableContext.java b/slo/simple-jdbc-test/src/main/java/tech/ydb/slo/JdbcSloTableContext.java new file mode 100644 index 0000000..a424499 --- /dev/null +++ b/slo/simple-jdbc-test/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/simple-jdbc-test/src/main/java/tech/ydb/slo/SloTableRow.java b/slo/simple-jdbc-test/src/main/java/tech/ydb/slo/SloTableRow.java new file mode 100644 index 0000000..0b37a23 --- /dev/null +++ b/slo/simple-jdbc-test/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/simple-jdbc-test/src/test/java/tech/ydb/slo/JdbcSloTest.java b/slo/simple-jdbc-test/src/test/java/tech/ydb/slo/JdbcSloTest.java new file mode 100644 index 0000000..52002cc --- /dev/null +++ b/slo/simple-jdbc-test/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/simple-jdbc-test/src/test/java/tech/ydb/slo/MetricsReporter.java b/slo/simple-jdbc-test/src/test/java/tech/ydb/slo/MetricsReporter.java new file mode 100644 index 0000000..b3a0d5f --- /dev/null +++ b/slo/simple-jdbc-test/src/test/java/tech/ydb/slo/MetricsReporter.java @@ -0,0 +1,167 @@ +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); + } 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) { + // Increment total operations + operationsTotal.labels(operation, "java", javaVersion, jobName, "0.0.0").inc(); + + // Increment successful operations + operationsSuccessTotal.labels(operation, "java", javaVersion, jobName, "0.0.0").inc(); + + // Record latency + operationLatencySeconds.labels(operation, "success", "java", javaVersion, jobName, "0.0.0") + .observe(latencySeconds); + + totalSuccess++; + + // Log every 100 operations + if (totalSuccess % 100 == 0) { + System.out.printf("✅ [%s] Success #%d, latency: %.3f ms%n", + operation, totalSuccess, latencySeconds * 1000); + } + } + + /** + * Record failed operation + */ + public void recordError(String operation, String errorType) { + // Increment total operations (errors are also operations) + operationsTotal.labels(operation, "java", javaVersion, jobName, "0.0.0").inc(); + + // Note: We could add error latency here if we tracked it + // For now, we just count the error + + totalErrors++; + + // Log errors + System.out.printf("❌ [%s] Error #%d, type: %s%n", + operation, totalErrors, errorType); + } + + /** + * 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" + ) + ); + + System.out.println("📤 Metrics pushed to Prometheus"); + System.out.println(" Success total: " + totalSuccess); + System.out.println(" Errors total: " + totalErrors); + + log.debug("Metrics pushed to Prometheus"); + } catch (IOException e) { + System.err.println("❌ Failed to push metrics: " + e.getMessage()); + log.error("Failed to push metrics", 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"); + + System.out.println("💾 Metrics saved to file:"); + System.out.println(" Success: " + totalSuccess); + System.out.println(" Errors: " + totalErrors); + System.out.println(" Latency: " + String.format("%.2f ms", latencySeconds * 1000)); + + log.info("Metrics saved to {}", filename); + } catch (IOException e) { + log.error("Failed to save metrics to file", e); + } + } +} \ No newline at end of file diff --git a/slo/simple-jdbc-test/src/test/java/tech/ydb/slo/SimpleJdbcConfig.java b/slo/simple-jdbc-test/src/test/java/tech/ydb/slo/SimpleJdbcConfig.java new file mode 100644 index 0000000..4f64923 --- /dev/null +++ b/slo/simple-jdbc-test/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/simple-jdbc-test/src/test/resources/application.yaml b/slo/simple-jdbc-test/src/test/resources/application.yaml new file mode 100644 index 0000000..df03a3b --- /dev/null +++ b/slo/simple-jdbc-test/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} From 9aca2e6b7696082f42401cce5878c795f87fd987 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B2=D0=B0=D0=BD=20=D0=92=D0=B0=D1=81=D0=B8=D0=BB?= =?UTF-8?q?=D1=8C=D0=B5=D0=B2?= Date: Tue, 13 Jan 2026 10:20:29 +0800 Subject: [PATCH 02/13] Rebased from ydb-java-sdk --- pom.xml | 2 +- {slo => slo1}/README.md | 0 {slo => slo1}/pom.xml | 0 {slo => slo1}/simple-jdbc-test/README.md | 0 {slo => slo1}/simple-jdbc-test/pom.xml | 0 .../src/main/java/tech/ydb/slo/JdbcSloTableContext.java | 0 .../src/main/java/tech/ydb/slo/SloTableRow.java | 0 .../src/test/java/tech/ydb/slo/JdbcSloTest.java | 0 .../src/test/java/tech/ydb/slo/MetricsReporter.java | 0 .../src/test/java/tech/ydb/slo/SimpleJdbcConfig.java | 0 .../simple-jdbc-test/src/test/resources/application.yaml | 0 11 files changed, 1 insertion(+), 1 deletion(-) rename {slo => slo1}/README.md (100%) rename {slo => slo1}/pom.xml (100%) rename {slo => slo1}/simple-jdbc-test/README.md (100%) rename {slo => slo1}/simple-jdbc-test/pom.xml (100%) rename {slo => slo1}/simple-jdbc-test/src/main/java/tech/ydb/slo/JdbcSloTableContext.java (100%) rename {slo => slo1}/simple-jdbc-test/src/main/java/tech/ydb/slo/SloTableRow.java (100%) rename {slo => slo1}/simple-jdbc-test/src/test/java/tech/ydb/slo/JdbcSloTest.java (100%) rename {slo => slo1}/simple-jdbc-test/src/test/java/tech/ydb/slo/MetricsReporter.java (100%) rename {slo => slo1}/simple-jdbc-test/src/test/java/tech/ydb/slo/SimpleJdbcConfig.java (100%) rename {slo => slo1}/simple-jdbc-test/src/test/resources/application.yaml (100%) diff --git a/pom.xml b/pom.xml index a725e61..50ac34f 100644 --- a/pom.xml +++ b/pom.xml @@ -30,7 +30,7 @@ url-shortener-demo jdbc project-course - slo + slo1 diff --git a/slo/README.md b/slo1/README.md similarity index 100% rename from slo/README.md rename to slo1/README.md diff --git a/slo/pom.xml b/slo1/pom.xml similarity index 100% rename from slo/pom.xml rename to slo1/pom.xml diff --git a/slo/simple-jdbc-test/README.md b/slo1/simple-jdbc-test/README.md similarity index 100% rename from slo/simple-jdbc-test/README.md rename to slo1/simple-jdbc-test/README.md diff --git a/slo/simple-jdbc-test/pom.xml b/slo1/simple-jdbc-test/pom.xml similarity index 100% rename from slo/simple-jdbc-test/pom.xml rename to slo1/simple-jdbc-test/pom.xml diff --git a/slo/simple-jdbc-test/src/main/java/tech/ydb/slo/JdbcSloTableContext.java b/slo1/simple-jdbc-test/src/main/java/tech/ydb/slo/JdbcSloTableContext.java similarity index 100% rename from slo/simple-jdbc-test/src/main/java/tech/ydb/slo/JdbcSloTableContext.java rename to slo1/simple-jdbc-test/src/main/java/tech/ydb/slo/JdbcSloTableContext.java diff --git a/slo/simple-jdbc-test/src/main/java/tech/ydb/slo/SloTableRow.java b/slo1/simple-jdbc-test/src/main/java/tech/ydb/slo/SloTableRow.java similarity index 100% rename from slo/simple-jdbc-test/src/main/java/tech/ydb/slo/SloTableRow.java rename to slo1/simple-jdbc-test/src/main/java/tech/ydb/slo/SloTableRow.java diff --git a/slo/simple-jdbc-test/src/test/java/tech/ydb/slo/JdbcSloTest.java b/slo1/simple-jdbc-test/src/test/java/tech/ydb/slo/JdbcSloTest.java similarity index 100% rename from slo/simple-jdbc-test/src/test/java/tech/ydb/slo/JdbcSloTest.java rename to slo1/simple-jdbc-test/src/test/java/tech/ydb/slo/JdbcSloTest.java diff --git a/slo/simple-jdbc-test/src/test/java/tech/ydb/slo/MetricsReporter.java b/slo1/simple-jdbc-test/src/test/java/tech/ydb/slo/MetricsReporter.java similarity index 100% rename from slo/simple-jdbc-test/src/test/java/tech/ydb/slo/MetricsReporter.java rename to slo1/simple-jdbc-test/src/test/java/tech/ydb/slo/MetricsReporter.java diff --git a/slo/simple-jdbc-test/src/test/java/tech/ydb/slo/SimpleJdbcConfig.java b/slo1/simple-jdbc-test/src/test/java/tech/ydb/slo/SimpleJdbcConfig.java similarity index 100% rename from slo/simple-jdbc-test/src/test/java/tech/ydb/slo/SimpleJdbcConfig.java rename to slo1/simple-jdbc-test/src/test/java/tech/ydb/slo/SimpleJdbcConfig.java diff --git a/slo/simple-jdbc-test/src/test/resources/application.yaml b/slo1/simple-jdbc-test/src/test/resources/application.yaml similarity index 100% rename from slo/simple-jdbc-test/src/test/resources/application.yaml rename to slo1/simple-jdbc-test/src/test/resources/application.yaml From 623de2c51114842d3912b1980c1a907ec5170a6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B2=D0=B0=D0=BD=20=D0=92=D0=B0=D1=81=D0=B8=D0=BB?= =?UTF-8?q?=D1=8C=D0=B5=D0=B2?= Date: Tue, 13 Jan 2026 10:21:48 +0800 Subject: [PATCH 03/13] Update pom.xml --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 50ac34f..a725e61 100644 --- a/pom.xml +++ b/pom.xml @@ -30,7 +30,7 @@ url-shortener-demo jdbc project-course - slo1 + slo From c5994a0aae95467b07b88e9b02ebeb6d41789cfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B2=D0=B0=D0=BD=20=D0=92=D0=B0=D1=81=D0=B8=D0=BB?= =?UTF-8?q?=D1=8C=D0=B5=D0=B2?= Date: Tue, 13 Jan 2026 10:23:18 +0800 Subject: [PATCH 04/13] Rebased from ydb-java-sdk --- {slo1 => slo}/README.md | 0 {slo1 => slo}/pom.xml | 0 {slo1 => slo}/simple-jdbc-test/README.md | 0 {slo1 => slo}/simple-jdbc-test/pom.xml | 0 .../src/main/java/tech/ydb/slo/JdbcSloTableContext.java | 0 .../simple-jdbc-test/src/main/java/tech/ydb/slo/SloTableRow.java | 0 .../simple-jdbc-test/src/test/java/tech/ydb/slo/JdbcSloTest.java | 0 .../src/test/java/tech/ydb/slo/MetricsReporter.java | 0 .../src/test/java/tech/ydb/slo/SimpleJdbcConfig.java | 0 .../simple-jdbc-test/src/test/resources/application.yaml | 0 10 files changed, 0 insertions(+), 0 deletions(-) rename {slo1 => slo}/README.md (100%) rename {slo1 => slo}/pom.xml (100%) rename {slo1 => slo}/simple-jdbc-test/README.md (100%) rename {slo1 => slo}/simple-jdbc-test/pom.xml (100%) rename {slo1 => slo}/simple-jdbc-test/src/main/java/tech/ydb/slo/JdbcSloTableContext.java (100%) rename {slo1 => slo}/simple-jdbc-test/src/main/java/tech/ydb/slo/SloTableRow.java (100%) rename {slo1 => slo}/simple-jdbc-test/src/test/java/tech/ydb/slo/JdbcSloTest.java (100%) rename {slo1 => slo}/simple-jdbc-test/src/test/java/tech/ydb/slo/MetricsReporter.java (100%) rename {slo1 => slo}/simple-jdbc-test/src/test/java/tech/ydb/slo/SimpleJdbcConfig.java (100%) rename {slo1 => slo}/simple-jdbc-test/src/test/resources/application.yaml (100%) diff --git a/slo1/README.md b/slo/README.md similarity index 100% rename from slo1/README.md rename to slo/README.md diff --git a/slo1/pom.xml b/slo/pom.xml similarity index 100% rename from slo1/pom.xml rename to slo/pom.xml diff --git a/slo1/simple-jdbc-test/README.md b/slo/simple-jdbc-test/README.md similarity index 100% rename from slo1/simple-jdbc-test/README.md rename to slo/simple-jdbc-test/README.md diff --git a/slo1/simple-jdbc-test/pom.xml b/slo/simple-jdbc-test/pom.xml similarity index 100% rename from slo1/simple-jdbc-test/pom.xml rename to slo/simple-jdbc-test/pom.xml diff --git a/slo1/simple-jdbc-test/src/main/java/tech/ydb/slo/JdbcSloTableContext.java b/slo/simple-jdbc-test/src/main/java/tech/ydb/slo/JdbcSloTableContext.java similarity index 100% rename from slo1/simple-jdbc-test/src/main/java/tech/ydb/slo/JdbcSloTableContext.java rename to slo/simple-jdbc-test/src/main/java/tech/ydb/slo/JdbcSloTableContext.java diff --git a/slo1/simple-jdbc-test/src/main/java/tech/ydb/slo/SloTableRow.java b/slo/simple-jdbc-test/src/main/java/tech/ydb/slo/SloTableRow.java similarity index 100% rename from slo1/simple-jdbc-test/src/main/java/tech/ydb/slo/SloTableRow.java rename to slo/simple-jdbc-test/src/main/java/tech/ydb/slo/SloTableRow.java diff --git a/slo1/simple-jdbc-test/src/test/java/tech/ydb/slo/JdbcSloTest.java b/slo/simple-jdbc-test/src/test/java/tech/ydb/slo/JdbcSloTest.java similarity index 100% rename from slo1/simple-jdbc-test/src/test/java/tech/ydb/slo/JdbcSloTest.java rename to slo/simple-jdbc-test/src/test/java/tech/ydb/slo/JdbcSloTest.java diff --git a/slo1/simple-jdbc-test/src/test/java/tech/ydb/slo/MetricsReporter.java b/slo/simple-jdbc-test/src/test/java/tech/ydb/slo/MetricsReporter.java similarity index 100% rename from slo1/simple-jdbc-test/src/test/java/tech/ydb/slo/MetricsReporter.java rename to slo/simple-jdbc-test/src/test/java/tech/ydb/slo/MetricsReporter.java diff --git a/slo1/simple-jdbc-test/src/test/java/tech/ydb/slo/SimpleJdbcConfig.java b/slo/simple-jdbc-test/src/test/java/tech/ydb/slo/SimpleJdbcConfig.java similarity index 100% rename from slo1/simple-jdbc-test/src/test/java/tech/ydb/slo/SimpleJdbcConfig.java rename to slo/simple-jdbc-test/src/test/java/tech/ydb/slo/SimpleJdbcConfig.java diff --git a/slo1/simple-jdbc-test/src/test/resources/application.yaml b/slo/simple-jdbc-test/src/test/resources/application.yaml similarity index 100% rename from slo1/simple-jdbc-test/src/test/resources/application.yaml rename to slo/simple-jdbc-test/src/test/resources/application.yaml From 7417dca6835d5bb22ec74435b101e5034393f13b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B2=D0=B0=D0=BD=20=D0=92=D0=B0=D1=81=D0=B8=D0=BB?= =?UTF-8?q?=D1=8C=D0=B5=D0=B2?= Date: Tue, 13 Jan 2026 11:22:08 +0800 Subject: [PATCH 05/13] Rebase finished --- .github/workflows/slo-test.yml | 110 ++++++++++++++++++ pom.xml | 2 +- {slo => slo-workload}/README.md | 0 slo-workload/pom.xml | 67 +++++++++++ .../simple-jdbc}/README.md | 0 slo-workload/simple-jdbc/pom.xml | 75 ++++++++++++ .../tech/ydb/slo/JdbcSloTableContext.java | 0 .../main/java/tech/ydb/slo/SloTableRow.java | 0 .../test/java/tech/ydb/slo/JdbcSloTest.java | 0 .../java/tech/ydb/slo/MetricsReporter.java | 0 .../java/tech/ydb/slo/SimpleJdbcConfig.java | 0 .../src/test/resources/application.yaml | 0 slo/pom.xml | 30 ----- slo/simple-jdbc-test/pom.xml | 100 ---------------- 14 files changed, 253 insertions(+), 131 deletions(-) create mode 100644 .github/workflows/slo-test.yml rename {slo => slo-workload}/README.md (100%) create mode 100644 slo-workload/pom.xml rename {slo/simple-jdbc-test => slo-workload/simple-jdbc}/README.md (100%) create mode 100644 slo-workload/simple-jdbc/pom.xml rename {slo/simple-jdbc-test => slo-workload/simple-jdbc}/src/main/java/tech/ydb/slo/JdbcSloTableContext.java (100%) rename {slo/simple-jdbc-test => slo-workload/simple-jdbc}/src/main/java/tech/ydb/slo/SloTableRow.java (100%) rename {slo/simple-jdbc-test => slo-workload/simple-jdbc}/src/test/java/tech/ydb/slo/JdbcSloTest.java (100%) rename {slo/simple-jdbc-test => slo-workload/simple-jdbc}/src/test/java/tech/ydb/slo/MetricsReporter.java (100%) rename {slo/simple-jdbc-test => slo-workload/simple-jdbc}/src/test/java/tech/ydb/slo/SimpleJdbcConfig.java (100%) rename {slo/simple-jdbc-test => slo-workload/simple-jdbc}/src/test/resources/application.yaml (100%) delete mode 100644 slo/pom.xml delete mode 100644 slo/simple-jdbc-test/pom.xml diff --git a/.github/workflows/slo-test.yml b/.github/workflows/slo-test.yml new file mode 100644 index 0000000..175052d --- /dev/null +++ b/.github/workflows/slo-test.yml @@ -0,0 +1,110 @@ +name: SLO JDBC + +on: + push: + branches: [master, main] + 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 a725e61..8b23a80 100644 --- a/pom.xml +++ b/pom.xml @@ -30,7 +30,7 @@ url-shortener-demo jdbc project-course - slo + slo-workload diff --git a/slo/README.md b/slo-workload/README.md similarity index 100% rename from slo/README.md rename to slo-workload/README.md diff --git a/slo-workload/pom.xml b/slo-workload/pom.xml new file mode 100644 index 0000000..dfabf26 --- /dev/null +++ b/slo-workload/pom.xml @@ -0,0 +1,67 @@ + + 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} + + + + \ No newline at end of file diff --git a/slo/simple-jdbc-test/README.md b/slo-workload/simple-jdbc/README.md similarity index 100% rename from slo/simple-jdbc-test/README.md rename to slo-workload/simple-jdbc/README.md diff --git a/slo-workload/simple-jdbc/pom.xml b/slo-workload/simple-jdbc/pom.xml new file mode 100644 index 0000000..431f617 --- /dev/null +++ b/slo-workload/simple-jdbc/pom.xml @@ -0,0 +1,75 @@ + + 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-surefire-plugin + + ${skip.jdbc.tests} + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + + + repackage + + + + + + + \ No newline at end of file diff --git a/slo/simple-jdbc-test/src/main/java/tech/ydb/slo/JdbcSloTableContext.java b/slo-workload/simple-jdbc/src/main/java/tech/ydb/slo/JdbcSloTableContext.java similarity index 100% rename from slo/simple-jdbc-test/src/main/java/tech/ydb/slo/JdbcSloTableContext.java rename to slo-workload/simple-jdbc/src/main/java/tech/ydb/slo/JdbcSloTableContext.java diff --git a/slo/simple-jdbc-test/src/main/java/tech/ydb/slo/SloTableRow.java b/slo-workload/simple-jdbc/src/main/java/tech/ydb/slo/SloTableRow.java similarity index 100% rename from slo/simple-jdbc-test/src/main/java/tech/ydb/slo/SloTableRow.java rename to slo-workload/simple-jdbc/src/main/java/tech/ydb/slo/SloTableRow.java diff --git a/slo/simple-jdbc-test/src/test/java/tech/ydb/slo/JdbcSloTest.java b/slo-workload/simple-jdbc/src/test/java/tech/ydb/slo/JdbcSloTest.java similarity index 100% rename from slo/simple-jdbc-test/src/test/java/tech/ydb/slo/JdbcSloTest.java rename to slo-workload/simple-jdbc/src/test/java/tech/ydb/slo/JdbcSloTest.java diff --git a/slo/simple-jdbc-test/src/test/java/tech/ydb/slo/MetricsReporter.java b/slo-workload/simple-jdbc/src/test/java/tech/ydb/slo/MetricsReporter.java similarity index 100% rename from slo/simple-jdbc-test/src/test/java/tech/ydb/slo/MetricsReporter.java rename to slo-workload/simple-jdbc/src/test/java/tech/ydb/slo/MetricsReporter.java diff --git a/slo/simple-jdbc-test/src/test/java/tech/ydb/slo/SimpleJdbcConfig.java b/slo-workload/simple-jdbc/src/test/java/tech/ydb/slo/SimpleJdbcConfig.java similarity index 100% rename from slo/simple-jdbc-test/src/test/java/tech/ydb/slo/SimpleJdbcConfig.java rename to slo-workload/simple-jdbc/src/test/java/tech/ydb/slo/SimpleJdbcConfig.java diff --git a/slo/simple-jdbc-test/src/test/resources/application.yaml b/slo-workload/simple-jdbc/src/test/resources/application.yaml similarity index 100% rename from slo/simple-jdbc-test/src/test/resources/application.yaml rename to slo-workload/simple-jdbc/src/test/resources/application.yaml diff --git a/slo/pom.xml b/slo/pom.xml deleted file mode 100644 index 5ae1e8f..0000000 --- a/slo/pom.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - 4.0.0 - - - tech.ydb - ydb-sdk-parent - 2.3.30-SNAPSHOT - - - - 21 - - - pom - - tech.ydb.slo - ydb-sdk-slo - 2.3.30-SNAPSHOT - YDB SLO Tests - - - simple-jdbc-test - - - diff --git a/slo/simple-jdbc-test/pom.xml b/slo/simple-jdbc-test/pom.xml deleted file mode 100644 index 901dbef..0000000 --- a/slo/simple-jdbc-test/pom.xml +++ /dev/null @@ -1,100 +0,0 @@ - - - 4.0.0 - - tech.ydb.slo - simple-jdbc-test - 1.0-SNAPSHOT - jar - Simple JDBC Test - - - 21 - 21 - 21 - UTF-8 - - 3.5.9 - 5.12.2 - true - - - - - - org.springframework.boot - spring-boot-dependencies - ${spring-boot.version} - pom - import - - - - - - - - org.springframework.boot - spring-boot-starter-jdbc - - - - - org.springframework.boot - spring-boot-starter-test - test - - - - - tech.ydb.jdbc - ydb-jdbc-driver - 2.3.20 - - - - - io.prometheus - simpleclient - 0.16.0 - - - - - io.prometheus - simpleclient_pushgateway - 0.16.0 - - - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.13.0 - - 21 - 21 - 21 - - - - - - org.apache.maven.plugins - maven-surefire-plugin - 3.1.2 - - ${skip.jdbc.tests} - - - - - - \ No newline at end of file From 0db66db5f17b1fd540979cc490ded4e5c9644562 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B2=D0=B0=D0=BD=20=D0=92=D0=B0=D1=81=D0=B8=D0=BB?= =?UTF-8?q?=D1=8C=D0=B5=D0=B2?= Date: Tue, 13 Jan 2026 11:28:41 +0800 Subject: [PATCH 06/13] Tuning in progress --- .github/workflows/slo-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/slo-test.yml b/.github/workflows/slo-test.yml index 175052d..5abeb9c 100644 --- a/.github/workflows/slo-test.yml +++ b/.github/workflows/slo-test.yml @@ -2,7 +2,7 @@ name: SLO JDBC on: push: - branches: [master, main] + branches: [master, main, add-slo-workload]] paths: - 'slo-workload/**' - '.github/workflows/slo-test.yml' From 37c3da372a46744d7ca802e73e931d5557fa66a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B2=D0=B0=D0=BD=20=D0=92=D0=B0=D1=81=D0=B8=D0=BB?= =?UTF-8?q?=D1=8C=D0=B5=D0=B2?= Date: Tue, 13 Jan 2026 11:29:38 +0800 Subject: [PATCH 07/13] Tuning in progress --- .github/workflows/slo-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/slo-test.yml b/.github/workflows/slo-test.yml index 5abeb9c..f0c5bde 100644 --- a/.github/workflows/slo-test.yml +++ b/.github/workflows/slo-test.yml @@ -2,7 +2,7 @@ name: SLO JDBC on: push: - branches: [master, main, add-slo-workload]] + branches: [master, main, add-slo-workload] paths: - 'slo-workload/**' - '.github/workflows/slo-test.yml' From b149a48e7299cec4a127761e3a76ad68a16cfb68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B2=D0=B0=D0=BD=20=D0=92=D0=B0=D1=81=D0=B8=D0=BB?= =?UTF-8?q?=D1=8C=D0=B5=D0=B2?= Date: Tue, 13 Jan 2026 11:37:44 +0800 Subject: [PATCH 08/13] Update pom.xml --- slo-workload/pom.xml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/slo-workload/pom.xml b/slo-workload/pom.xml index dfabf26..c7e9f8c 100644 --- a/slo-workload/pom.xml +++ b/slo-workload/pom.xml @@ -64,4 +64,20 @@ + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 21 + 21 + 21 + + + + + \ No newline at end of file From 7262c3dc5c035f24f453d11c9a737ddf5fdcb973 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B2=D0=B0=D0=BD=20=D0=92=D0=B0=D1=81=D0=B8=D0=BB?= =?UTF-8?q?=D1=8C=D0=B5=D0=B2?= Date: Tue, 13 Jan 2026 11:44:34 +0800 Subject: [PATCH 09/13] Tuning in progress --- slo-workload/pom.xml | 130 ++++++++++++++++++++++--------------------- 1 file changed, 67 insertions(+), 63 deletions(-) diff --git a/slo-workload/pom.xml b/slo-workload/pom.xml index c7e9f8c..a529ae0 100644 --- a/slo-workload/pom.xml +++ b/slo-workload/pom.xml @@ -3,81 +3,85 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 + tech.ydb.examples - ydb-sdk-examples + slo-workload 1.1.0-SNAPSHOT ../pom.xml - slo-workload - pom + simple-jdbc + jar + Simple JDBC SLO Test - YDB SLO Workload Tests - SLO testing applications for YDB performance validation + + + + org.springframework.boot + spring-boot-starter-jdbc + - - 21 - 21 - 21 - UTF-8 + + + org.springframework.boot + spring-boot-starter-test + test + - 3.5.9 - 2.3.20 - 0.16.0 - 5.12.2 - true - + + + tech.ydb.jdbc + ydb-jdbc-driver + - - simple-jdbc - + + + io.prometheus + simpleclient + + + io.prometheus + simpleclient_pushgateway + + - - - - - org.springframework.boot - spring-boot-dependencies - ${spring-boot.version} - pom - import - + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 21 + 21 + 21 + + - - - tech.ydb.jdbc - ydb-jdbc-driver - ${ydb.jdbc.version} - + + + org.apache.maven.plugins + maven-surefire-plugin + + ${skip.jdbc.tests} + + - - - io.prometheus - simpleclient - ${prometheus.version} - - - io.prometheus - simpleclient_pushgateway - ${prometheus.version} - - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.13.0 - - 21 - 21 - 21 - - - - + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + + + repackage + + + + + \ No newline at end of file From 8e7f193ed94f0f50024e7968b3d403aece504db1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B2=D0=B0=D0=BD=20=D0=92=D0=B0=D1=81=D0=B8=D0=BB?= =?UTF-8?q?=D1=8C=D0=B5=D0=B2?= Date: Tue, 13 Jan 2026 11:47:10 +0800 Subject: [PATCH 10/13] Tuning in progress --- slo-workload/pom.xml | 130 +++++++++++++++---------------- slo-workload/simple-jdbc/pom.xml | 20 ++++- 2 files changed, 79 insertions(+), 71 deletions(-) diff --git a/slo-workload/pom.xml b/slo-workload/pom.xml index a529ae0..c7e9f8c 100644 --- a/slo-workload/pom.xml +++ b/slo-workload/pom.xml @@ -3,85 +3,81 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - tech.ydb.examples - slo-workload + ydb-sdk-examples 1.1.0-SNAPSHOT ../pom.xml - simple-jdbc - jar - Simple JDBC SLO Test - - - - - org.springframework.boot - spring-boot-starter-jdbc - + slo-workload + pom - - - org.springframework.boot - spring-boot-starter-test - test - + YDB SLO Workload Tests + SLO testing applications for YDB performance validation - - - tech.ydb.jdbc - ydb-jdbc-driver - + + 21 + 21 + 21 + UTF-8 - - - io.prometheus - simpleclient - - - io.prometheus - simpleclient_pushgateway - - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.13.0 - - 21 - 21 - 21 - - + 3.5.9 + 2.3.20 + 0.16.0 + 5.12.2 + true + - - - org.apache.maven.plugins - maven-surefire-plugin - - ${skip.jdbc.tests} - - + + simple-jdbc + - - + + + + org.springframework.boot - spring-boot-maven-plugin + spring-boot-dependencies ${spring-boot.version} - - - - repackage - - - - - + 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/pom.xml b/slo-workload/simple-jdbc/pom.xml index 431f617..a529ae0 100644 --- a/slo-workload/simple-jdbc/pom.xml +++ b/slo-workload/simple-jdbc/pom.xml @@ -16,26 +16,26 @@ 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 @@ -48,6 +48,18 @@ + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 21 + 21 + 21 + + + org.apache.maven.plugins From 129d0e7e68f825ece7b4c6246ed694f0266901c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B2=D0=B0=D0=BD=20=D0=92=D0=B0=D1=81=D0=B8=D0=BB?= =?UTF-8?q?=D1=8C=D0=B5=D0=B2?= Date: Tue, 13 Jan 2026 11:50:45 +0800 Subject: [PATCH 11/13] Tuning in progress --- slo-workload/simple-jdbc/pom.xml | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/slo-workload/simple-jdbc/pom.xml b/slo-workload/simple-jdbc/pom.xml index a529ae0..34f698a 100644 --- a/slo-workload/simple-jdbc/pom.xml +++ b/slo-workload/simple-jdbc/pom.xml @@ -68,20 +68,7 @@ ${skip.jdbc.tests} - - - - org.springframework.boot - spring-boot-maven-plugin - ${spring-boot.version} - - - - repackage - - - - + \ No newline at end of file From b77c4d635f94d39a8eaedf35cb84f77aae2ec168 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B2=D0=B0=D0=BD=20=D0=92=D0=B0=D1=81=D0=B8=D0=BB?= =?UTF-8?q?=D1=8C=D0=B5=D0=B2?= Date: Tue, 13 Jan 2026 14:17:05 +0800 Subject: [PATCH 12/13] Clean up and document --- slo-workload/simple-jdbc/pom.xml | 2 +- .../java/tech/ydb/slo/MetricsReporter.java | 52 +++++++------------ 2 files changed, 21 insertions(+), 33 deletions(-) diff --git a/slo-workload/simple-jdbc/pom.xml b/slo-workload/simple-jdbc/pom.xml index 34f698a..2d05435 100644 --- a/slo-workload/simple-jdbc/pom.xml +++ b/slo-workload/simple-jdbc/pom.xml @@ -68,7 +68,7 @@ ${skip.jdbc.tests} - + \ 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 index b3a0d5f..26ced2c 100644 --- 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 @@ -38,6 +38,7 @@ public MetricsReporter(String promPgwUrl, String jobName) { 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); } @@ -76,22 +77,17 @@ public MetricsReporter(String promPgwUrl, String jobName) { * Record successful operation */ public void recordSuccess(String operation, double latencySeconds) { - // Increment total operations operationsTotal.labels(operation, "java", javaVersion, jobName, "0.0.0").inc(); - - // Increment successful operations operationsSuccessTotal.labels(operation, "java", javaVersion, jobName, "0.0.0").inc(); - - // Record latency operationLatencySeconds.labels(operation, "success", "java", javaVersion, jobName, "0.0.0") .observe(latencySeconds); totalSuccess++; - // Log every 100 operations - if (totalSuccess % 100 == 0) { - System.out.printf("✅ [%s] Success #%d, latency: %.3f ms%n", - operation, totalSuccess, latencySeconds * 1000); + // Log only at milestones + if (totalSuccess % 1000 == 0) { + log.info("{} operations completed successfully (avg latency: {:.2f} ms)", + totalSuccess, latencySeconds * 1000); } } @@ -99,17 +95,10 @@ public void recordSuccess(String operation, double latencySeconds) { * Record failed operation */ public void recordError(String operation, String errorType) { - // Increment total operations (errors are also operations) operationsTotal.labels(operation, "java", javaVersion, jobName, "0.0.0").inc(); - - // Note: We could add error latency here if we tracked it - // For now, we just count the error - totalErrors++; - // Log errors - System.out.printf("❌ [%s] Error #%d, type: %s%n", - operation, totalErrors, errorType); + log.warn("Operation failed: {} (type: {}, total errors: {})", operation, errorType, totalErrors); } /** @@ -132,15 +121,9 @@ public void push() { "instance", "jdbc" ) ); - - System.out.println("📤 Metrics pushed to Prometheus"); - System.out.println(" Success total: " + totalSuccess); - System.out.println(" Errors total: " + totalErrors); - - log.debug("Metrics pushed to Prometheus"); + log.info("Metrics pushed to Prometheus (success: {}, errors: {})", totalSuccess, totalErrors); } catch (IOException e) { - System.err.println("❌ Failed to push metrics: " + e.getMessage()); - log.error("Failed to push metrics", e); + log.error("Failed to push metrics to Prometheus", e); } } @@ -154,14 +137,19 @@ public void saveToFile(String filename, double latencySeconds) { writer.println("LATENCY_MS=" + String.format("%.2f", latencySeconds * 1000)); writer.println("PENDING_OPERATIONS=0"); - System.out.println("💾 Metrics saved to file:"); - System.out.println(" Success: " + totalSuccess); - System.out.println(" Errors: " + totalErrors); - System.out.println(" Latency: " + String.format("%.2f ms", latencySeconds * 1000)); - - log.info("Metrics saved to {}", filename); + 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", 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 From 779f96f0454baf63ada0f4aab9c4c22e2a61ea43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B2=D0=B0=D0=BD=20=D0=92=D0=B0=D1=81=D0=B8=D0=BB?= =?UTF-8?q?=D1=8C=D0=B5=D0=B2?= Date: Tue, 13 Jan 2026 14:39:09 +0800 Subject: [PATCH 13/13] Clean up and document --- slo-workload/README.md | 517 ++++++++++++++++++++++++++++- slo-workload/simple-jdbc/README.md | 194 +++++++---- 2 files changed, 626 insertions(+), 85 deletions(-) diff --git a/slo-workload/README.md b/slo-workload/README.md index e1f8fd2..d48ea9e 100644 --- a/slo-workload/README.md +++ b/slo-workload/README.md @@ -1,19 +1,508 @@ +# YDB SLO Workload Tests -# Пробная реализация SLO +Набор SLO (Service Level Objectives) тестов для проверки производительности и надежности YDB клиентов на Java. -Проект представляет собой пробный подход к решению задачи проверки SLO для SDK. -В состав модулей SDK добавлен модуль **slo**. -Модуль должен быть подключен в числе дочерних модулей ydb-java-sdk - - slo +## Назначение -Запуск модулей slo осуществляется в задаче [slo.yml](../.github/workflows/slo.yml) +SLO тесты измеряют соответствие реальной производительности заявленным целевым метрикам: +- **Latency**: время отклика операций (P50, P95, P99) +- **Availability**: процент успешных операций +- **Throughput**: количество операций в секунду +- **Stability**: стабильность под длительной нагрузкой -В настоящее время реализован только один тест .. [simple-jdbc-test](simple-jdbc-test) -по аналогии с AdoNet проверяющий базовое JDBC подключение. +## Стратегия тестирования -В развитие просятся модули slo для подключений c использованием: - - JDBCTemplate - - Spring Data JDBC - - Spring Data JPA - - -Думаю, ими можно заняться после согласования simple-jdbc-test \ No newline at end of file +### Фазы выполнения + +#### 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/simple-jdbc/README.md b/slo-workload/simple-jdbc/README.md index 4e8cd8b..ef31f60 100644 --- a/slo-workload/simple-jdbc/README.md +++ b/slo-workload/simple-jdbc/README.md @@ -1,85 +1,137 @@ -# simple-jdbc-test +# Simple JDBC SLO Test -Модуль, содержащий полноценный SLO тест с использованием JDBC подключения к YDB. +Базовый 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`. Реализует retry logic с exponential backoff. - -**Методы:** -- `createTable(timeout)` - создание таблицы с составным ключом (Guid, Id) -- `save(row, timeout)` - UPSERT запись с автоматическим retry (до 5 попыток) -- `select(guid, id, timeout)` - чтение записи по ключу с retry -- `selectCount()` - подсчёт общего количества записей -- `tableExists()` - проверка существования таблицы -- `isRetryableError(exception)` - определение временных ошибок (timeout, network, overload) - -**Retry стратегия:** -- Максимум попыток: 5 -- Backoff: 100ms → 200ms → 400ms → 800ms → 1600ms -- Повторяет: timeout, connection, network, overload, session expired -- Не повторяет: schema errors, constraint violations, syntax errors +Сервисный класс для работы с таблицей `slo_table`. -### SloTableRow -Data Transfer Object для строки таблицы. Используется для тестирования и будущих workload'ов. +**Основные методы:** +```java +createTable(timeout) // Создание таблицы +save(row, timeout) // UPSERT с retry +select(guid, id, timeout) // SELECT с retry +``` -**Поля:** -- `guid: UUID` - уникальный идентификатор -- `id: int` - порядковый номер -- `payloadStr: String` - строковая нагрузка (~1KB) -- `payloadDouble: double` - числовая нагрузка -- `payloadTimestamp: Timestamp` - время создания +**Особенности:** +- Retry с exponential backoff (5 попыток) +- Автоматическое восстановление после временных ошибок +- Подробности: см. [родительский README](../README.md#retry-механизм) -**Методы:** -- `generate(id)` - генерация случайной строки с заданным id -- `generatePayloadString(size)` - создание payload заданного размера +### SloTableRow +DTO для строки таблицы (~1KB payload). +```java +SloTableRow row = SloTableRow.generate(id); +// Поля: guid, id, payloadStr, payloadDouble, payloadTimestamp +``` ### JdbcSloTest -Основной SLO тест. Выполняет полный цикл нагрузочного тестирования. - -**Фазы выполнения:** -1. **Table Initialization** - создание таблицы -2. **Data Preparation** - генерация и запись начальных данных -3. **SLO Test Execution** - параллельная нагрузка (read + write) в течение заданного времени -4. **Results Validation** - проверка соответствия SLO порогам -5. **Metrics Export** - отправка метрик в Prometheus и сохранение в файл - -**SLO пороги:** -- P50 Latency: < 10ms -- P95 Latency: < 50ms -- P99 Latency: < 100ms -- Success Rate: > 99.9% - -**Параметры окружения:** -- `TEST_DURATION` - длительность теста в секундах (default: 60) -- `READ_RPS` - read операций в секунду (default: 100) -- `WRITE_RPS` - write операций в секунду (default: 10) -- `READ_TIMEOUT` - timeout для read в ms (default: 1000) -- `WRITE_TIMEOUT` - timeout для write в ms (default: 1000) -- `PROM_PGW` - URL Prometheus Push Gateway (default: http://localhost:9091) -- `REPORT_PERIOD` - период отправки метрик в ms (default: 10000) -- `YDB_JDBC_URL` - строка подключения к YDB +Основной тест JUnit, выполняющий полный цикл SLO тестирования. + +**Фазы:** +1. Инициализация таблицы +2. Warmup (10 потоков, 10s) +3. Load test (30 потоков, TEST_DURATION) +4. Валидация SLO +5. Экспорт метрик ### MetricsReporter -Класс для сбора и отправки метрик в Prometheus Push Gateway. +Экспорт метрик в Prometheus Push Gateway. **Метрики:** -- `jdbc_test_success_total` - Counter успешных операций (labels: operation) -- `jdbc_test_errors_total` - Counter ошибок (labels: operation, error_type) -- `jdbc_test_latency_seconds` - Histogram latency (labels: operation) -- `jdbc_test_active_connections` - Gauge активных подключений - -**Методы:** -- `recordSuccess(operation, latency)` - запись успешной операции -- `recordError(operation, errorType)` - запись ошибки -- `push()` - отправка метрик в Prometheus (полная замена) -- `pushAdd()` - инкрементальное обновление метрик -- `saveToFile(filename, latency)` - сохранение в файл для GitHub Summary - -### SimpleJdbcConfig -Spring конфигурация для DataSource и JdbcTemplate. - -**Beans:** -- `dataSource()` - DriverManagerDataSource для YDB JDBC Driver -- `jdbcTemplate(dataSource)` - Spring JdbcTemplate (для будущего использования) \ No newline at end of file +- `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