From c81a3b7793dde28a7dd6dc408ed005f2292b73cf Mon Sep 17 00:00:00 2001 From: Alexandr Gorshenin Date: Mon, 21 Jul 2025 13:30:04 +0100 Subject: [PATCH 01/19] 2DC tests --- .../src/main/java/tech/ydb/apps/Application.java | 2 +- .../main/java/tech/ydb/apps/annotation/YdbRetryable.java | 3 ++- .../src/main/resources/application.properties | 9 +++++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java index c594153..0847d75 100644 --- a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java +++ b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java @@ -42,7 +42,7 @@ public class Application implements CommandLineRunner { private static final int RECORDS_COUNT = 1_000_000; private static final int LOAD_BATCH_SIZE = 1000; - private static final int WORKLOAD_DURATION_SECS = 60; // 60 seconds + private static final int WORKLOAD_DURATION_SECS = 120; // 600 seconds public static void main(String[] args) { SpringApplication.run(Application.class, args).close(); diff --git a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/annotation/YdbRetryable.java b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/annotation/YdbRetryable.java index 4225352..83d7e2b 100644 --- a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/annotation/YdbRetryable.java +++ b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/annotation/YdbRetryable.java @@ -5,6 +5,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.sql.SQLRecoverableException; +import java.sql.SQLTransientException; import org.springframework.core.annotation.AliasFor; import org.springframework.retry.annotation.Backoff; @@ -17,7 +18,7 @@ @Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Retryable( - retryFor = SQLRecoverableException.class, + retryFor = { SQLRecoverableException.class, SQLTransientException.class }, maxAttempts = 5, backoff = @Backoff(delay = 100, multiplier = 2.0, maxDelay = 5000, random = true) ) diff --git a/jdbc/ydb-token-app/src/main/resources/application.properties b/jdbc/ydb-token-app/src/main/resources/application.properties index 0a88d58..8a53a33 100644 --- a/jdbc/ydb-token-app/src/main/resources/application.properties +++ b/jdbc/ydb-token-app/src/main/resources/application.properties @@ -11,9 +11,14 @@ spring.jpa.properties.hibernate.order_inserts=true spring.jpa.properties.hibernate.dialect=tech.ydb.hibernate.dialect.YdbDialect #spring.jpa.show-sql = true -logging.level.org.hibernate.engine=OFF +#logging.level.org.hibernate.engine=OFF + +logging.level.tech.ydb.core.impl.YdbDiscovery=DEBUG +logging.level.tech.ydb.core.impl.pool.EndpointPool=DEBUG +logging.level.com.zaxxer=OFF +logging.level.org.hibernate=OFF +logging.level.org.springframework=OFF -#logging.level.tech.ydb.apps=TRACE #logging.level.tech.ydb.jdbc.YdbDriver=TRACE #logging.level.org.hibernate.SQL=DEBUG #logging.level.org.hibernate.type=TRACE \ No newline at end of file From e054f023ad8a82490a90f119082c8f77ce1ded0c Mon Sep 17 00:00:00 2001 From: Alexandr Gorshenin Date: Fri, 15 Aug 2025 15:03:45 +0100 Subject: [PATCH 02/19] Upgrade SDK & JDBC versions --- .../src/main/resources/log4j2.xml | 33 +++++++++++++++++++ jdbc/pom.xml | 2 +- jdbc/ydb-token-app/pom.xml | 2 +- pom.xml | 2 +- 4 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 auth/access_token_credentials/src/main/resources/log4j2.xml diff --git a/auth/access_token_credentials/src/main/resources/log4j2.xml b/auth/access_token_credentials/src/main/resources/log4j2.xml new file mode 100644 index 0000000..2e48fc4 --- /dev/null +++ b/auth/access_token_credentials/src/main/resources/log4j2.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jdbc/pom.xml b/jdbc/pom.xml index 3e6ae92..c974a24 100644 --- a/jdbc/pom.xml +++ b/jdbc/pom.xml @@ -15,7 +15,7 @@ pom - 2.3.13 + 2.3.16 1.7.36 diff --git a/jdbc/ydb-token-app/pom.xml b/jdbc/ydb-token-app/pom.xml index 931441f..d6c0515 100644 --- a/jdbc/ydb-token-app/pom.xml +++ b/jdbc/ydb-token-app/pom.xml @@ -31,7 +31,7 @@ org.springframework.retry spring-retry - 2.0.7 + 2.0.12 jakarta.xml.bind diff --git a/pom.xml b/pom.xml index 8978897..621dae8 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,7 @@ 2.22.1 1.82 - 2.3.17 + 2.3.18 From 408331153c88b305d94aa0985a47187fe08fe3e6 Mon Sep 17 00:00:00 2001 From: Alexandr Gorshenin Date: Mon, 18 Aug 2025 11:24:54 +0100 Subject: [PATCH 03/19] Small fixes --- .../src/main/java/tech/ydb/apps/Application.java | 2 +- .../src/main/resources/application.properties | 12 +++++++----- jdbc/ydb-token-app/src/main/resources/sql/drop.sql | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java index 0847d75..f0873bc 100644 --- a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java +++ b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java @@ -203,7 +203,7 @@ private void workload(long finishAt) { measure.inc(); } } else { - int id = rnd.nextInt(RECORDS_COUNT); + int id = rnd.nextInt(RECORDS_COUNT); try (Ticker.Measure measure = ticker.getUpdate().newCall()) { tokenService.updateToken(id); measure.inc(); diff --git a/jdbc/ydb-token-app/src/main/resources/application.properties b/jdbc/ydb-token-app/src/main/resources/application.properties index 8a53a33..5366ce4 100644 --- a/jdbc/ydb-token-app/src/main/resources/application.properties +++ b/jdbc/ydb-token-app/src/main/resources/application.properties @@ -2,7 +2,6 @@ spring.datasource.url=jdbc:ydb:grpc://localhost:2136/local spring.datasource.driver-class-name=tech.ydb.jdbc.YdbDriver spring.datasource.hikari.maximum-pool-size=100 -spring.datasource.hikari.data-source-properties.useQueryService=true spring.datasource.hikari.data-source-properties.enableTxTracer=true spring.jpa.properties.hibernate.jdbc.batch_size=1000 @@ -11,14 +10,17 @@ spring.jpa.properties.hibernate.order_inserts=true spring.jpa.properties.hibernate.dialect=tech.ydb.hibernate.dialect.YdbDialect #spring.jpa.show-sql = true + +logging.pattern.console=%clr(%d{yyyy-MM-dd'T'HH:mm:ss.SSS}){faint} %clr(%5p) %clr(---){faint} %clr([%12.12t]){faint} %clr(%-24.24logger{1}){cyan} %clr(:){faint} %m%n%wEx #logging.level.org.hibernate.engine=OFF -logging.level.tech.ydb.core.impl.YdbDiscovery=DEBUG -logging.level.tech.ydb.core.impl.pool.EndpointPool=DEBUG logging.level.com.zaxxer=OFF logging.level.org.hibernate=OFF +#logging.level.org.hibernate.SQL=DEBUG +#logging.level.org.hibernate.type=TRACE + logging.level.org.springframework=OFF #logging.level.tech.ydb.jdbc.YdbDriver=TRACE -#logging.level.org.hibernate.SQL=DEBUG -#logging.level.org.hibernate.type=TRACE \ No newline at end of file +#logging.level.tech.ydb.core.impl.YdbDiscovery=DEBUG +#logging.level.tech.ydb.core.impl.pool.EndpointPool=DEBUG diff --git a/jdbc/ydb-token-app/src/main/resources/sql/drop.sql b/jdbc/ydb-token-app/src/main/resources/sql/drop.sql index 3f4fd46..59e12eb 100644 --- a/jdbc/ydb-token-app/src/main/resources/sql/drop.sql +++ b/jdbc/ydb-token-app/src/main/resources/sql/drop.sql @@ -1 +1 @@ -DROP TABLE app_token; +DROP TABLE IF EXISTS app_token; From 8374a56777dd3fe79d8c1d35eaf63986f88af618 Mon Sep 17 00:00:00 2001 From: Alexandr Gorshenin Date: Mon, 18 Aug 2025 14:23:41 +0100 Subject: [PATCH 04/19] Added configuration for options --- .../main/java/tech/ydb/apps/Application.java | 95 ++++++++++++------- .../src/main/java/tech/ydb/apps/Config.java | 51 ++++++++++ .../src/main/resources/application.properties | 8 +- 3 files changed, 117 insertions(+), 37 deletions(-) create mode 100644 jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Config.java diff --git a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java index f0873bc..0f95bb2 100644 --- a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java +++ b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java @@ -19,6 +19,7 @@ import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.retry.RetryCallback; import org.springframework.retry.RetryContext; @@ -34,22 +35,22 @@ * @author Aleksandr Gorshenin */ @EnableRetry +@EnableConfigurationProperties(Config.class) @SpringBootApplication public class Application implements CommandLineRunner { private static final Logger logger = LoggerFactory.getLogger(Application.class); - private static final int THREADS_COUNT = 32; - private static final int RECORDS_COUNT = 1_000_000; - private static final int LOAD_BATCH_SIZE = 1000; - - private static final int WORKLOAD_DURATION_SECS = 120; // 600 seconds - public static void main(String[] args) { - SpringApplication.run(Application.class, args).close(); + try { + SpringApplication.run(Application.class, args).close(); + } catch (Exception ex) { + logger.error("App finished with error", ex); + } } private final Ticker ticker = new Ticker(logger); + private final Config config; private final SchemeService schemeService; private final TokenService tokenService; @@ -58,11 +59,12 @@ public static void main(String[] args) { private final AtomicInteger executionsCount = new AtomicInteger(0); private final AtomicInteger retriesCount = new AtomicInteger(0); - public Application(SchemeService schemeService, TokenService tokenService) { + public Application(Config config, SchemeService schemeService, TokenService tokenService) { + this.config = config; this.schemeService = schemeService; this.tokenService = tokenService; - this.executor = Executors.newFixedThreadPool(THREADS_COUNT, this::threadFactory); + this.executor = Executors.newFixedThreadPool(config.getThreadCount(), this::threadFactory); } @PreDestroy @@ -109,9 +111,13 @@ private String printSqlException(Throwable th) { @Override public void run(String... args) { - logger.info("CLI app has started"); + logger.info("CLI app has started with database {}", config.getConnection()); for (String arg : args) { + if (arg.startsWith("--")) { // skip Spring parameters + continue; + } + logger.info("execute {} step", arg); if ("clean".equalsIgnoreCase(arg)) { @@ -141,12 +147,15 @@ private Thread threadFactory(Runnable runnable) { } private void loadData() { + int recordsCount = config.getRecordsCount(); + int batchSize = config.getLoadBatchSize(); + List> futures = new ArrayList<>(); int id = 0; - while (id < RECORDS_COUNT) { + while (id < recordsCount) { final int first = id; - id += LOAD_BATCH_SIZE; - final int last = id < RECORDS_COUNT ? id : RECORDS_COUNT; + id += batchSize; + final int last = id < recordsCount ? id : recordsCount; futures.add(CompletableFuture.runAsync(() -> { try (Ticker.Measure measure = ticker.getLoad().newCall()) { @@ -163,18 +172,19 @@ private void loadData() { private void test() { YdbTracer.current().markToPrint("test"); + int recordsCount = config.getRecordsCount(); final Random rnd = new Random(); List randomIds = IntStream.range(0, 100) - .mapToObj(idx -> rnd.nextInt(RECORDS_COUNT)) + .mapToObj(idx -> rnd.nextInt(recordsCount)) .collect(Collectors.toList()); tokenService.updateBatch(randomIds); } private void runWorkloads() { - long finishAt = System.currentTimeMillis() + WORKLOAD_DURATION_SECS * 1000; + long finishAt = System.currentTimeMillis() + config.getWorkloadDurationSec() * 1000; List> futures = new ArrayList<>(); - for (int i = 0; i < THREADS_COUNT; i++) { + for (int i = 0; i < config.getThreadCount(); i++) { futures.add(CompletableFuture.runAsync(() -> this.workload(finishAt), executor)); } @@ -183,35 +193,48 @@ private void runWorkloads() { private void workload(long finishAt) { final Random rnd = new Random(); + final int recordCount = config.getRecordsCount(); + while (System.currentTimeMillis() < finishAt) { int mode = rnd.nextInt(10); try { - if (mode < 2) { - try (Ticker.Measure measure = ticker.getBatchUpdate().newCall()) { - List randomIds = IntStream.range(0, 100) - .mapToObj(idx -> rnd.nextInt(RECORDS_COUNT)) - .collect(Collectors.toList()); - tokenService.updateBatch(randomIds); - measure.inc(); - } - - } else if (mode < 6) { - int id = rnd.nextInt(RECORDS_COUNT); - try (Ticker.Measure measure = ticker.getFetch().newCall()) { - tokenService.fetchToken(id); - measure.inc(); - } + if (mode < 5) { + executeFetch(rnd, recordCount); // 50 percents + } else if (mode < 9) { + executeUpdate(rnd, recordCount); // 40 percents } else { - int id = rnd.nextInt(RECORDS_COUNT); - try (Ticker.Measure measure = ticker.getUpdate().newCall()) { - tokenService.updateToken(id); - measure.inc(); - } + executeBatchUpdate(rnd, recordCount); // 10 percents } } catch (RuntimeException ex) { logger.debug("got exception {}", ex.getMessage()); } } } + + private void executeFetch(Random rnd, int recordCount) { + int id = rnd.nextInt(recordCount); + try (Ticker.Measure measure = ticker.getFetch().newCall()) { + tokenService.fetchToken(id); + measure.inc(); + } + } + + private void executeUpdate(Random rnd, int recordCount) { + int id = rnd.nextInt(recordCount); + try (Ticker.Measure measure = ticker.getUpdate().newCall()) { + tokenService.updateToken(id); + measure.inc(); + } + } + + private void executeBatchUpdate(Random rnd, int recordCount) { + try (Ticker.Measure measure = ticker.getBatchUpdate().newCall()) { + List randomIds = IntStream.range(0, 100) + .mapToObj(idx -> rnd.nextInt(recordCount)) + .collect(Collectors.toList()); + tokenService.updateBatch(randomIds); + measure.inc(); + } + } } diff --git a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Config.java b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Config.java new file mode 100644 index 0000000..72140eb --- /dev/null +++ b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Config.java @@ -0,0 +1,51 @@ +package tech.ydb.apps; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConstructorBinding; +import org.springframework.boot.context.properties.bind.Name; + +/** + * + * @author Aleksandr Gorshenin + */ + +@ConstructorBinding +@ConfigurationProperties(prefix = "app") +public class Config { + private final String connection; + private final int threadsCount; + private final int recordsCount; + private final int loadBatchSize; + private final int workloadDurationSec; + + public Config(String connection, int threadsCount, int recordsCount, + @Name("load.batchSize") int loadBatchSize, + @Name("workload.duration") int workloadDuration) { + this.connection = connection; + this.threadsCount = threadsCount <= 0 ? Runtime.getRuntime().availableProcessors() : threadsCount; + this.recordsCount = recordsCount; + this.loadBatchSize = loadBatchSize; + this.workloadDurationSec = workloadDuration; + } + + public String getConnection() { + return this.connection; + } + + public int getThreadCount() { + return this.threadsCount; + } + + public int getRecordsCount() { + return this.recordsCount; + } + + public int getLoadBatchSize() { + return this.loadBatchSize; + } + + public int getWorkloadDurationSec() { + return workloadDurationSec; + } + +} diff --git a/jdbc/ydb-token-app/src/main/resources/application.properties b/jdbc/ydb-token-app/src/main/resources/application.properties index 5366ce4..a17e56b 100644 --- a/jdbc/ydb-token-app/src/main/resources/application.properties +++ b/jdbc/ydb-token-app/src/main/resources/application.properties @@ -1,4 +1,10 @@ -spring.datasource.url=jdbc:ydb:grpc://localhost:2136/local +app.connection=grpc://localhost:2136/local +app.threadsCount=-1 +app.recordsCount=1000000 +app.load.batchSize=1000 +app.workload.duration=60 + +spring.datasource.url=jdbc:ydb:${app.connection} spring.datasource.driver-class-name=tech.ydb.jdbc.YdbDriver spring.datasource.hikari.maximum-pool-size=100 From f337435c68a57389ffcb1881e2382f5ccad724ef Mon Sep 17 00:00:00 2001 From: Alexandr Gorshenin Date: Mon, 18 Aug 2025 15:00:09 +0100 Subject: [PATCH 05/19] Added support of RPS limit --- .../main/java/tech/ydb/apps/Application.java | 6 ++++-- .../src/main/java/tech/ydb/apps/Config.java | 11 +++++++++-- .../main/java/tech/ydb/apps/RateLimiter.java | 19 +++++++++++++++++++ .../src/main/resources/application.properties | 1 + 4 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 jdbc/ydb-token-app/src/main/java/tech/ydb/apps/RateLimiter.java diff --git a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java index 0f95bb2..2d04761 100644 --- a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java +++ b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java @@ -182,20 +182,22 @@ private void test() { } private void runWorkloads() { + RateLimiter rt = config.getRpsLimiter(); long finishAt = System.currentTimeMillis() + config.getWorkloadDurationSec() * 1000; List> futures = new ArrayList<>(); for (int i = 0; i < config.getThreadCount(); i++) { - futures.add(CompletableFuture.runAsync(() -> this.workload(finishAt), executor)); + futures.add(CompletableFuture.runAsync(() -> this.workload(rt, finishAt), executor)); } futures.forEach(CompletableFuture::join); } - private void workload(long finishAt) { + private void workload(RateLimiter rt, long finishAt) { final Random rnd = new Random(); final int recordCount = config.getRecordsCount(); while (System.currentTimeMillis() < finishAt) { + rt.acquire(); int mode = rnd.nextInt(10); try { diff --git a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Config.java b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Config.java index 72140eb..211bfe3 100644 --- a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Config.java +++ b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Config.java @@ -17,15 +17,18 @@ public class Config { private final int recordsCount; private final int loadBatchSize; private final int workloadDurationSec; + private final int rpsLimit; public Config(String connection, int threadsCount, int recordsCount, - @Name("load.batchSize") int loadBatchSize, - @Name("workload.duration") int workloadDuration) { + @Name("load.batchSize") int loadBatchSize, @Name("workload.duration") int workloadDuration, + int rpsLimit + ) { this.connection = connection; this.threadsCount = threadsCount <= 0 ? Runtime.getRuntime().availableProcessors() : threadsCount; this.recordsCount = recordsCount; this.loadBatchSize = loadBatchSize; this.workloadDurationSec = workloadDuration; + this.rpsLimit = rpsLimit; } public String getConnection() { @@ -48,4 +51,8 @@ public int getWorkloadDurationSec() { return workloadDurationSec; } + public RateLimiter getRpsLimiter() { + return rpsLimit <= 0 ? RateLimiter.noLimit() : RateLimiter.withRps(rpsLimit); + } + } diff --git a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/RateLimiter.java b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/RateLimiter.java new file mode 100644 index 0000000..ff04267 --- /dev/null +++ b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/RateLimiter.java @@ -0,0 +1,19 @@ +package tech.ydb.apps; + + +/** + * + * @author Aleksandr Gorshenin + */ +public interface RateLimiter { + void acquire(); + + static RateLimiter noLimit() { + // nothing + return () -> { }; + } + + static RateLimiter withRps(int rps) { + return com.google.common.util.concurrent.RateLimiter.create(rps)::acquire; + } +} diff --git a/jdbc/ydb-token-app/src/main/resources/application.properties b/jdbc/ydb-token-app/src/main/resources/application.properties index a17e56b..fa75cab 100644 --- a/jdbc/ydb-token-app/src/main/resources/application.properties +++ b/jdbc/ydb-token-app/src/main/resources/application.properties @@ -3,6 +3,7 @@ app.threadsCount=-1 app.recordsCount=1000000 app.load.batchSize=1000 app.workload.duration=60 +app.rpsLimit=-1 spring.datasource.url=jdbc:ydb:${app.connection} spring.datasource.driver-class-name=tech.ydb.jdbc.YdbDriver From 710252726b231a6c2515d05193b4969646942f8c Mon Sep 17 00:00:00 2001 From: Alexandr Gorshenin Date: Tue, 19 Aug 2025 18:35:45 +0100 Subject: [PATCH 06/19] Enabled prometheus metrics --- jdbc/ydb-token-app/pom.xml | 36 +++++++++++++++++-- .../main/java/tech/ydb/apps/Application.java | 26 +++++++++++++- .../src/main/resources/application.properties | 9 +++++ 3 files changed, 67 insertions(+), 4 deletions(-) diff --git a/jdbc/ydb-token-app/pom.xml b/jdbc/ydb-token-app/pom.xml index d6c0515..cd6585b 100644 --- a/jdbc/ydb-token-app/pom.xml +++ b/jdbc/ydb-token-app/pom.xml @@ -26,23 +26,34 @@ org.springframework.boot spring-boot-starter-data-jpa - ${spring.boot.version} + + + + org.springframework.boot + spring-boot-starter-actuator + + + io.micrometer + micrometer-registry-prometheus + + + io.prometheus + simpleclient_pushgateway org.springframework.retry spring-retry - 2.0.12 jakarta.xml.bind jakarta.xml.bind-api - 2.3.2 tech.ydb.jdbc ydb-jdbc-driver + tech.ydb.dialects hibernate-ydb-dialect-v5 @@ -59,4 +70,23 @@ + + + + + org.springframework.boot + spring-boot-starter-parent + ${spring.boot.version} + pom + import + + + + org.springframework.retry + spring-retry + 2.0.12 + + + + \ No newline at end of file diff --git a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java index 2d04761..a4e9841 100644 --- a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java +++ b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java @@ -14,6 +14,8 @@ import javax.annotation.PreDestroy; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.CommandLineRunner; @@ -53,24 +55,38 @@ public static void main(String[] args) { private final Config config; private final SchemeService schemeService; private final TokenService tokenService; + private final MeterRegistry meterRegistry; private final ExecutorService executor; + private final AtomicInteger threadCounter = new AtomicInteger(0); private final AtomicInteger executionsCount = new AtomicInteger(0); private final AtomicInteger retriesCount = new AtomicInteger(0); - public Application(Config config, SchemeService schemeService, TokenService tokenService) { + private final Counter executionsCounter; + private final Counter errorsCounter; +// private final Counter successCounter; + + public Application(Config config, SchemeService schemeService, TokenService tokenService, MeterRegistry registry) { this.config = config; this.schemeService = schemeService; this.tokenService = tokenService; + this.meterRegistry = registry; this.executor = Executors.newFixedThreadPool(config.getThreadCount(), this::threadFactory); + + this.executionsCounter = Counter.builder("sdk.operations").register(meterRegistry); + this.errorsCounter = Counter.builder("sdk.operations.errors").register(meterRegistry); +// this.successCounter = Counter.builder("OK").register(meterRegistry); } @PreDestroy public void close() throws Exception { logger.info("CLI app is waiting for finishing"); + errorsCounter.close(); + executionsCounter.close(); + executor.shutdown(); executor.awaitTermination(5, TimeUnit.MINUTES); @@ -79,6 +95,7 @@ public void close() throws Exception { logger.info("Executed {} transactions with {} retries", executionsCount.get(), retriesCount.get()); logger.info("CLI app has finished"); + } @Bean @@ -87,6 +104,7 @@ public RetryListener retryListener() { @Override public boolean open(RetryContext ctx, RetryCallback callback) { executionsCount.incrementAndGet(); + executionsCounter.increment(); return true; } @@ -94,6 +112,12 @@ public boolean open(RetryContext ctx, RetryCallback void onError(RetryContext ctx, RetryCallback callback, Throwable th) { logger.debug("Retry operation with error {} ", printSqlException(th)); retriesCount.incrementAndGet(); + errorsCounter.increment(); + } + + @Override + public void close(RetryContext rc, RetryCallback callback, Throwable th) { + // nothing } }; } diff --git a/jdbc/ydb-token-app/src/main/resources/application.properties b/jdbc/ydb-token-app/src/main/resources/application.properties index fa75cab..a147c4a 100644 --- a/jdbc/ydb-token-app/src/main/resources/application.properties +++ b/jdbc/ydb-token-app/src/main/resources/application.properties @@ -4,7 +4,10 @@ app.recordsCount=1000000 app.load.batchSize=1000 app.workload.duration=60 app.rpsLimit=-1 +app.pushMetrics=false +app.prometheusUrl=http://localhost:9091 +spring.application.name=ydb-token-app spring.datasource.url=jdbc:ydb:${app.connection} spring.datasource.driver-class-name=tech.ydb.jdbc.YdbDriver @@ -17,6 +20,12 @@ spring.jpa.properties.hibernate.order_inserts=true spring.jpa.properties.hibernate.dialect=tech.ydb.hibernate.dialect.YdbDialect #spring.jpa.show-sql = true +management.metrics.enable.all=false +management.metrics.enable.jdbc=true +management.metrics.enable.sdk=true +management.metrics.export.prometheus.pushgateway.enabled=${app.pushMetrics} +management.metrics.export.prometheus.pushgateway.push-rate=15s +management.metrics.export.prometheus.pushgateway.base-url=${app.prometheusUrl} logging.pattern.console=%clr(%d{yyyy-MM-dd'T'HH:mm:ss.SSS}){faint} %clr(%5p) %clr(---){faint} %clr([%12.12t]){faint} %clr(%-24.24logger{1}){cyan} %clr(:){faint} %m%n%wEx #logging.level.org.hibernate.engine=OFF From 41e58407c77fca2098e8930f4dbde98e32be7414 Mon Sep 17 00:00:00 2001 From: Alexandr Gorshenin Date: Tue, 19 Aug 2025 22:48:56 +0100 Subject: [PATCH 07/19] Added SLO metrics --- .../main/java/tech/ydb/apps/AppMetrics.java | 258 ++++++++++++++++++ .../main/java/tech/ydb/apps/Application.java | 77 +----- .../src/main/java/tech/ydb/apps/Ticker.java | 141 ---------- .../src/main/resources/application.properties | 3 +- 4 files changed, 269 insertions(+), 210 deletions(-) create mode 100644 jdbc/ydb-token-app/src/main/java/tech/ydb/apps/AppMetrics.java delete mode 100644 jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Ticker.java diff --git a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/AppMetrics.java b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/AppMetrics.java new file mode 100644 index 0000000..a38e563 --- /dev/null +++ b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/AppMetrics.java @@ -0,0 +1,258 @@ +package tech.ydb.apps; + +import java.sql.SQLException; +import java.time.Duration; +import java.util.Arrays; +import java.util.EnumMap; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.LongAdder; +import java.util.function.Function; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import org.slf4j.Logger; +import org.springframework.retry.RetryCallback; +import org.springframework.retry.RetryContext; +import org.springframework.retry.RetryListener; + +import tech.ydb.core.StatusCode; +import tech.ydb.jdbc.exception.YdbStatusable; + +/** + * + * @author Aleksandr Gorshenin + */ +public class AppMetrics { + private static final ThreadLocal LOCAL = new ThreadLocal<>(); + + private static final Counter.Builder SDK_OPERATIONS = Counter.builder("sdk.operations"); + private static final Counter.Builder SDK_OPERATIONS_SUCCESS = Counter.builder("sdk.operations.success"); + private static final Counter.Builder SDK_OPERATIONS_FAILTURE = Counter.builder("sdk.operations.failture"); + private static final Counter.Builder SDK_RETRY_ATTEMPS = Counter.builder("sdk.retry.attempts"); + + private static final Timer.Builder SDK_OPERATION_LATENCY = Timer.builder("sdk.operation.latency") + .sla( + Duration.ofMillis(1), + Duration.ofMillis(2), + Duration.ofMillis(3), + Duration.ofMillis(4), + Duration.ofMillis(5), + Duration.ofMillis(8), + Duration.ofMillis(10), + Duration.ofMillis(20), + Duration.ofMillis(50), + Duration.ofMillis(100), + Duration.ofMillis(200), + Duration.ofMillis(500), + Duration.ofMillis(1000) + ); +// .publishPercentiles(); + + public class Method { + private final String name; + + private final LongAdder totalCount = new LongAdder(); + private final LongAdder totalTimeMs = new LongAdder(); + + private final LongAdder count = new LongAdder(); + private final LongAdder timeMs = new LongAdder(); + + private final Counter executionsCounter; + private final Counter successCounter; + private final Map errorsCountersMap = new EnumMap<>(StatusCode.class); + private final Map retriesCountersMap = new EnumMap<>(StatusCode.class); + private final Map durationTimerMap = new EnumMap<>(StatusCode.class); + private final Function errorCounter; + private final Function retriesCounter; + private final Function durationTimer; + + private volatile long lastPrinted = 0; + + public Method(MeterRegistry registry, String name, String label) { + this.name = name; + this.executionsCounter = SDK_OPERATIONS.tag("method", label).register(registry); + this.successCounter = SDK_OPERATIONS_SUCCESS.tag("method", label).register(registry); + this.errorCounter = code -> errorsCountersMap.computeIfAbsent(code, key -> + SDK_OPERATIONS_FAILTURE.tag("method", label).tag("status", key.toString()).register(registry) + ); + this.retriesCounter = code -> retriesCountersMap.computeIfAbsent(code, key -> + SDK_RETRY_ATTEMPS.tag("method", label).tag("status", key.toString()).register(registry) + ); + this.durationTimer = code -> durationTimerMap.computeIfAbsent(code, key -> + SDK_OPERATION_LATENCY.tag("method", label).tag("status", key.toString()).register(registry) + ); + } + + public void measure(Runnable run) { + LOCAL.set(this); + + executionsCounter.increment(); + + StatusCode code = StatusCode.SUCCESS; + long startedAt = System.currentTimeMillis(); + try { + run.run(); + successCounter.increment(); + } catch (RuntimeException ex) { + code = extractStatusCode(ex); + errorCounter.apply(code).increment(); + throw ex; + } finally { + LOCAL.remove(); + + long ms = System.currentTimeMillis() - startedAt; + count.add(1); + totalCount.add(1); + timeMs.add(ms); + totalTimeMs.add(ms); + + durationTimer.apply(code).record(ms, TimeUnit.MILLISECONDS); + } + } + + public void close() { + successCounter.close(); + executionsCounter.close(); + errorsCountersMap.forEach((status, counter) -> counter.close()); + } + + private void reset() { + count.reset(); + timeMs.reset(); + lastPrinted = System.currentTimeMillis(); + } + + private void print(Logger logger) { + if (count.longValue() > 0 && lastPrinted != 0) { + long ms = System.currentTimeMillis() - lastPrinted; + double rps = 1000 * count.longValue() / ms; + logger.info("{}\twas executed {} times\t with RPS {} ops", name, count.longValue(), rps); + } + + reset(); + } + + private void printTotal(Logger logger) { + if (totalCount.longValue() > 0) { + double average = 1.0d * totalTimeMs.longValue() / totalCount.longValue(); + logger.info("{}\twas executed {} times,\twith average time {} ms/op", name, totalCount.longValue(), average); + } + } + } + + private final Logger logger; + private final Method load; + private final Method fetch; + private final Method update; + private final Method batchUpdate; + + private final AtomicInteger executionsCount = new AtomicInteger(0); + private final AtomicInteger retriesCount = new AtomicInteger(0); + + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor( + r -> new Thread(r, "ticker") + ); + + public AppMetrics(Logger logger, MeterRegistry meterRegistry) { + this.logger = logger; + this.load = new Method(meterRegistry, "LOAD ", "load"); + this.fetch = new Method(meterRegistry, "FETCH ", "read"); + this.update = new Method(meterRegistry, "UPDATE", "update"); + this.batchUpdate = new Method(meterRegistry, "BULK_UP", "batch_update"); + } + + public Method getLoad() { + return this.load; + } + + public Method getFetch() { + return this.fetch; + } + + public Method getUpdate() { + return this.update; + } + + public Method getBatchUpdate() { + return this.batchUpdate; + } + + public void runWithMonitor(Runnable runnable) { + Arrays.asList(load, fetch, update, batchUpdate).forEach(Method::reset); + final ScheduledFuture future = scheduler.scheduleAtFixedRate(this::print, 1, 10, TimeUnit.SECONDS); + runnable.run(); + future.cancel(false); + print(); + } + + public void close() throws InterruptedException { + scheduler.shutdownNow(); + scheduler.awaitTermination(20, TimeUnit.SECONDS); + + Arrays.asList(load, fetch, update, batchUpdate).forEach(m -> m.close()); + } + + private void print() { + Arrays.asList(load, fetch, update, batchUpdate).forEach(m -> m.print(logger)); + } + + public void printTotal() { + logger.info("=========== TOTAL =============="); + Arrays.asList(load, fetch, update, batchUpdate).forEach(m -> m.printTotal(logger)); + logger.info("Executed {} transactions with {} retries", executionsCount.get(), retriesCount.get()); + } + + public RetryListener getRetryListener() { + return new RetryListener() { + @Override + public boolean open(RetryContext ctx, RetryCallback cb) { + executionsCount.incrementAndGet(); + return true; + } + + @Override + public void onError(RetryContext ctx, RetryCallback cb, Throwable th) { + logger.debug("Retry operation with error {} ", printSqlException(th)); + retriesCount.incrementAndGet(); + Method m = LOCAL.get(); + if (m != null) { + m.retriesCounter.apply(extractStatusCode(th)).increment(); + } + + } + + @Override + public void onSuccess(RetryContext context, RetryCallback cb, T result) { + // nothing + } + }; + } + + private String printSqlException(Throwable th) { + Throwable ex = th; + while (ex != null) { + if (ex instanceof SQLException) { + return ex.getMessage(); + } + ex = ex.getCause(); + } + return th.getMessage(); + } + + private StatusCode extractStatusCode(Throwable th) { + Throwable ex = th; + while (ex != null) { + if (ex instanceof YdbStatusable) { + return ((YdbStatusable) ex).getStatus().getCode(); + } + ex = ex.getCause(); + } + return StatusCode.CLIENT_INTERNAL_ERROR; + } +} diff --git a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java index a4e9841..510b878 100644 --- a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java +++ b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java @@ -1,6 +1,5 @@ package tech.ydb.apps; -import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import java.util.Random; @@ -14,7 +13,6 @@ import javax.annotation.PreDestroy; -import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.MeterRegistry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -23,8 +21,6 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; -import org.springframework.retry.RetryCallback; -import org.springframework.retry.RetryContext; import org.springframework.retry.RetryListener; import org.springframework.retry.annotation.EnableRetry; @@ -50,87 +46,40 @@ public static void main(String[] args) { } } - private final Ticker ticker = new Ticker(logger); + private final AppMetrics ticker; private final Config config; private final SchemeService schemeService; private final TokenService tokenService; - private final MeterRegistry meterRegistry; private final ExecutorService executor; - private final AtomicInteger threadCounter = new AtomicInteger(0); - private final AtomicInteger executionsCount = new AtomicInteger(0); - private final AtomicInteger retriesCount = new AtomicInteger(0); - - private final Counter executionsCounter; - private final Counter errorsCounter; -// private final Counter successCounter; public Application(Config config, SchemeService schemeService, TokenService tokenService, MeterRegistry registry) { this.config = config; this.schemeService = schemeService; this.tokenService = tokenService; - this.meterRegistry = registry; + this.ticker = new AppMetrics(logger, registry); this.executor = Executors.newFixedThreadPool(config.getThreadCount(), this::threadFactory); - - this.executionsCounter = Counter.builder("sdk.operations").register(meterRegistry); - this.errorsCounter = Counter.builder("sdk.operations.errors").register(meterRegistry); -// this.successCounter = Counter.builder("OK").register(meterRegistry); } @PreDestroy public void close() throws Exception { logger.info("CLI app is waiting for finishing"); - errorsCounter.close(); - executionsCounter.close(); - executor.shutdown(); executor.awaitTermination(5, TimeUnit.MINUTES); ticker.printTotal(); ticker.close(); - logger.info("Executed {} transactions with {} retries", executionsCount.get(), retriesCount.get()); logger.info("CLI app has finished"); - } @Bean public RetryListener retryListener() { - return new RetryListener() { - @Override - public boolean open(RetryContext ctx, RetryCallback callback) { - executionsCount.incrementAndGet(); - executionsCounter.increment(); - return true; - } - - @Override - public void onError(RetryContext ctx, RetryCallback callback, Throwable th) { - logger.debug("Retry operation with error {} ", printSqlException(th)); - retriesCount.incrementAndGet(); - errorsCounter.increment(); - } - - @Override - public void close(RetryContext rc, RetryCallback callback, Throwable th) { - // nothing - } - }; - } - - private String printSqlException(Throwable th) { - Throwable ex = th; - while (ex != null) { - if (ex instanceof SQLException) { - return ex.getMessage(); - } - ex = ex.getCause(); - } - return th.getMessage(); + return ticker.getRetryListener(); } @Override @@ -182,11 +131,10 @@ private void loadData() { final int last = id < recordsCount ? id : recordsCount; futures.add(CompletableFuture.runAsync(() -> { - try (Ticker.Measure measure = ticker.getLoad().newCall()) { + ticker.getLoad().measure(() -> { tokenService.insertBatch(first, last); logger.debug("inserted tokens [{}, {})", first, last); - measure.inc(); - } + }); }, executor)); } @@ -240,27 +188,20 @@ private void workload(RateLimiter rt, long finishAt) { private void executeFetch(Random rnd, int recordCount) { int id = rnd.nextInt(recordCount); - try (Ticker.Measure measure = ticker.getFetch().newCall()) { - tokenService.fetchToken(id); - measure.inc(); - } + ticker.getFetch().measure(() -> tokenService.fetchToken(id)); } private void executeUpdate(Random rnd, int recordCount) { int id = rnd.nextInt(recordCount); - try (Ticker.Measure measure = ticker.getUpdate().newCall()) { - tokenService.updateToken(id); - measure.inc(); - } + ticker.getUpdate().measure(() -> tokenService.updateToken(id)); } private void executeBatchUpdate(Random rnd, int recordCount) { - try (Ticker.Measure measure = ticker.getBatchUpdate().newCall()) { + ticker.getBatchUpdate().measure(() -> { List randomIds = IntStream.range(0, 100) .mapToObj(idx -> rnd.nextInt(recordCount)) .collect(Collectors.toList()); tokenService.updateBatch(randomIds); - measure.inc(); - } + }); } } diff --git a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Ticker.java b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Ticker.java deleted file mode 100644 index 1948c7e..0000000 --- a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Ticker.java +++ /dev/null @@ -1,141 +0,0 @@ -package tech.ydb.apps; - -import java.util.Arrays; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.LongAdder; - -import org.slf4j.Logger; - -/** - * - * @author Aleksandr Gorshenin - */ -public class Ticker { - public class Measure implements AutoCloseable { - private final Method method; - private final long startedAt; - private long count = 0; - - public Measure(Method method) { - this.method = method; - this.startedAt = System.currentTimeMillis(); - } - - public void inc() { - count += 1; - } - - @Override - public void close() { - if (count == 0) { - return; - } - - long ms = System.currentTimeMillis() - startedAt; - - method.count.add(count); - method.totalCount.add(count); - - method.timeMs.add(ms); - method.totalTimeMs.add(ms); - } - } - - public class Method { - private final String name; - - private final LongAdder totalCount = new LongAdder(); - private final LongAdder totalTimeMs = new LongAdder(); - - private final LongAdder count = new LongAdder(); - private final LongAdder timeMs = new LongAdder(); - - private volatile long lastPrinted = 0; - - public Method(String name) { - this.name = name; - } - - public Measure newCall() { - return new Measure(this); - } - - private void reset() { - count.reset(); - timeMs.reset(); - lastPrinted = System.currentTimeMillis(); - } - - private void print(Logger logger) { - if (count.longValue() > 0 && lastPrinted != 0) { - long ms = System.currentTimeMillis() - lastPrinted; - double rps = 1000 * count.longValue() / ms; - logger.info("{}\twas executed {} times\t with RPS {} ops", name, count.longValue(), rps); - } - - reset(); - } - - private void printTotal(Logger logger) { - if (totalCount.longValue() > 0) { - double average = 1.0d * totalTimeMs.longValue() / totalCount.longValue(); - logger.info("{}\twas executed {} times,\twith average time {} ms/op", name, totalCount.longValue(), average); - } - } - } - - private final Logger logger; - private final Method load = new Method("LOAD "); - private final Method fetch = new Method("FETCH "); - private final Method update = new Method("UPDATE"); - private final Method batchUpdate = new Method("BULK_UP"); - - private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor( - r -> new Thread(r, "ticker") - ); - - public Ticker(Logger logger) { - this.logger = logger; - } - - public Method getLoad() { - return this.load; - } - - public Method getFetch() { - return this.fetch; - } - - public Method getUpdate() { - return this.update; - } - - public Method getBatchUpdate() { - return this.batchUpdate; - } - - public void runWithMonitor(Runnable runnable) { - Arrays.asList(load, fetch, update, batchUpdate).forEach(Method::reset); - final ScheduledFuture future = scheduler.scheduleAtFixedRate(this::print, 1, 10, TimeUnit.SECONDS); - runnable.run(); - future.cancel(false); - print(); - } - - public void close() throws InterruptedException { - scheduler.shutdownNow(); - scheduler.awaitTermination(20, TimeUnit.SECONDS); - } - - private void print() { - Arrays.asList(load, fetch, update, batchUpdate).forEach(m -> m.print(logger)); - } - - public void printTotal() { - logger.info("=========== TOTAL =============="); - Arrays.asList(load, fetch, update, batchUpdate).forEach(m -> m.printTotal(logger)); - } -} diff --git a/jdbc/ydb-token-app/src/main/resources/application.properties b/jdbc/ydb-token-app/src/main/resources/application.properties index a147c4a..d69023f 100644 --- a/jdbc/ydb-token-app/src/main/resources/application.properties +++ b/jdbc/ydb-token-app/src/main/resources/application.properties @@ -11,7 +11,7 @@ spring.application.name=ydb-token-app spring.datasource.url=jdbc:ydb:${app.connection} spring.datasource.driver-class-name=tech.ydb.jdbc.YdbDriver -spring.datasource.hikari.maximum-pool-size=100 +spring.datasource.hikari.maximum-pool-size=200 spring.datasource.hikari.data-source-properties.enableTxTracer=true spring.jpa.properties.hibernate.jdbc.batch_size=1000 @@ -23,6 +23,7 @@ spring.jpa.properties.hibernate.dialect=tech.ydb.hibernate.dialect.YdbDialect management.metrics.enable.all=false management.metrics.enable.jdbc=true management.metrics.enable.sdk=true +management.metrics.distribution.percentiles-histogram.all=true management.metrics.export.prometheus.pushgateway.enabled=${app.pushMetrics} management.metrics.export.prometheus.pushgateway.push-rate=15s management.metrics.export.prometheus.pushgateway.base-url=${app.prometheusUrl} From 7db59aea9216b483bac93d62f4fb0ab58174359f Mon Sep 17 00:00:00 2001 From: Alexandr Gorshenin Date: Wed, 20 Aug 2025 11:50:06 +0100 Subject: [PATCH 08/19] Fixed metric lables --- .../main/java/tech/ydb/apps/AppMetrics.java | 54 +++++++++---------- 1 file changed, 24 insertions(+), 30 deletions(-) diff --git a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/AppMetrics.java b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/AppMetrics.java index a38e563..9c8f35d 100644 --- a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/AppMetrics.java +++ b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/AppMetrics.java @@ -1,7 +1,6 @@ package tech.ydb.apps; import java.sql.SQLException; -import java.time.Duration; import java.util.Arrays; import java.util.EnumMap; import java.util.Map; @@ -14,8 +13,8 @@ import java.util.function.Function; import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.DistributionSummary; import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.Timer; import org.slf4j.Logger; import org.springframework.retry.RetryCallback; import org.springframework.retry.RetryContext; @@ -36,23 +35,10 @@ public class AppMetrics { private static final Counter.Builder SDK_OPERATIONS_FAILTURE = Counter.builder("sdk.operations.failture"); private static final Counter.Builder SDK_RETRY_ATTEMPS = Counter.builder("sdk.retry.attempts"); - private static final Timer.Builder SDK_OPERATION_LATENCY = Timer.builder("sdk.operation.latency") - .sla( - Duration.ofMillis(1), - Duration.ofMillis(2), - Duration.ofMillis(3), - Duration.ofMillis(4), - Duration.ofMillis(5), - Duration.ofMillis(8), - Duration.ofMillis(10), - Duration.ofMillis(20), - Duration.ofMillis(50), - Duration.ofMillis(100), - Duration.ofMillis(200), - Duration.ofMillis(500), - Duration.ofMillis(1000) - ); -// .publishPercentiles(); +// private static final Timer.Builder SDK_OPERATION_LATENCY = Timer.builder("sdk.operation.latency") + private static final DistributionSummary.Builder SDK_OPERATION_LATENCY = DistributionSummary + .builder("sdk.operation.latency") + .serviceLevelObjectives(0.001, 0.002, 0.003, 0.004, 0.005, 0.0075, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1); public class Method { private final String name; @@ -67,25 +53,31 @@ public class Method { private final Counter successCounter; private final Map errorsCountersMap = new EnumMap<>(StatusCode.class); private final Map retriesCountersMap = new EnumMap<>(StatusCode.class); - private final Map durationTimerMap = new EnumMap<>(StatusCode.class); + private final Map durationTimerMap = new EnumMap<>(StatusCode.class); private final Function errorCounter; private final Function retriesCounter; - private final Function durationTimer; + private final Function durationTimer; private volatile long lastPrinted = 0; public Method(MeterRegistry registry, String name, String label) { this.name = name; - this.executionsCounter = SDK_OPERATIONS.tag("method", label).register(registry); - this.successCounter = SDK_OPERATIONS_SUCCESS.tag("method", label).register(registry); - this.errorCounter = code -> errorsCountersMap.computeIfAbsent(code, key -> - SDK_OPERATIONS_FAILTURE.tag("method", label).tag("status", key.toString()).register(registry) + this.executionsCounter = SDK_OPERATIONS.tag("operation_type", label).register(registry); + this.successCounter = SDK_OPERATIONS_SUCCESS.tag("operation_type", label).register(registry); + this.errorCounter = code -> errorsCountersMap.computeIfAbsent(code, key -> SDK_OPERATIONS_FAILTURE + .tag("operation_type", label) + .tag("operation_status", key.toString()) + .register(registry) ); - this.retriesCounter = code -> retriesCountersMap.computeIfAbsent(code, key -> - SDK_RETRY_ATTEMPS.tag("method", label).tag("status", key.toString()).register(registry) + this.retriesCounter = code -> retriesCountersMap.computeIfAbsent(code, key -> SDK_RETRY_ATTEMPS + .tag("operation_type", label) + .tag("operation_status", key.toString()) + .register(registry) ); - this.durationTimer = code -> durationTimerMap.computeIfAbsent(code, key -> - SDK_OPERATION_LATENCY.tag("method", label).tag("status", key.toString()).register(registry) + this.durationTimer = code -> durationTimerMap.computeIfAbsent(code, key -> SDK_OPERATION_LATENCY + .tag("operation_type", label) + .tag("operation_status", key.toString()) + .register(registry) ); } @@ -112,13 +104,15 @@ public void measure(Runnable run) { timeMs.add(ms); totalTimeMs.add(ms); - durationTimer.apply(code).record(ms, TimeUnit.MILLISECONDS); + durationTimer.apply(code).record(0.001 * ms); } } public void close() { successCounter.close(); executionsCounter.close(); + durationTimerMap.forEach((status, counter) -> counter.close()); + retriesCountersMap.forEach((status, counter) -> counter.close()); errorsCountersMap.forEach((status, counter) -> counter.close()); } From 753e7ef2aa5e77b3d6693e485952dcc35b0bd467 Mon Sep 17 00:00:00 2001 From: Alexandr Gorshenin Date: Thu, 21 Aug 2025 09:55:36 +0100 Subject: [PATCH 09/19] Added applicaiton interrupt by Ctrl-C --- .../src/main/java/tech/ydb/apps/Application.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java index 510b878..03383e7 100644 --- a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java +++ b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java @@ -54,6 +54,7 @@ public static void main(String[] args) { private final ExecutorService executor; private final AtomicInteger threadCounter = new AtomicInteger(0); + private volatile boolean isStopped = false; public Application(Config config, SchemeService schemeService, TokenService tokenService, MeterRegistry registry) { this.config = config; @@ -66,6 +67,7 @@ public Application(Config config, SchemeService schemeService, TokenService toke @PreDestroy public void close() throws Exception { + isStopped = true; logger.info("CLI app is waiting for finishing"); executor.shutdown(); @@ -131,6 +133,10 @@ private void loadData() { final int last = id < recordsCount ? id : recordsCount; futures.add(CompletableFuture.runAsync(() -> { + if (isStopped) { + return; + } + ticker.getLoad().measure(() -> { tokenService.insertBatch(first, last); logger.debug("inserted tokens [{}, {})", first, last); @@ -168,7 +174,7 @@ private void workload(RateLimiter rt, long finishAt) { final Random rnd = new Random(); final int recordCount = config.getRecordsCount(); - while (System.currentTimeMillis() < finishAt) { + while ((System.currentTimeMillis() < finishAt) && !isStopped) { rt.acquire(); int mode = rnd.nextInt(10); From 1525e1044f758107954df1cac498dbf6fecefae2 Mon Sep 17 00:00:00 2001 From: Alexandr Gorshenin Date: Thu, 21 Aug 2025 10:40:31 +0100 Subject: [PATCH 10/19] Added README.md --- jdbc/ydb-token-app/README.md | 75 ++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 jdbc/ydb-token-app/README.md diff --git a/jdbc/ydb-token-app/README.md b/jdbc/ydb-token-app/README.md new file mode 100644 index 0000000..809fa09 --- /dev/null +++ b/jdbc/ydb-token-app/README.md @@ -0,0 +1,75 @@ +## YDB Token Application +## A simple example of Spring Boot 2 Application working with YDB Database + +### How to build + +Requirements +* Java 17 or newer +* Maven 3.0.0 or newer + +To build the application as a single executable jar file, run the command: +``` +cd ydb-java-examples/jdbc/ydb-token-app +mvn clean package spring-boot:repackage +``` +After that, the compiled `ydb-token-app-1.1.0-SNAPSHOT.jar` can be found in the target folder. + +### What this application does + +This application allows you to create a test table called `app_token` in the YDB database, populate it with data, and +launch a test workload for parallel reading and writing to this table. During the test, the following operations will be +performed in parallel in several threads: +* Read a random token from the database - 50% of operations +* Read and update a random token in the database - 40% of operations +* Read and update 100 random tokens in the database - 10% of operations + +The statistics collected during the test include the number of operations performed, RPS (requests per second), and +average execution time for each type of operation. There is also support for exporting application metrics in Prometheus +format. + +### How to launch + +The application is built as a single executable jar file and can be run with the command: +``` +java -jar ydb-token-app-1.1.0-SNAPSHOT.jar +``` +Where `options` are application parameters (see the Application Parameters section), and `commands` are the sequence of +commands the application will execute one after the other. Currently, the following commands are supported: +* clean - clean the database, the `app_token` table will be deleted +* init - prepare the database, the empty `app_token` table will be created +* load - load test data, the `app_token` table will be filled with initial data +* run - start the test workload + +Commands can be used individually or sequenced, for example: + +Recreate the `app_token` table and initialize it with initial data: +``` +java -jar ydb-token-app-1.1.0-SNAPSHOT.jar --app.connection=grpcs://my-ydb:2135/my-database clean init load +``` + +Start the test and then clean the database: +``` +java -jar ydb-token-app-1.1.0-SNAPSHOT.jar --app.connection=grpcs://my-ydb:2135/my-database run clean +``` + +Recreate the `app_token` table, initialize it with data, and start the test: +``` +java -jar ydb-token-app-1.1.0-SNAPSHOT.jar --app.connection=grpcs://my-ydb:2135/my-database clean init load run +``` + +### Application parameters + +Application parameters allow you to configure different aspects of the application's operation, primarily the database connection address. +The main parameters list: + +* `app.connection` - database connection address. Specified as `://:/` +* `app.threadsCount` - number of threads the application creates. Defaults to the number of CPU cores on the host. +* `app.recordsCount` - number of records in the table used for testing. Default is 1 million. +* `app.load.batchSize` - batch size for loading data when running the load command. Default is 1000. +* `app.workload.duration` - test duration in seconds when running the run command. Default is 60 seconds. +* `app.rpsLimit` - limit on the number of operations per second during the run command. By default, there is no limit (-1). +* `app.pushMetrics` - flag indicating whether metrics should be exported to Prometheus; disabled by default. +* `app.prometheusUrl` - endpoint of Prometheus to export metrics to. Default is http://localhost:9091. + +All parameters can be passed directly when launching the application (in the format `--param_name=value`) or can be +preconfigured in an `application.properties` file saved next to the executable jar of the application. From 9ae82874a5af0b00cf1fdf028d8667b885f6173e Mon Sep 17 00:00:00 2001 From: Alexandr Gorshenin Date: Fri, 22 Aug 2025 14:06:26 +0100 Subject: [PATCH 11/19] Added grpc calls metrics --- .../main/java/tech/ydb/apps/Application.java | 2 + .../main/java/tech/ydb/apps/GrpcMetrics.java | 133 ++++++++++++++++++ .../src/main/resources/application.properties | 2 + 3 files changed, 137 insertions(+) create mode 100644 jdbc/ydb-token-app/src/main/java/tech/ydb/apps/GrpcMetrics.java diff --git a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java index 03383e7..51ac193 100644 --- a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java +++ b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java @@ -57,6 +57,8 @@ public static void main(String[] args) { private volatile boolean isStopped = false; public Application(Config config, SchemeService schemeService, TokenService tokenService, MeterRegistry registry) { + GrpcMetrics.init(registry); + this.config = config; this.schemeService = schemeService; this.tokenService = tokenService; diff --git a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/GrpcMetrics.java b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/GrpcMetrics.java new file mode 100644 index 0000000..15df6e6 --- /dev/null +++ b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/GrpcMetrics.java @@ -0,0 +1,133 @@ +package tech.ydb.apps; + +import java.util.function.Consumer; + +import javax.annotation.Nullable; + +import io.grpc.Attributes; +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.ManagedChannelBuilder; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.Status; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; + +/** + * + * @author Aleksandr Gorshenin + */ +public class GrpcMetrics implements Consumer>, ClientInterceptor { + private static final Counter.Builder REQUEST = Counter.builder("grpc.request"); + private static final Counter.Builder RESPONSE = Counter.builder("grpc.response"); + + private static MeterRegistry REGISTRY = null; + + public static void init(MeterRegistry registry) { + REGISTRY = registry; + } + + @Override + public void accept(ManagedChannelBuilder t) { + t.intercept(this); + } + + @Override + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel next) { + if (REGISTRY != null) { + return new ProxyClientCall<>(REGISTRY, next, method, callOptions); + } + return next.newCall(method, callOptions); + } + + private static class ProxyClientCall extends ClientCall { + private final MeterRegistry registry; + private final String method; + private final String authority; + private final ClientCall delegate; + + private ProxyClientCall(MeterRegistry registry, Channel channel, MethodDescriptor method, + CallOptions callOptions) { + this.registry = registry; + this.method = method.getBareMethodName(); + this.authority = channel.authority(); + this.delegate = channel.newCall(method, callOptions); + } + + @Override + public void request(int numMessages) { + delegate.request(numMessages); + } + + @Override + public void cancel(@Nullable String message, @Nullable Throwable cause) { + delegate.cancel(message, cause); + } + + @Override + public void halfClose() { + delegate.halfClose(); + } + + @Override + public void setMessageCompression(boolean enabled) { + delegate.setMessageCompression(enabled); + } + + @Override + public boolean isReady() { + return delegate.isReady(); + } + + @Override + public Attributes getAttributes() { + return delegate.getAttributes(); + } + + @Override + public void start(Listener listener, Metadata headers) { + REQUEST.tag("method", method).tag("authority", authority).register(registry).increment(); + delegate.start(new ProxyListener(listener), headers); + } + + @Override + public void sendMessage(ReqT message) { + delegate.sendMessage(message); + } + + private class ProxyListener extends Listener { + private final Listener delegate; + + public ProxyListener(Listener delegate) { + this.delegate = delegate; + } + + + @Override + public void onHeaders(Metadata headers) { + delegate.onHeaders(headers); + } + + @Override + public void onMessage(RespT message) { + delegate.onMessage(message); + } + + @Override + public void onClose(Status status, Metadata trailers) { + RESPONSE.tag("method", method).tag("authority", authority).tag("status", status.getCode().toString()) + .register(registry).increment(); + delegate.onClose(status, trailers); + } + + @Override + public void onReady() { + delegate.onReady(); + } + } + } +} diff --git a/jdbc/ydb-token-app/src/main/resources/application.properties b/jdbc/ydb-token-app/src/main/resources/application.properties index d69023f..ea8032b 100644 --- a/jdbc/ydb-token-app/src/main/resources/application.properties +++ b/jdbc/ydb-token-app/src/main/resources/application.properties @@ -13,6 +13,7 @@ spring.datasource.driver-class-name=tech.ydb.jdbc.YdbDriver spring.datasource.hikari.maximum-pool-size=200 spring.datasource.hikari.data-source-properties.enableTxTracer=true +spring.datasource.hikari.data-source-properties.channelInitializer=tech.ydb.apps.GrpcMetrics spring.jpa.properties.hibernate.jdbc.batch_size=1000 spring.jpa.properties.hibernate.order_updates=true @@ -23,6 +24,7 @@ spring.jpa.properties.hibernate.dialect=tech.ydb.hibernate.dialect.YdbDialect management.metrics.enable.all=false management.metrics.enable.jdbc=true management.metrics.enable.sdk=true +management.metrics.enable.grpc=true management.metrics.distribution.percentiles-histogram.all=true management.metrics.export.prometheus.pushgateway.enabled=${app.pushMetrics} management.metrics.export.prometheus.pushgateway.push-rate=15s From c68cfb631a804d86d205fe367b8924d7ab17a661 Mon Sep 17 00:00:00 2001 From: Alexandr Gorshenin Date: Mon, 25 Aug 2025 15:16:00 +0100 Subject: [PATCH 12/19] Added log table --- .../main/java/tech/ydb/apps/Application.java | 33 ++++--- .../src/main/java/tech/ydb/apps/Config.java | 6 +- .../java/tech/ydb/apps/entity/TokenLog.java | 99 +++++++++++++++++++ .../ydb/apps/repo/TokenLogRepository.java | 17 ++++ ...TokenService.java => WorkloadService.java} | 52 +++++----- .../src/main/resources/sql/drop.sql | 1 + .../src/main/resources/sql/init.sql | 9 ++ 7 files changed, 175 insertions(+), 42 deletions(-) create mode 100644 jdbc/ydb-token-app/src/main/java/tech/ydb/apps/entity/TokenLog.java create mode 100644 jdbc/ydb-token-app/src/main/java/tech/ydb/apps/repo/TokenLogRepository.java rename jdbc/ydb-token-app/src/main/java/tech/ydb/apps/service/{TokenService.java => WorkloadService.java} (59%) diff --git a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java index 51ac193..7105aea 100644 --- a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java +++ b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java @@ -3,11 +3,13 @@ import java.util.ArrayList; import java.util.List; import java.util.Random; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -25,7 +27,7 @@ import org.springframework.retry.annotation.EnableRetry; import tech.ydb.apps.service.SchemeService; -import tech.ydb.apps.service.TokenService; +import tech.ydb.apps.service.WorkloadService; import tech.ydb.jdbc.YdbTracer; /** @@ -50,18 +52,19 @@ public static void main(String[] args) { private final Config config; private final SchemeService schemeService; - private final TokenService tokenService; + private final WorkloadService workloadService; private final ExecutorService executor; private final AtomicInteger threadCounter = new AtomicInteger(0); + private final AtomicLong logCounter = new AtomicLong(0); private volatile boolean isStopped = false; - public Application(Config config, SchemeService schemeService, TokenService tokenService, MeterRegistry registry) { + public Application(Config config, SchemeService scheme, WorkloadService worload, MeterRegistry registry) { GrpcMetrics.init(registry); this.config = config; - this.schemeService = schemeService; - this.tokenService = tokenService; + this.schemeService = scheme; + this.workloadService = worload; this.ticker = new AppMetrics(logger, registry); this.executor = Executors.newFixedThreadPool(config.getThreadCount(), this::threadFactory); @@ -140,7 +143,7 @@ private void loadData() { } ticker.getLoad().measure(() -> { - tokenService.insertBatch(first, last); + workloadService.loadData(first, last); logger.debug("inserted tokens [{}, {})", first, last); }); }, executor)); @@ -154,15 +157,16 @@ private void test() { int recordsCount = config.getRecordsCount(); final Random rnd = new Random(); - List randomIds = IntStream.range(0, 100) + Set randomIds = IntStream.range(0, 100) .mapToObj(idx -> rnd.nextInt(recordsCount)) - .collect(Collectors.toList()); + .collect(Collectors.toSet()); - tokenService.updateBatch(randomIds); + workloadService.updateBatch(randomIds, 0); } private void runWorkloads() { RateLimiter rt = config.getRpsLimiter(); + logCounter.set(workloadService.readLastGlobalVersion()); long finishAt = System.currentTimeMillis() + config.getWorkloadDurationSec() * 1000; List> futures = new ArrayList<>(); for (int i = 0; i < config.getThreadCount(); i++) { @@ -196,20 +200,21 @@ private void workload(RateLimiter rt, long finishAt) { private void executeFetch(Random rnd, int recordCount) { int id = rnd.nextInt(recordCount); - ticker.getFetch().measure(() -> tokenService.fetchToken(id)); + ticker.getFetch().measure(() -> workloadService.fetchToken(id)); } private void executeUpdate(Random rnd, int recordCount) { int id = rnd.nextInt(recordCount); - ticker.getUpdate().measure(() -> tokenService.updateToken(id)); + ticker.getUpdate().measure(() -> workloadService.updateToken(id, logCounter.incrementAndGet())); } private void executeBatchUpdate(Random rnd, int recordCount) { ticker.getBatchUpdate().measure(() -> { - List randomIds = IntStream.range(0, 100) + Set randomIds = IntStream.range(0, 100) .mapToObj(idx -> rnd.nextInt(recordCount)) - .collect(Collectors.toList()); - tokenService.updateBatch(randomIds); + .collect(Collectors.toSet()); + long counter = logCounter.getAndAdd(randomIds.size()) + 1; + workloadService.updateBatch(randomIds, counter); }); } } diff --git a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Config.java b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Config.java index 211bfe3..7ad8942 100644 --- a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Config.java +++ b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Config.java @@ -19,10 +19,8 @@ public class Config { private final int workloadDurationSec; private final int rpsLimit; - public Config(String connection, int threadsCount, int recordsCount, - @Name("load.batchSize") int loadBatchSize, @Name("workload.duration") int workloadDuration, - int rpsLimit - ) { + public Config(String connection, int threadsCount, int recordsCount, @Name("load.batchSize") int loadBatchSize, + @Name("workload.duration") int workloadDuration, int rpsLimit) { this.connection = connection; this.threadsCount = threadsCount <= 0 ? Runtime.getRuntime().availableProcessors() : threadsCount; this.recordsCount = recordsCount; diff --git a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/entity/TokenLog.java b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/entity/TokenLog.java new file mode 100644 index 0000000..c08e038 --- /dev/null +++ b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/entity/TokenLog.java @@ -0,0 +1,99 @@ +package tech.ydb.apps.entity; + +import java.io.Serializable; +import java.time.Instant; +import java.util.UUID; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import javax.persistence.Transient; + +import com.google.common.hash.Hasher; +import com.google.common.hash.Hashing; +import org.hibernate.annotations.DynamicUpdate; +import org.springframework.data.domain.Persistable; + +/** + * + * @author Aleksandr Gorshenin + */ +@Entity +@DynamicUpdate +@Table(name = "app_token_log") +public class TokenLog implements Serializable, Persistable { + private static final long serialVersionUID = -3643491448443852677L; + + @Id + private String id; + + @ManyToOne + private Token token; + + @Column + private Long globalVersion; + + @Column + private Instant updatedAt; + + @Column + private Integer updatedTo; + + @Transient + private final boolean isNew; + + @Override + public String getId() { + return this.id; + } + + public Token getToken() { + return this.token; + } + + public Long getGlobalVersion() { + return this.globalVersion; + } + + public Instant getUpdatedAt() { + return this.updatedAt; + } + + public Integer getUpdatedTo() { + return this.updatedTo; + } + + @Override + public boolean isNew() { + return isNew; + } + + public TokenLog() { + this.isNew = false; + } + + public TokenLog(Token token, long version) { + this.id = hash256(token.getId(), version); + this.globalVersion = version; + this.token = token; + this.updatedAt = Instant.now(); + this.updatedTo = token.getVersion(); + this.isNew = true; + } + + @Override + public String toString() { + return "TokenLog{version=" + globalVersion + ", token=" + token.getId() + + ", updateAt='" + updatedAt + "', updateTo=" + updatedTo + "}"; + } + + private static String hash256(UUID uuid, long version) { + Hasher hasher = Hashing.sha256().newHasher(24); + hasher.putLong(uuid.getMostSignificantBits()); + hasher.putLong(uuid.getLeastSignificantBits()); + hasher.putLong(version); + return hasher.hash().toString(); + } +} diff --git a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/repo/TokenLogRepository.java b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/repo/TokenLogRepository.java new file mode 100644 index 0000000..bc581f6 --- /dev/null +++ b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/repo/TokenLogRepository.java @@ -0,0 +1,17 @@ +package tech.ydb.apps.repo; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; + +import tech.ydb.apps.entity.TokenLog; + +/** + * + * @author Aleksandr Gorshenin + */ +public interface TokenLogRepository extends CrudRepository { + @Query("SELECT MAX(log.globalVersion) FROM TokenLog log") + Optional findTopGlobalVersion(); +} diff --git a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/service/TokenService.java b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/service/WorkloadService.java similarity index 59% rename from jdbc/ydb-token-app/src/main/java/tech/ydb/apps/service/TokenService.java rename to jdbc/ydb-token-app/src/main/java/tech/ydb/apps/service/WorkloadService.java index d2aff7d..1f449e2 100644 --- a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/service/TokenService.java +++ b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/service/WorkloadService.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; @@ -13,6 +14,8 @@ import tech.ydb.apps.annotation.YdbRetryable; import tech.ydb.apps.entity.Token; +import tech.ydb.apps.entity.TokenLog; +import tech.ydb.apps.repo.TokenLogRepository; import tech.ydb.apps.repo.TokenRepository; /** @@ -20,12 +23,14 @@ * @author Aleksandr Gorshenin */ @Service -public class TokenService { - private final static Logger logger = LoggerFactory.getLogger(TokenService.class); - private final TokenRepository repository; - - public TokenService(TokenRepository repository) { - this.repository = repository; +public class WorkloadService { + private final static Logger logger = LoggerFactory.getLogger(WorkloadService.class); + private final TokenRepository tokenRepo; + private final TokenLogRepository tokenLogRepo; + + public WorkloadService(TokenRepository tokenRepo, TokenLogRepository tokenLogRepo) { + this.tokenRepo = tokenRepo; + this.tokenLogRepo = tokenLogRepo; } private UUID getKey(int id) { @@ -34,18 +39,18 @@ private UUID getKey(int id) { @YdbRetryable @Transactional - public void insertBatch(int firstID, int lastID) { + public void loadData(int firstID, int lastID) { List batch = new ArrayList<>(); for (int id = firstID; id < lastID; id++) { batch.add(new Token("user_" + id)); } - repository.saveAll(batch); + tokenRepo.saveAll(batch); } @YdbRetryable @Transactional public Token fetchToken(int id) { - Optional token = repository.findById(getKey(id)); + Optional token = tokenRepo.findById(getKey(id)); if (!token.isPresent()) { logger.warn("token {} is not found", id); @@ -57,45 +62,44 @@ public Token fetchToken(int id) { @YdbRetryable @Transactional - public void updateToken(int id) { + public void updateToken(int id, long counter) { Token token = fetchToken(id); if (token != null) { token.incVersion(); - repository.save(token); + tokenRepo.save(token); + tokenLogRepo.save(new TokenLog(token, counter)); logger.trace("updated token {} -> {}", id, token.getVersion()); + } else { + logger.warn("token {} is not found", id); } } @YdbRetryable @Transactional - public void updateBatch(List ids) { + public void updateBatch(Set ids, long counterFrom) { List uuids = ids.stream().map(this::getKey).collect(Collectors.toList()); - Iterable batch = repository.findAllById(uuids); + Iterable batch = tokenRepo.findAllById(uuids); + List logs = new ArrayList<>(); for (Token token: batch) { logger.trace("update token {}", token); token.incVersion(); + logs.add(new TokenLog(token, counterFrom++)); } - repository.saveAllAndFlush(batch); + tokenRepo.saveAll(batch); + tokenLogRepo.saveAll(logs); } @YdbRetryable @Transactional public void removeBatch(List ids) { List uuids = ids.stream().map(this::getKey).collect(Collectors.toList()); - repository.deleteAllByIdInBatch(uuids); + tokenRepo.deleteAllByIdInBatch(uuids); } @YdbRetryable - @Transactional - public void listManyRecords() { - long count = 0; - for (String id : repository.scanFindAll()) { - count ++; - if (count % 1000 == 0) { - logger.info("scan readed {} records", count); - } - } + public long readLastGlobalVersion() { + return tokenLogRepo.findTopGlobalVersion().orElse(0l); } } diff --git a/jdbc/ydb-token-app/src/main/resources/sql/drop.sql b/jdbc/ydb-token-app/src/main/resources/sql/drop.sql index 59e12eb..41a0356 100644 --- a/jdbc/ydb-token-app/src/main/resources/sql/drop.sql +++ b/jdbc/ydb-token-app/src/main/resources/sql/drop.sql @@ -1 +1,2 @@ DROP TABLE IF EXISTS app_token; +DROP TABLE IF EXISTS app_token_log; diff --git a/jdbc/ydb-token-app/src/main/resources/sql/init.sql b/jdbc/ydb-token-app/src/main/resources/sql/init.sql index 5426b4f..2a364bf 100644 --- a/jdbc/ydb-token-app/src/main/resources/sql/init.sql +++ b/jdbc/ydb-token-app/src/main/resources/sql/init.sql @@ -4,3 +4,12 @@ CREATE TABLE app_token ( version Int32, PRIMARY KEY (id) ); + +CREATE TABLE app_token_log ( + id Text NOT NULL, + global_version Int64 NOT NULl, + token_id Text NOT NULL, + updated_at Timestamp, + updated_to Int32, + PRIMARY KEY (id) +); From 5c8744da56f95f50bdaed784a94c3a71b6dd8601 Mon Sep 17 00:00:00 2001 From: Alexandr Gorshenin Date: Mon, 25 Aug 2025 15:27:13 +0100 Subject: [PATCH 13/19] Added validate phase --- jdbc/ydb-token-app/README.md | 9 +++++---- .../src/main/java/tech/ydb/apps/Application.java | 11 +++++++++++ .../main/java/tech/ydb/apps/repo/TokenRepository.java | 4 ++++ .../java/tech/ydb/apps/service/WorkloadService.java | 10 ++++++++++ 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/jdbc/ydb-token-app/README.md b/jdbc/ydb-token-app/README.md index 809fa09..d95f6bf 100644 --- a/jdbc/ydb-token-app/README.md +++ b/jdbc/ydb-token-app/README.md @@ -35,10 +35,11 @@ java -jar ydb-token-app-1.1.0-SNAPSHOT.jar ``` Where `options` are application parameters (see the Application Parameters section), and `commands` are the sequence of commands the application will execute one after the other. Currently, the following commands are supported: -* clean - clean the database, the `app_token` table will be deleted -* init - prepare the database, the empty `app_token` table will be created -* load - load test data, the `app_token` table will be filled with initial data -* run - start the test workload +* clean - clean the database, the `app_token` table will be deleted +* init - prepare the database, the empty `app_token` table will be created +* load - load test data, the `app_token` table will be filled with initial data +* run - start the test workload +* validate - validate current data stored in database Commands can be used individually or sequenced, for example: diff --git a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java index 7105aea..1b1fa2a 100644 --- a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java +++ b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java @@ -116,6 +116,10 @@ public void run(String... args) { ticker.runWithMonitor(this::runWorkloads); } + if ("validate".equalsIgnoreCase(arg)) { + executeValidate(); + } + if ("test".equalsIgnoreCase(arg)) { ticker.runWithMonitor(this::test); } @@ -217,4 +221,11 @@ private void executeBatchUpdate(Random rnd, int recordCount) { workloadService.updateBatch(randomIds, counter); }); } + + private void executeValidate() { + logger.info("=========== VALIDATE =============="); + logger.info("Log table size = {}", workloadService.countTokenLogs()); + logger.info("Last log version = {}", workloadService.readLastGlobalVersion()); + logger.info("Token updates count = {}", workloadService.countAllTokenUpdates()); + } } diff --git a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/repo/TokenRepository.java b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/repo/TokenRepository.java index e6f3a13..fcd02a1 100644 --- a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/repo/TokenRepository.java +++ b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/repo/TokenRepository.java @@ -1,5 +1,6 @@ package tech.ydb.apps.repo; +import java.util.Optional; import java.util.UUID; import org.springframework.data.jpa.repository.Query; @@ -19,4 +20,7 @@ public interface TokenRepository extends CrudRepository { void saveAllAndFlush(Iterable list); void deleteAllByIdInBatch(Iterable ids); + + @Query("SELECT SUM(token.version - 1) FROM Token token") + Optional countAllUpdates(); } diff --git a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/service/WorkloadService.java b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/service/WorkloadService.java index 1f449e2..35268bd 100644 --- a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/service/WorkloadService.java +++ b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/service/WorkloadService.java @@ -102,4 +102,14 @@ public void removeBatch(List ids) { public long readLastGlobalVersion() { return tokenLogRepo.findTopGlobalVersion().orElse(0l); } + + @YdbRetryable + public long countTokenLogs() { + return tokenLogRepo.count(); + } + + @YdbRetryable + public long countAllTokenUpdates() { + return tokenRepo.countAllUpdates().orElse(0l); + } } From 5abd464f830b87d86f9227aaa24501297a3d6727 Mon Sep 17 00:00:00 2001 From: Alexandr Gorshenin Date: Mon, 25 Aug 2025 20:09:23 +0100 Subject: [PATCH 14/19] Added failture count --- jdbc/ydb-token-app/pom.xml | 1 - .../main/java/tech/ydb/apps/AppMetrics.java | 19 +++++++++++++++---- .../main/java/tech/ydb/apps/Application.java | 1 + .../src/main/java/tech/ydb/apps/Config.java | 5 +++-- .../ydb/apps/annotation/YdbRetryable.java | 2 +- .../ydb/apps/service/WorkloadService.java | 5 ++++- .../src/main/resources/application.properties | 2 ++ 7 files changed, 26 insertions(+), 9 deletions(-) diff --git a/jdbc/ydb-token-app/pom.xml b/jdbc/ydb-token-app/pom.xml index cd6585b..dc3bf81 100644 --- a/jdbc/ydb-token-app/pom.xml +++ b/jdbc/ydb-token-app/pom.xml @@ -87,6 +87,5 @@ 2.0.12 - \ No newline at end of file diff --git a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/AppMetrics.java b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/AppMetrics.java index 9c8f35d..f802072 100644 --- a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/AppMetrics.java +++ b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/AppMetrics.java @@ -147,6 +147,7 @@ private void printTotal(Logger logger) { private final Method batchUpdate; private final AtomicInteger executionsCount = new AtomicInteger(0); + private final AtomicInteger failturesCount = new AtomicInteger(0); private final AtomicInteger retriesCount = new AtomicInteger(0); private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor( @@ -177,6 +178,10 @@ public Method getBatchUpdate() { return this.batchUpdate; } + public void incrementFaiture() { + failturesCount.incrementAndGet(); + } + public void runWithMonitor(Runnable runnable) { Arrays.asList(load, fetch, update, batchUpdate).forEach(Method::reset); final ScheduledFuture future = scheduler.scheduleAtFixedRate(this::print, 1, 10, TimeUnit.SECONDS); @@ -197,9 +202,16 @@ private void print() { } public void printTotal() { - logger.info("=========== TOTAL =============="); - Arrays.asList(load, fetch, update, batchUpdate).forEach(m -> m.printTotal(logger)); - logger.info("Executed {} transactions with {} retries", executionsCount.get(), retriesCount.get()); + if (failturesCount.get() > 0) { + logger.error("=========== TOTAL =============="); + Arrays.asList(load, fetch, update, batchUpdate).forEach(m -> m.printTotal(logger)); + logger.error("Executed {} transactions with {} retries and {} failtures", executionsCount.get(), + retriesCount.get() - failturesCount.get(), failturesCount.get()); + } else { + logger.info("=========== TOTAL =============="); + Arrays.asList(load, fetch, update, batchUpdate).forEach(m -> m.printTotal(logger)); + logger.info("Executed {} transactions with {} retries", executionsCount.get(), retriesCount.get()); + } } public RetryListener getRetryListener() { @@ -218,7 +230,6 @@ public void onError(RetryContext ctx, RetryCallback {}", id, token.getVersion()); } else { logger.warn("token {} is not found", id); + throw new EntityNotFoundException("token " + id + " is not found"); } } diff --git a/jdbc/ydb-token-app/src/main/resources/application.properties b/jdbc/ydb-token-app/src/main/resources/application.properties index ea8032b..ab5ec9b 100644 --- a/jdbc/ydb-token-app/src/main/resources/application.properties +++ b/jdbc/ydb-token-app/src/main/resources/application.properties @@ -26,6 +26,8 @@ management.metrics.enable.jdbc=true management.metrics.enable.sdk=true management.metrics.enable.grpc=true management.metrics.distribution.percentiles-histogram.all=true +management.metrics.distribution.sla.all=true + management.metrics.export.prometheus.pushgateway.enabled=${app.pushMetrics} management.metrics.export.prometheus.pushgateway.push-rate=15s management.metrics.export.prometheus.pushgateway.base-url=${app.prometheusUrl} From c0cde2c8f8c025d25cd708026c85f373acdb9357 Mon Sep 17 00:00:00 2001 From: Alexandr Gorshenin Date: Tue, 26 Aug 2025 11:24:02 +0100 Subject: [PATCH 15/19] Updated latency metric --- jdbc/ydb-token-app/pom.xml | 4 ++++ .../main/java/tech/ydb/apps/AppMetrics.java | 18 ++++++++++-------- .../src/main/resources/application.properties | 9 +++++++-- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/jdbc/ydb-token-app/pom.xml b/jdbc/ydb-token-app/pom.xml index dc3bf81..4591b1d 100644 --- a/jdbc/ydb-token-app/pom.xml +++ b/jdbc/ydb-token-app/pom.xml @@ -27,6 +27,10 @@ org.springframework.boot spring-boot-starter-data-jpa + org.springframework.boot diff --git a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/AppMetrics.java b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/AppMetrics.java index f802072..43282e7 100644 --- a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/AppMetrics.java +++ b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/AppMetrics.java @@ -1,6 +1,7 @@ package tech.ydb.apps; import java.sql.SQLException; +import java.time.Duration; import java.util.Arrays; import java.util.EnumMap; import java.util.Map; @@ -13,8 +14,8 @@ import java.util.function.Function; import io.micrometer.core.instrument.Counter; -import io.micrometer.core.instrument.DistributionSummary; import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; import org.slf4j.Logger; import org.springframework.retry.RetryCallback; import org.springframework.retry.RetryContext; @@ -35,10 +36,11 @@ public class AppMetrics { private static final Counter.Builder SDK_OPERATIONS_FAILTURE = Counter.builder("sdk.operations.failture"); private static final Counter.Builder SDK_RETRY_ATTEMPS = Counter.builder("sdk.retry.attempts"); -// private static final Timer.Builder SDK_OPERATION_LATENCY = Timer.builder("sdk.operation.latency") - private static final DistributionSummary.Builder SDK_OPERATION_LATENCY = DistributionSummary - .builder("sdk.operation.latency") - .serviceLevelObjectives(0.001, 0.002, 0.003, 0.004, 0.005, 0.0075, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1); + private static final Timer.Builder SDK_OPERATION_LATENCY = Timer.builder("sdk.operation.latency") +// .serviceLevelObjectives(Duration.ofMillis(8), Duration.ofMillis(16), Duration.ofMillis(32), +// Duration.ofMillis(64), Duration.ofMillis(128), Duration.ofMillis(256), Duration.ofMillis(512), +// Duration.ofMillis(1024), Duration.ofMillis(2048), Duration.ofMillis(4096)) + .publishPercentiles(0.5, 0.9, 0.95, 0.99); public class Method { private final String name; @@ -53,10 +55,10 @@ public class Method { private final Counter successCounter; private final Map errorsCountersMap = new EnumMap<>(StatusCode.class); private final Map retriesCountersMap = new EnumMap<>(StatusCode.class); - private final Map durationTimerMap = new EnumMap<>(StatusCode.class); + private final Map durationTimerMap = new EnumMap<>(StatusCode.class); private final Function errorCounter; private final Function retriesCounter; - private final Function durationTimer; + private final Function durationTimer; private volatile long lastPrinted = 0; @@ -104,7 +106,7 @@ public void measure(Runnable run) { timeMs.add(ms); totalTimeMs.add(ms); - durationTimer.apply(code).record(0.001 * ms); + durationTimer.apply(code).record(Duration.ofMillis(ms)); } } diff --git a/jdbc/ydb-token-app/src/main/resources/application.properties b/jdbc/ydb-token-app/src/main/resources/application.properties index ab5ec9b..9b56f33 100644 --- a/jdbc/ydb-token-app/src/main/resources/application.properties +++ b/jdbc/ydb-token-app/src/main/resources/application.properties @@ -25,8 +25,13 @@ management.metrics.enable.all=false management.metrics.enable.jdbc=true management.metrics.enable.sdk=true management.metrics.enable.grpc=true -management.metrics.distribution.percentiles-histogram.all=true -management.metrics.distribution.sla.all=true + +# Enable Spring Boot Actuator +#management.endpoints.web.exposure.include=health,metrics,prometheus,info +#management.endpoint.health.enabled=true +#management.endpoint.metrics.enabled=true +#management.endpoint.prometheus.enabled=true +#management.metrics.export.prometheus.enabled=true management.metrics.export.prometheus.pushgateway.enabled=${app.pushMetrics} management.metrics.export.prometheus.pushgateway.push-rate=15s From d931bf9190bafb09f6e63edd8a63a0315cc9a408 Mon Sep 17 00:00:00 2001 From: Alexandr Gorshenin Date: Tue, 26 Aug 2025 11:28:09 +0100 Subject: [PATCH 16/19] Decrease push rate to 1s --- jdbc/ydb-token-app/src/main/resources/application.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jdbc/ydb-token-app/src/main/resources/application.properties b/jdbc/ydb-token-app/src/main/resources/application.properties index 9b56f33..1cc894b 100644 --- a/jdbc/ydb-token-app/src/main/resources/application.properties +++ b/jdbc/ydb-token-app/src/main/resources/application.properties @@ -34,7 +34,7 @@ management.metrics.enable.grpc=true #management.metrics.export.prometheus.enabled=true management.metrics.export.prometheus.pushgateway.enabled=${app.pushMetrics} -management.metrics.export.prometheus.pushgateway.push-rate=15s +management.metrics.export.prometheus.pushgateway.push-rate=1s management.metrics.export.prometheus.pushgateway.base-url=${app.prometheusUrl} logging.pattern.console=%clr(%d{yyyy-MM-dd'T'HH:mm:ss.SSS}){faint} %clr(%5p) %clr(---){faint} %clr([%12.12t]){faint} %clr(%-24.24logger{1}){cyan} %clr(:){faint} %m%n%wEx From 1e38902a4db4c482ead1ead3df2d10902aa2803e Mon Sep 17 00:00:00 2001 From: Alexandr Gorshenin Date: Tue, 26 Aug 2025 14:31:47 +0100 Subject: [PATCH 17/19] Enabled autopartition by load --- jdbc/ydb-token-app/src/main/resources/sql/init.sql | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/jdbc/ydb-token-app/src/main/resources/sql/init.sql b/jdbc/ydb-token-app/src/main/resources/sql/init.sql index 2a364bf..c4e2980 100644 --- a/jdbc/ydb-token-app/src/main/resources/sql/init.sql +++ b/jdbc/ydb-token-app/src/main/resources/sql/init.sql @@ -3,6 +3,8 @@ CREATE TABLE app_token ( username Text, version Int32, PRIMARY KEY (id) +) WITH ( + AUTO_PARTITIONING_BY_LOAD=ENABLED ); CREATE TABLE app_token_log ( @@ -12,4 +14,6 @@ CREATE TABLE app_token_log ( updated_at Timestamp, updated_to Int32, PRIMARY KEY (id) +) WITH ( + AUTO_PARTITIONING_BY_LOAD=ENABLED ); From b8be40c8861673e7a4d9a9a36404e6ba3e9e9bf5 Mon Sep 17 00:00:00 2001 From: Alexandr Gorshenin Date: Tue, 26 Aug 2025 14:32:06 +0100 Subject: [PATCH 18/19] Added grpc calls latency metric --- jdbc/ydb-token-app/pom.xml | 11 +++++++++++ .../src/main/java/tech/ydb/apps/Application.java | 1 + .../src/main/java/tech/ydb/apps/GrpcMetrics.java | 9 +++++++++ 3 files changed, 21 insertions(+) diff --git a/jdbc/ydb-token-app/pom.xml b/jdbc/ydb-token-app/pom.xml index 4591b1d..80ab123 100644 --- a/jdbc/ydb-token-app/pom.xml +++ b/jdbc/ydb-token-app/pom.xml @@ -72,6 +72,17 @@ spring-boot-maven-plugin ${spring.boot.version} + + org.apache.maven.plugins + maven-compiler-plugin + 3.12.1 + + 17 + + -Xlint + + + diff --git a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java index 44e1fa3..c39d85b 100644 --- a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java +++ b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java @@ -67,6 +67,7 @@ public Application(Config config, SchemeService scheme, WorkloadService worload, this.workloadService = worload; this.ticker = new AppMetrics(logger, registry); + logger.info("Create fixed thread pool with size {}", config.getThreadCount()); this.executor = Executors.newFixedThreadPool(config.getThreadCount(), this::threadFactory); } diff --git a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/GrpcMetrics.java b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/GrpcMetrics.java index 15df6e6..3435634 100644 --- a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/GrpcMetrics.java +++ b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/GrpcMetrics.java @@ -1,5 +1,6 @@ package tech.ydb.apps; +import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import javax.annotation.Nullable; @@ -15,6 +16,7 @@ import io.grpc.Status; import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; /** * @@ -23,6 +25,8 @@ public class GrpcMetrics implements Consumer>, ClientInterceptor { private static final Counter.Builder REQUEST = Counter.builder("grpc.request"); private static final Counter.Builder RESPONSE = Counter.builder("grpc.response"); + private static final Timer.Builder LATENCY = Timer.builder("grpc.latency") + .publishPercentiles(0.5, 0.9, 0.95, 0.99); private static MeterRegistry REGISTRY = null; @@ -101,9 +105,11 @@ public void sendMessage(ReqT message) { private class ProxyListener extends Listener { private final Listener delegate; + private final long startedAt; public ProxyListener(Listener delegate) { this.delegate = delegate; + this.startedAt = System.currentTimeMillis(); } @@ -119,8 +125,11 @@ public void onMessage(RespT message) { @Override public void onClose(Status status, Metadata trailers) { + long ms = System.currentTimeMillis() - startedAt; RESPONSE.tag("method", method).tag("authority", authority).tag("status", status.getCode().toString()) .register(registry).increment(); + LATENCY.tag("method", method).tag("authority", authority) + .register(registry).record(ms, TimeUnit.MILLISECONDS); delegate.onClose(status, trailers); } From 678df59d2f18ad78c3ad129f63d262a7f410d0a3 Mon Sep 17 00:00:00 2001 From: Alexandr Gorshenin Date: Thu, 28 Aug 2025 11:06:23 +0100 Subject: [PATCH 19/19] Added warn on errors --- jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java index c39d85b..a6f1373 100644 --- a/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java +++ b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Application.java @@ -199,7 +199,7 @@ private void workload(RateLimiter rt, long finishAt) { } } catch (RuntimeException ex) { ticker.incrementFaiture(); - logger.debug("got exception {}", ex.getMessage()); + logger.warn("got exception {}", ex.getMessage()); } } }