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/README.md b/jdbc/ydb-token-app/README.md
new file mode 100644
index 0000000..d95f6bf
--- /dev/null
+++ b/jdbc/ydb-token-app/README.md
@@ -0,0 +1,76 @@
+## 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
+* validate - validate current data stored in database
+
+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.
diff --git a/jdbc/ydb-token-app/pom.xml b/jdbc/ydb-token-app/pom.xml
index 931441f..80ab123 100644
--- a/jdbc/ydb-token-app/pom.xml
+++ b/jdbc/ydb-token-app/pom.xml
@@ -26,23 +26,38 @@
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.7
jakarta.xml.bind
jakarta.xml.bind-api
- 2.3.2
tech.ydb.jdbc
ydb-jdbc-driver
+
tech.ydb.dialects
hibernate-ydb-dialect-v5
@@ -57,6 +72,35 @@
spring-boot-maven-plugin
${spring.boot.version}
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.12.1
+
+ 17
+
+ -Xlint
+
+
+
+
+
+
+
+ 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/AppMetrics.java b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/AppMetrics.java
new file mode 100644
index 0000000..43282e7
--- /dev/null
+++ b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/AppMetrics.java
@@ -0,0 +1,265 @@
+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")
+// .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;
+
+ 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("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("operation_type", label)
+ .tag("operation_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)
+ );
+ }
+
+ 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(Duration.ofMillis(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());
+ }
+
+ 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 failturesCount = 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 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);
+ 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() {
+ 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() {
+ 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 c594153..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
@@ -1,32 +1,33 @@
package tech.ydb.apps;
-import java.sql.SQLException;
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;
import javax.annotation.PreDestroy;
+import io.micrometer.core.instrument.MeterRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;
import org.springframework.retry.RetryListener;
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;
/**
@@ -34,39 +35,45 @@
* @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 = 60; // 60 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 AppMetrics ticker;
+ 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 AtomicInteger executionsCount = new AtomicInteger(0);
- private final AtomicInteger retriesCount = new AtomicInteger(0);
+ private final AtomicLong logCounter = new AtomicLong(0);
+ private volatile boolean isStopped = false;
+
+ public Application(Config config, SchemeService scheme, WorkloadService worload, MeterRegistry registry) {
+ GrpcMetrics.init(registry);
- public Application(SchemeService schemeService, TokenService tokenService) {
- this.schemeService = schemeService;
- this.tokenService = tokenService;
+ this.config = config;
+ this.schemeService = scheme;
+ this.workloadService = worload;
+ this.ticker = new AppMetrics(logger, registry);
- this.executor = Executors.newFixedThreadPool(THREADS_COUNT, this::threadFactory);
+ logger.info("Create fixed thread pool with size {}", config.getThreadCount());
+ this.executor = Executors.newFixedThreadPool(config.getThreadCount(), this::threadFactory);
}
@PreDestroy
public void close() throws Exception {
+ isStopped = true;
logger.info("CLI app is waiting for finishing");
executor.shutdown();
@@ -75,43 +82,23 @@ public void close() throws Exception {
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();
- return true;
- }
-
- @Override
- public void onError(RetryContext ctx, RetryCallback callback, Throwable th) {
- logger.debug("Retry operation with error {} ", printSqlException(th));
- retriesCount.incrementAndGet();
- }
- };
- }
-
- 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
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)) {
@@ -130,6 +117,10 @@ public void run(String... args) {
ticker.runWithMonitor(this::runWorkloads);
}
+ if ("validate".equalsIgnoreCase(arg)) {
+ executeValidate();
+ }
+
if ("test".equalsIgnoreCase(arg)) {
ticker.runWithMonitor(this::test);
}
@@ -141,19 +132,25 @@ 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()) {
- tokenService.insertBatch(first, last);
- logger.debug("inserted tokens [{}, {})", first, last);
- measure.inc();
+ if (isStopped) {
+ return;
}
+
+ ticker.getLoad().measure(() -> {
+ workloadService.loadData(first, last);
+ logger.debug("inserted tokens [{}, {})", first, last);
+ });
}, executor));
}
@@ -163,55 +160,74 @@ 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))
- .collect(Collectors.toList());
+ Set randomIds = IntStream.range(0, 100)
+ .mapToObj(idx -> rnd.nextInt(recordsCount))
+ .collect(Collectors.toSet());
- tokenService.updateBatch(randomIds);
+ workloadService.updateBatch(randomIds, 0);
}
private void runWorkloads() {
- long finishAt = System.currentTimeMillis() + WORKLOAD_DURATION_SECS * 1000;
+ RateLimiter rt = config.getRpsLimiter();
+ logCounter.set(workloadService.readLastGlobalVersion());
+ long finishAt = System.currentTimeMillis() + config.getWorkloadDurationSec() * 1000;
List> futures = new ArrayList<>();
- for (int i = 0; i < THREADS_COUNT; i++) {
- futures.add(CompletableFuture.runAsync(() -> this.workload(finishAt), executor));
+ for (int i = 0; i < config.getThreadCount(); i++) {
+ 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();
- while (System.currentTimeMillis() < finishAt) {
+ final int recordCount = config.getRecordsCount();
+
+ while ((System.currentTimeMillis() < finishAt) && !isStopped) {
+ rt.acquire();
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());
+ ticker.incrementFaiture();
+ logger.warn("got exception {}", ex.getMessage());
}
}
}
+
+ private void executeFetch(Random rnd, int recordCount) {
+ int id = rnd.nextInt(recordCount);
+ ticker.getFetch().measure(() -> workloadService.fetchToken(id));
+ }
+
+ private void executeUpdate(Random rnd, int recordCount) {
+ int id = rnd.nextInt(recordCount);
+ ticker.getUpdate().measure(() -> workloadService.updateToken(id, logCounter.incrementAndGet()));
+ }
+
+ private void executeBatchUpdate(Random rnd, int recordCount) {
+ ticker.getBatchUpdate().measure(() -> {
+ Set randomIds = IntStream.range(0, 100)
+ .mapToObj(idx -> rnd.nextInt(recordCount))
+ .collect(Collectors.toSet());
+ long counter = logCounter.getAndAdd(randomIds.size()) + 1;
+ 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/Config.java b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Config.java
new file mode 100644
index 0000000..c3f5aa3
--- /dev/null
+++ b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/Config.java
@@ -0,0 +1,57 @@
+package tech.ydb.apps;
+
+
+import io.micrometer.core.instrument.MeterRegistry;
+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;
+ private final int rpsLimit;
+
+ public Config(String connection, int threadsCount, int recordsCount, @Name("load.batchSize") int loadBatchSize,
+ @Name("workload.duration") int workloadDuration, int rpsLimit, MeterRegistry registy) {
+ 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() {
+ 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;
+ }
+
+ public RateLimiter getRpsLimiter() {
+ return rpsLimit <= 0 ? RateLimiter.noLimit() : RateLimiter.withRps(rpsLimit);
+ }
+}
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..3435634
--- /dev/null
+++ b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/GrpcMetrics.java
@@ -0,0 +1,142 @@
+package tech.ydb.apps;
+
+import java.util.concurrent.TimeUnit;
+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;
+import io.micrometer.core.instrument.Timer;
+
+/**
+ *
+ * @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 final Timer.Builder LATENCY = Timer.builder("grpc.latency")
+ .publishPercentiles(0.5, 0.9, 0.95, 0.99);
+
+ 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;
+ private final long startedAt;
+
+ public ProxyListener(Listener delegate) {
+ this.delegate = delegate;
+ this.startedAt = System.currentTimeMillis();
+ }
+
+
+ @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) {
+ 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);
+ }
+
+ @Override
+ public void onReady() {
+ delegate.onReady();
+ }
+ }
+ }
+}
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/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/java/tech/ydb/apps/annotation/YdbRetryable.java b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/annotation/YdbRetryable.java
index 4225352..c958bcd 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,8 +18,8 @@
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Retryable(
- retryFor = SQLRecoverableException.class,
- maxAttempts = 5,
+ retryFor = { SQLRecoverableException.class, SQLTransientException.class },
+ maxAttempts = 15,
backoff = @Backoff(delay = 100, multiplier = 2.0, maxDelay = 5000, random = true)
)
public @interface YdbRetryable {
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/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/TokenService.java b/jdbc/ydb-token-app/src/main/java/tech/ydb/apps/service/WorkloadService.java
similarity index 51%
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..324d92d 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,9 +3,12 @@
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;
+import javax.persistence.EntityNotFoundException;
+
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@@ -13,6 +16,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 +25,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,22 +41,22 @@ 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);
- return null;
+ throw new EntityNotFoundException("token " + id + " is not found");
}
return token.get();
@@ -57,45 +64,55 @@ 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);
+ throw new EntityNotFoundException("token " + id + " is not found");
}
}
@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);
+ }
+
+ @YdbRetryable
+ public long countTokenLogs() {
+ return tokenLogRepo.count();
+ }
+
+ @YdbRetryable
+ public long countAllTokenUpdates() {
+ return tokenRepo.countAllUpdates().orElse(0l);
}
}
diff --git a/jdbc/ydb-token-app/src/main/resources/application.properties b/jdbc/ydb-token-app/src/main/resources/application.properties
index 0a88d58..1cc894b 100644
--- a/jdbc/ydb-token-app/src/main/resources/application.properties
+++ b/jdbc/ydb-token-app/src/main/resources/application.properties
@@ -1,9 +1,19 @@
-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
+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
-spring.datasource.hikari.maximum-pool-size=100
-spring.datasource.hikari.data-source-properties.useQueryService=true
+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
@@ -11,9 +21,32 @@ 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
+management.metrics.enable.all=false
+management.metrics.enable.jdbc=true
+management.metrics.enable.sdk=true
+management.metrics.enable.grpc=true
-#logging.level.tech.ydb.apps=TRACE
-#logging.level.tech.ydb.jdbc.YdbDriver=TRACE
+# 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=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
+#logging.level.org.hibernate.engine=OFF
+
+logging.level.com.zaxxer=OFF
+logging.level.org.hibernate=OFF
#logging.level.org.hibernate.SQL=DEBUG
-#logging.level.org.hibernate.type=TRACE
\ No newline at end of file
+#logging.level.org.hibernate.type=TRACE
+
+logging.level.org.springframework=OFF
+
+#logging.level.tech.ydb.jdbc.YdbDriver=TRACE
+#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..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 app_token;
+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..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,4 +3,17 @@ CREATE TABLE app_token (
username Text,
version Int32,
PRIMARY KEY (id)
+) WITH (
+ AUTO_PARTITIONING_BY_LOAD=ENABLED
+);
+
+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)
+) WITH (
+ AUTO_PARTITIONING_BY_LOAD=ENABLED
);
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