Compare commits

..

3 Commits

Author SHA1 Message Date
Savosin Denis
e6db3360c2 add entrypoint to dockerfile and doc
fix resource usage
2025-11-06 15:47:56 +07:00
Savosin Denis
11847af074 refactor libs.versions.toml 2025-11-06 14:12:44 +07:00
Savosin Denis
9a98763261 bump java version up to 20 2025-11-06 14:12:44 +07:00
20 changed files with 536 additions and 134 deletions

View File

@@ -1,2 +1,3 @@
* *
!build/libs/ !build/libs/
!entrypoint.sh

View File

@@ -9,4 +9,16 @@ DB_PASSWORD=postgres
KAFKA_SERVERS=localhost:9095 KAFKA_SERVERS=localhost:9095
OTLP_TRACING_HTTP_URL=http://localhost:4318/v1/traces OTLP_TRACING_HTTP_URL=http://localhost:4318/v1/traces
# jvm tuning
TOMCAT_THREADS_MAX=30
#TOMCAT_THREADS_MIN=
HIKARI_DB_MAXIMUM_POOL_SIZE=4
#HIKARI_DB_MINIMUM_IDLE_SIZE
#JVM_NATIVE_MB=120
#RESERVED_CODE_CACHE_SIZE_MB=64
MAX_METASPACE_SIZE_MB=100
#DIRECT_BYTES_BUFFERS_MB=10
#COMPRESSED_CLASS_SPACE_MB=16
#OVERHEAD_GC_SIZE_PERCENT=5

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
.gradle .gradle
build/ build/
gradle.properties
!gradle/wrapper/gradle-wrapper.jar !gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/ !**/src/main/**/build/
!**/src/test/**/build/ !**/src/test/**/build/

View File

@@ -1,3 +1,9 @@
FROM openjdk:17-jdk-slim FROM eclipse-temurin:20-jdk
COPY --chmod=777 build/libs/demo-single-version.jar /application/demo.jar
CMD ["java", "-jar", "/application/demo.jar"] WORKDIR /app
COPY ./entrypoint.sh .
RUN chmod +x ./entrypoint.sh
COPY ./build/libs/*.jar .
ENTRYPOINT ["/app/entrypoint.sh"]

View File

@@ -1,12 +1,14 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins { plugins {
idea idea
alias(libs.plugins.kotlin.kover) alias(libs.plugins.io.spring.dependency.management)
alias(libs.plugins.kotlin.jpa) alias(libs.plugins.org.jetbrains.kotlin.jvm)
alias(libs.plugins.kotlin.jvm) alias(libs.plugins.org.jetbrains.kotlin.plugin.jpa)
alias(libs.plugins.kotlin.serialization) alias(libs.plugins.org.jetbrains.kotlin.plugin.serialization)
alias(libs.plugins.kotlin.spring) alias(libs.plugins.org.jetbrains.kotlin.plugin.spring)
alias(libs.plugins.spring.boot) alias(libs.plugins.org.jetbrains.kotlinx.kover)
alias(libs.plugins.spring.dependencyManagement) alias(libs.plugins.org.springframework.boot)
} }
group = "com.github.dannecron.demo" group = "com.github.dannecron.demo"
@@ -14,26 +16,28 @@ version = "single-version"
allprojects { allprojects {
apply { apply {
plugin(rootProject.libs.plugins.kotlin.jvm.get().pluginId) plugin(rootProject.libs.plugins.org.jetbrains.kotlin.jvm.get().pluginId)
plugin(rootProject.libs.plugins.kotlin.serialization.get().pluginId) plugin(rootProject.libs.plugins.org.jetbrains.kotlin.plugin.serialization.get().pluginId)
plugin(rootProject.libs.plugins.kotlin.kover.get().pluginId) plugin(rootProject.libs.plugins.org.jetbrains.kotlinx.kover.get().pluginId)
plugin(rootProject.libs.plugins.spring.boot.get().pluginId) plugin(rootProject.libs.plugins.org.springframework.boot.get().pluginId)
plugin("java") plugin("java")
} }
plugins.withId("org.jetbrains.kotlinx.kover") { plugins.withId(rootProject.libs.plugins.org.jetbrains.kotlinx.kover.get().pluginId) {
tasks.named("koverXmlReport") { tasks.named("koverXmlReport") {
dependsOn(tasks.test) dependsOn(tasks.test)
} }
} }
java { java {
sourceCompatibility = JavaVersion.VERSION_17 sourceCompatibility = JavaVersion.VERSION_20
targetCompatibility = JavaVersion.VERSION_20
} }
kotlin { kotlin {
compilerOptions { compilerOptions {
jvmTarget.set(JvmTarget.JVM_20)
freeCompilerArgs.addAll("-Xjsr305=strict") freeCompilerArgs.addAll("-Xjsr305=strict")
apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_1) apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_1)
} }
@@ -44,17 +48,17 @@ allprojects {
} }
dependencies { dependencies {
runtimeOnly(rootProject.libs.micrometer.registry.prometheus) runtimeOnly(rootProject.libs.io.micrometer.micrometer.registry.prometheus)
implementation(rootProject.libs.bundles.tracing) implementation(rootProject.libs.bundles.tracing)
implementation(rootProject.libs.kotlin.reflect) implementation(rootProject.libs.net.logstash.logback.logstash.logback.encoder)
implementation(rootProject.libs.kotlinx.serialization.json) implementation(rootProject.libs.org.jetbrains.kotlin.kotlin.reflect)
implementation(rootProject.libs.logback.encoder) implementation(rootProject.libs.org.jetbrains.kotlinx.kotlinx.serialization.json)
implementation(rootProject.libs.springFramework.aspects) implementation(rootProject.libs.org.springframework.spring.aspects)
testImplementation(rootProject.libs.kotlin.test.junit) testImplementation(rootProject.libs.org.jetbrains.kotlin.kotlin.test.junit5)
testImplementation(rootProject.libs.mockito.kotlin) testImplementation(rootProject.libs.org.mockito.kotlin.mockito.kotlin)
testImplementation(rootProject.libs.springBoot.starter.test) testImplementation(rootProject.libs.org.springframework.boot.spring.boot.starter.test)
} }
tasks.test { tasks.test {
@@ -67,9 +71,9 @@ allprojects {
subprojects { subprojects {
apply { apply {
plugin(rootProject.libs.plugins.kotlin.spring.get().pluginId) plugin(rootProject.libs.plugins.io.spring.dependency.management.get().pluginId)
plugin(rootProject.libs.plugins.spring.boot.get().pluginId) plugin(rootProject.libs.plugins.org.jetbrains.kotlin.plugin.spring.get().pluginId)
plugin(rootProject.libs.plugins.spring.dependencyManagement.get().pluginId) plugin(rootProject.libs.plugins.org.springframework.boot.get().pluginId)
} }
tasks.bootJar { tasks.bootJar {
@@ -86,12 +90,12 @@ dependencies {
implementation(project(":edge-consuming")) implementation(project(":edge-consuming"))
implementation(project(":edge-rest")) implementation(project(":edge-rest"))
implementation(libs.springBoot.starter.mustache) implementation(libs.org.springframework.boot.spring.boot.starter.mustache)
implementation(libs.springBoot.starter.web) implementation(libs.org.springframework.boot.spring.boot.starter.web)
testImplementation(libs.archUnit.junit) testImplementation(libs.com.tngtech.archunit.archunit.junit5)
developmentOnly(libs.springBoot.devtools) developmentOnly(libs.org.springframework.boot.spring.boot.devtools)
kover(project(":edge-contracts")) kover(project(":edge-contracts"))
kover(project(":db")) kover(project(":db"))

View File

@@ -3,8 +3,8 @@ dependencies {
implementation(project(":edge-producing")) implementation(project(":edge-producing"))
implementation(project(":edge-integration")) implementation(project(":edge-integration"))
implementation(rootProject.libs.springBoot.starter.actuator) implementation(rootProject.libs.org.springframework.boot.spring.boot.starter.actuator)
implementation(rootProject.libs.springData.commons) implementation(rootProject.libs.org.springframework.data.spring.data.commons)
testImplementation(rootProject.libs.springBoot.starter.actuatorAutoconfigure) testImplementation(rootProject.libs.org.springframework.boot.spring.boot.starter.actuator.autoconfigure)
} }

View File

@@ -1,13 +1,13 @@
plugins { plugins {
alias(libs.plugins.kotlin.jpa) alias(libs.plugins.org.jetbrains.kotlin.plugin.jpa)
} }
dependencies { dependencies {
implementation(rootProject.libs.flyway.core) implementation(rootProject.libs.org.flywaydb.flyway.core)
implementation(rootProject.libs.postgres) implementation(rootProject.libs.org.postgresql.postgresql)
implementation(rootProject.libs.springBoot.starter.jdbc) implementation(rootProject.libs.org.springframework.boot.spring.boot.starter.data.jdbc)
testImplementation(libs.testcontainers) testImplementation(libs.org.testcontainers.junit.jupiter)
testImplementation(libs.testcontainers.junit.jupiter) testImplementation(libs.org.testcontainers.postgresql)
testImplementation(libs.testcontainers.postgresql) testImplementation(libs.org.testcontainers.testcontainers)
} }

278
doc/entrypoint.md Normal file
View File

@@ -0,0 +1,278 @@
# Entrypoint: Запуск JAR-файла и конфигурация JVM
_LLM-generated документация._
## Оглавление
- [Обзор](#обзор)
- [Параметры запуска](#параметры-запуска)
- [Алгоритм работы](#алгоритм-работы)
- [1. Определение ограничений памяти контейнера](#1-определение-ограничений-памяти-контейнера)
- [2. Конфигурация компонентов приложения](#2-конфигурация-компонентов-приложения)
- [Переменные окружения со значениями по умолчанию:](#переменные-окружения-со-значениями-по-умолчанию)
- [3. Расчет "складских" ресурсов](#3-расчет-складских-ресурсов)
- [4. Расчет доступной памяти для JVM](#4-расчет-доступной-памяти-для-jvm)
- [5. Настройка Non-Heap областей JVM](#5-настройка-non-heap-областей-jvm)
- [Конфигурация областей памяти:](#конфигурация-областей-памяти)
- [6. Расчет Heap памяти](#6-расчет-heap-памяти)
- [Этап 1: Память доступная для Heap + GC](#этап-1-память-доступная-для-heap--gc)
- [Этап 2: Резерв для GC overhead](#этап-2-резерв-для-gc-overhead)
- [Этап 3: Финальный размер Heap](#этап-3-финальный-размер-heap)
- [Этап 4: Процент от MaxRAM для JVM](#этап-4-процент-от-maxram-для-jvm)
- [JVM флаги и их назначение](#jvm-флаги-и-их-назначение)
- [Контейнерная поддержка](#контейнерная-поддержка)
- [Управление памятью](#управление-памятью)
- [Оптимизация производительности](#оптимизация-производительности)
- [Дополнительные опции](#дополнительные-опции)
- [Экспортируемые переменные](#экспортируемые-переменные)
- [Пример расчета](#пример-расчета)
- [Дано:](#дано)
- [Расчет:](#расчет)
- [Результирующая команда Java:](#результирующая-команда-java)
- [Мониторинг и отладка](#мониторинг-и-отладка)
- [Verbose режим (`-v`)](#verbose-режим--v)
- [Рекомендуемые размеры контейнеров](#рекомендуемые-размеры-контейнеров)
- [Рекомендации по настройке:](#рекомендации-по-настройке)
- [Troubleshooting](#troubleshooting)
- [Проблема: Приложение падает с OOM](#проблема-приложение-падает-с-oom)
- [Проблема: Медленная работа GC](#проблема-медленная-работа-gc)
- [Проблема: Много database connections timeout](#проблема-много-database-connections-timeout)
## Обзор
Скрипт `entrypoint.sh` выполняет интеллектуальную настройку JVM на основе ограничений памяти контейнера. Он автоматически вычисляет оптимальные размеры heap, non-heap областей и других параметров для обеспечения стабильной работы Spring Boot приложения.
### Параметры запуска
- **`-v`** - включает verbose режим с выводом отладочной информации о расчетах памяти
- **`JAR_FILE_NAME`** - имя JAR-файла для запуска (позиционный аргумент)
**Пример использования:**
```bash
./entrypoint.sh -v app.jar # С отладочной информацией
./entrypoint.sh app.jar # Без отладочной информации
```
## Алгоритм работы
### 1. Определение ограничений памяти контейнера
```bash
# Проверка версии cgroups и получение лимита памяти
if [ -f /sys/fs/cgroup/memory/memory.limit_in_bytes ]; then
# Cgroups v1
LIMITED_RAM_CONTAINER_CGROUP_SIZE_BYTES=$(cat /sys/fs/cgroup/memory/memory.limit_in_bytes)
elif [ -f /sys/fs/cgroup/memory.max ]; then
# Cgroups v2
LIMITED_RAM_CONTAINER_CGROUP_SIZE_BYTES=$(cat /sys/fs/cgroup/memory.max)
else
# Ошибка: нет доступа к информации о памяти
exit 1
fi
```
**Результат:** Получение лимита памяти контейнера в байтах.
### 2. Конфигурация компонентов приложения
#### Переменные окружения со значениями по умолчанию:
| Переменная | Значение по умолчанию | Описание |
|-------------------------------|-------------------------------|----------------------------------------|
| `TOMCAT_THREADS_MAX` | 100 | Максимальное количество потоков Tomcat |
| `TOMCAT_THREADS_MIN` | = TOMCAT_THREADS_MAX | Минимальное количество потоков Tomcat |
| `HIKARI_DB_MAXIMUM_POOL_SIZE` | 10 | Максимальный размер пула соединений БД |
| `HIKARI_DB_MINIMUM_IDLE_SIZE` | = HIKARI_DB_MAXIMUM_POOL_SIZE | Минимальное количество idle соединений |
| `JVM_NATIVE_MB` | 120 | Резерв памяти для native кода JVM |
### 3. Расчет "складских" ресурсов
```bash
STOCK_SIZE_MB = (TOMCAT_THREADS_MAX / 2) + HIKARI_DB_MAXIMUM_POOL_SIZE + JVM_NATIVE_MB
```
**Логика расчета:**
- **Tomcat потоки:** каждый поток потребляет ~0.5 MB памяти
- **Пул соединений БД:** каждое соединение ~1 MB
- **Native память JVM:** фиксированный резерв для JNI, сжатия, etc.
**Пример:** При дефолтных значениях: `(100/2) + 10 + 120 = 180 MB`
### 4. Расчет доступной памяти для JVM
```bash
LIMITED_MAXRAM_JAVA_SIZE_MB = LIMITED_RAM_CONTAINER_CGROUP_SIZE_MB - STOCK_SIZE_MB
```
**Назначение:** Память, которую JVM может использовать без риска превышения лимитов контейнера.
### 5. Настройка Non-Heap областей JVM
#### Конфигурация областей памяти:
| Область | Переменная | Значение по умолчанию | Назначение |
|----------------------------|-------------------------------|-----------------------|----------------------------|
| **Code Cache** | `RESERVED_CODE_CACHE_SIZE_MB` | 64 MB | Компилированный JIT код |
| **Metaspace** | `MAX_METASPACE_SIZE_MB` | 80 MB | Метаданные классов |
| **Direct Buffers** | `DIRECT_BYTES_BUFFERS_MB` | 10 MB | NIO буферы |
| **Compressed Class Space** | `COMPRESSED_CLASS_SPACE_MB` | 16 MB | Сжатые указатели на классы |
```bash
NON_HEAP_SIZE_MB = RESERVED_CODE_CACHE_SIZE_MB + MAX_METASPACE_SIZE_MB +
DIRECT_BYTES_BUFFERS_MB + COMPRESSED_CLASS_SPACE_MB
```
### 6. Расчет Heap памяти
#### Этап 1: Память доступная для Heap + GC
```bash
HEAP_GC_SIZE_MB = LIMITED_MAXRAM_JAVA_SIZE_MB - NON_HEAP_SIZE_MB
```
#### Этап 2: Резерв для GC overhead
```bash
OVERHEAD_GC_SIZE_PERCENT = 5 # По умолчанию 5%
OVERHEAD_GC_SIZE_MB = (HEAP_GC_SIZE_MB * OVERHEAD_GC_SIZE_PERCENT) / 100
```
#### Этап 3: Финальный размер Heap
```bash
HEAP_SIZE_MB = HEAP_GC_SIZE_MB - OVERHEAD_GC_SIZE_MB
```
#### Этап 4: Процент от MaxRAM для JVM
```bash
MAX_RAM_PERCENTAGE = (HEAP_SIZE_MB * 100) / LIMITED_MAXRAM_JAVA_SIZE_MB
```
## JVM флаги и их назначение
### Контейнерная поддержка
- **`-XX:+UseContainerSupport`** - Включает автоопределение ресурсов контейнера
- **`-XX:MaxRAM="${LIMITED_MAXRAM_JAVA_SIZE_MB}m"`** - Максимальная память для JVM
- **`-XX:MaxRAMPercentage="$MAX_RAM_PERCENTAGE"`** - Процент MaxRAM для heap
### Управление памятью
- **`-XX:+ExitOnOutOfMemoryError`** - Принудительное завершение при OOM
- **`-XX:MaxMetaspaceSize="${MAX_METASPACE_SIZE_MB}m"`** - Лимит Metaspace
### Оптимизация производительности
- **`-XX:+SegmentedCodeCache`** - Сегментированный code cache для лучшей производительности
- **`-XX:ReservedCodeCacheSize="${RESERVED_CODE_CACHE_SIZE_MB}m"`** - Размер code cache
### Дополнительные опции
- **`$JAVA_OPTS_OVERRIDE`** - Переменная для переопределения опций
## Экспортируемые переменные
Скрипт экспортирует рассчитанные значения для использования приложением:
```bash
export TOMCAT_THREADS_MAX=$TOMCAT_THREADS_MAX
export TOMCAT_THREADS_MIN=$TOMCAT_THREADS_MIN
export HIKARI_DB_MAXIMUM_POOL_SIZE=$HIKARI_DB_MAXIMUM_POOL_SIZE
export HIKARI_DB_MINIMUM_IDLE_SIZE=$HIKARI_DB_MINIMUM_IDLE_SIZE
```
## Пример расчета
### Дано:
- Лимит контейнера: **512 MB**
- Дефолтные значения всех переменных
### Расчет:
1. **Складские ресурсы:**
```
STOCK_SIZE_MB = (100/2) + 10 + 120 = 180 MB
```
2. **Доступная память для JVM:**
```
LIMITED_MAXRAM_JAVA_SIZE_MB = 512 - 180 = 332 MB
```
3. **Non-heap память:**
```
NON_HEAP_SIZE_MB = 64 + 80 + 10 + 16 = 170 MB
```
4. **Память для Heap + GC:**
```
HEAP_GC_SIZE_MB = 332 - 170 = 162 MB
```
5. **GC overhead:**
```
OVERHEAD_GC_SIZE_MB = (162 * 5) / 100 = 8 MB
```
6. **Финальный размер Heap:**
```
HEAP_SIZE_MB = 162 - 8 = 154 MB
```
7. **Процент RAM для heap:**
```
MAX_RAM_PERCENTAGE = (154 * 100) / 332 = 46%
```
### Результирующая команда Java:
```bash
exec java \
-XX:+UseContainerSupport \
-XX:+ExitOnOutOfMemoryError \
-XX:MaxRAM="332m" \
-XX:MaxRAMPercentage="46" \
-XX:+SegmentedCodeCache \
-XX:ReservedCodeCacheSize="64m" \
-XX:MaxMetaspaceSize="80m" \
-jar /app/app.jar
```
## Мониторинг и отладка
### Verbose режим (`-v`)
При запуске с флагом `-v` скрипт выводит отладочную информацию:
```bash
# Версия cgroups
Cgroups v2 are used
# Расчеты памяти
LIMITED_MAXRAM_JAVA_SIZE_MB=332
HEAP_SIZE_MB=154
MAX_RAM_PERCENTAGE=46
```
**Без флага `-v`** - никаких отладочных сообщений не выводится, только стандартные логи приложения.
### Рекомендуемые размеры контейнеров
| Размер контейнера | Heap | Статус |
|-------------------|-----------------|---------------------|
| **≤ 352 MB** | ❌ Отрицательный | Не работает |
| **353 MB** | 🔴 3 MB | Критический минимум |
| **400 MB** | 🟡 47 MB | Базовый |
| **512 MB** | 🟢 154 MB | Рекомендуемый |
| **768 MB** | 🟢 380 MB | Комфортный |
### Рекомендации по настройке:
1. **Для высоко нагруженных приложений** - увеличьте `TOMCAT_THREADS_MAX`
2. **Для БД-интенсивных приложений** - увеличьте `HIKARI_DB_MAXIMUM_POOL_SIZE`
3. **Для приложений с большим количеством классов** - увеличьте `MAX_METASPACE_SIZE_MB`
4. **При частых OOM** - уменьшите `OVERHEAD_GC_SIZE_PERCENT` или увеличьте память контейнера
## Troubleshooting
### Проблема: Приложение падает с OOM
**Решение:** Проверьте соотношение heap/non-heap памяти, возможно нужно увеличить лимит контейнера.
### Проблема: Медленная работа GC
**Решение:** Увеличьте `OVERHEAD_GC_SIZE_PERCENT` или используйте альтернативный GC через `JAVA_OPTS_OVERRIDE`.
### Проблема: Много database connections timeout
**Решение:** Увеличьте `HIKARI_DB_MAXIMUM_POOL_SIZE` и соответственно лимит памяти контейнера.

View File

@@ -5,16 +5,16 @@ services:
build: build:
dockerfile: Dockerfile dockerfile: Dockerfile
context: . context: .
environment: env_file:
SPRING_LOG_LEVEL: $SPRING_LOG_LEVEL - .env
SPRING_ACTIVE_PROFILE: $SPRING_ACTIVE_PROFILE
DB_URL: $DB_URL
DB_NAME: $DB_NAME
DB_SCHEMA: $DB_SCHEMA
DB_USERNAME: $DB_USERNAME
DB_PASSWORD: $DB_PASSWORD
KAFKA_SERVERS: $KAFKA_SERVERS
OTLP_TRACING_HTTP_URL: $OTLP_TRACING_HTTP_URL
expose: expose:
- 8080 - 8080
- 8081 - 8081
command:
- app.jar
healthcheck:
test: [ "CMD", "curl", "--fail", "--silent", "http://localhost:8081/health" ]
interval: 30s
timeout: 10s
retries: 5
start_period: 10s

View File

@@ -2,10 +2,10 @@ dependencies {
implementation(project(":edge-contracts")) implementation(project(":edge-contracts"))
implementation(project(":core")) implementation(project(":core"))
implementation(rootProject.libs.jackson.datatype.jsr) implementation(rootProject.libs.com.fasterxml.jackson.datatype.jackson.datatype.jsr310)
implementation(rootProject.libs.jackson.module.kotlin) implementation(rootProject.libs.com.fasterxml.jackson.module.jackson.module.kotlin)
implementation(rootProject.libs.springCloud.starter.streamKafka) implementation(rootProject.libs.org.springframework.cloud.spring.cloud.starter.stream.kafka)
implementation(rootProject.libs.springCloud.stream) implementation(rootProject.libs.org.springframework.cloud.spring.cloud.stream)
testImplementation(rootProject.libs.springCloud.streamTestBinder) testImplementation(rootProject.libs.org.springframework.cloud.spring.cloud.stream.test.binder)
} }

View File

@@ -1,8 +1,7 @@
dependencies { dependencies {
implementation(rootProject.libs.json.schema.validator) implementation(rootProject.libs.io.github.optimumcode.json.schema.validator)
implementation(rootProject.libs.springBoot.starter.web) implementation(rootProject.libs.org.springdoc.springdoc.openapi.starter.webmvc.ui)
implementation(rootProject.libs.springData.commons) implementation(rootProject.libs.org.springframework.boot.spring.boot.starter.web)
implementation(rootProject.libs.springDoc.openapi.starter) implementation(rootProject.libs.org.springframework.cloud.spring.cloud.stream)
implementation(rootProject.libs.springCloud.stream) implementation(rootProject.libs.org.springframework.data.spring.data.commons)
} }

View File

@@ -5,19 +5,21 @@ import com.github.dannecron.demo.edgecontracts.validation.SchemaValidatorImp
import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
import org.springframework.util.ResourceUtils import org.springframework.core.io.ResourceLoader
@Configuration @Configuration
@EnableConfigurationProperties(ValidationProperties::class) @EnableConfigurationProperties(ValidationProperties::class)
class SchemaValidationConfig( class SchemaValidationConfig(
private val validationProperties: ValidationProperties, private val validationProperties: ValidationProperties,
private val resourceLoader: ResourceLoader,
) { ) {
@Bean @Bean
fun schemaValidator(): SchemaValidator = SchemaValidatorImp( fun schemaValidator(): SchemaValidator = SchemaValidatorImp(
schemaMap = validationProperties.schema.mapValues { schemaMap = validationProperties.schema.mapValues {
schema -> ResourceUtils.getFile("classpath:json-schemas/${schema.value}") schema -> resourceLoader.getResource("classpath:json-schemas/${schema.value}")
.readText(Charsets.UTF_8) .takeIf { it.exists() }!!
.getContentAsString(Charsets.UTF_8)
} }
) )
} }

View File

@@ -1,7 +1,7 @@
dependencies { dependencies {
implementation(rootProject.libs.springFramework.context) implementation(rootProject.libs.io.ktor.ktor.client.cio)
implementation(rootProject.libs.ktor.client.cio) implementation(rootProject.libs.io.ktor.ktor.client.core)
implementation(rootProject.libs.ktor.client.core) implementation(rootProject.libs.org.springframework.spring.context)
testImplementation(rootProject.libs.ktor.client.mock) testImplementation(rootProject.libs.io.ktor.ktor.client.mock)
} }

View File

@@ -5,43 +5,57 @@ import com.github.dannecron.demo.edgeintegration.client.neko.dto.ImagesResponse
import com.github.dannecron.demo.edgeintegration.client.neko.exceptions.RequestException import com.github.dannecron.demo.edgeintegration.client.neko.exceptions.RequestException
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.engine.HttpClientEngine import io.ktor.client.engine.HttpClientEngine
import io.ktor.client.plugins.defaultRequest
import io.ktor.client.request.get import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText import io.ktor.client.statement.bodyAsText
import io.ktor.http.path import io.ktor.http.path
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.springframework.stereotype.Service
class ClientImpl( class ClientImpl(
engine: HttpClientEngine, engine: HttpClientEngine,
private val baseUrl: String, private val baseUrl: String,
): Client { ): Client {
private val httpClient = HttpClient(engine) private val httpClient = HttpClient(engine) {
defaultRequest {
url(baseUrl)
}
}
override fun getCategories() = runBlocking { override fun getCategories() = runBlocking {
httpClient.get(urlString = baseUrl) { httpClient.get {
url { url {
path("/api/v2/endpoints") path("/api/v2/endpoints")
} }
} }
.takeIf { it.status.value in 200..209 } .let { response ->
?.let { val responseBody = response.bodyAsText()
response -> Json.decodeFromString<Map<String, CategoryFormat>>(response.bodyAsText()).keys if (response.status.value in 200..209) {
Json.decodeFromString<Map<String, CategoryFormat>>(responseBody).keys
} else {
throw RequestException(
"get categories error. Status: ${response.status.value}, response: $responseBody"
)
}
} }
?: throw RequestException("get categories error")
} }
override fun getImages(category: String, amount: Int) = runBlocking { override fun getImages(category: String, amount: Int) = runBlocking {
httpClient.get(urlString = baseUrl) { httpClient.get {
url { url {
path("/api/v2/$category") path("/api/v2/$category")
parameters.append("amount", amount.toString()) parameters.append("amount", amount.toString())
} }
} }
.takeIf { it.status.value in 200..209 } .let { response ->
?.let { val responseBody = response.bodyAsText()
response -> Json.decodeFromString<ImagesResponse>(response.bodyAsText()) if (response.status.value in 200..209) {
Json.decodeFromString<ImagesResponse>(responseBody)
} else {
throw RequestException(
"get images error. Status: ${response.status.value}, response: $responseBody"
)
}
} }
?: throw RequestException("get images error")
} }
} }

View File

@@ -1,10 +1,10 @@
dependencies { dependencies {
implementation(project(":edge-contracts")) implementation(project(":edge-contracts"))
implementation(rootProject.libs.jackson.datatype.jsr) implementation(rootProject.libs.com.fasterxml.jackson.datatype.jackson.datatype.jsr310)
implementation(rootProject.libs.jackson.module.kotlin) implementation(rootProject.libs.com.fasterxml.jackson.module.jackson.module.kotlin)
implementation(rootProject.libs.springBoot.starter.validation) implementation(rootProject.libs.org.springframework.boot.spring.boot.starter.validation)
implementation(rootProject.libs.springCloud.starter.streamKafka) implementation(rootProject.libs.org.springframework.cloud.spring.cloud.starter.stream.kafka)
testImplementation(rootProject.libs.springCloud.streamTestBinder) testImplementation(rootProject.libs.org.springframework.cloud.spring.cloud.stream.test.binder)
} }

View File

@@ -2,6 +2,6 @@ dependencies {
implementation(project(":edge-contracts")) implementation(project(":edge-contracts"))
implementation(project(":core")) implementation(project(":core"))
implementation(rootProject.libs.springBoot.starter.web) implementation(rootProject.libs.org.springframework.boot.spring.boot.starter.web)
implementation(rootProject.libs.springData.commons) implementation(rootProject.libs.org.springframework.data.spring.data.commons)
} }

77
entrypoint.sh Executable file
View File

@@ -0,0 +1,77 @@
#!/bin/bash
VERBOSE=false
while getopts "v" arg; do
case $arg in
v )
VERBOSE=true
;;
* )
;;
esac
done
# Сдвигаем позиционные параметры после обработки опций
shift $((OPTIND-1))
JAR_FILE_NAME=$1
if [ -f /sys/fs/cgroup/memory/memory.limit_in_bytes ]
then
LIMITED_RAM_CONTAINER_CGROUP_SIZE_BYTES=$(cat /sys/fs/cgroup/memory/memory.limit_in_bytes)
if [ $VERBOSE = true ]; then echo "Cgroups v1 are used"; fi
elif [ -f /sys/fs/cgroup/memory.max ]
then
LIMITED_RAM_CONTAINER_CGROUP_SIZE_BYTES=$(cat /sys/fs/cgroup/memory.max)
if [ $VERBOSE = true ]; then echo "Cgroups v2 are used"; fi
else
echo "No cgroups files with memory limits"; exit 1
fi
#
LIMITED_RAM_CONTAINER_CGROUP_SIZE_MB=$((LIMITED_RAM_CONTAINER_CGROUP_SIZE_BYTES / 1024 / 1024))
TOMCAT_THREADS_MAX=${TOMCAT_THREADS_MAX:-100}
TOMCAT_THREADS_MIN=${TOMCAT_THREADS_MIN-$TOMCAT_THREADS_MAX}
HIKARI_DB_MAXIMUM_POOL_SIZE=${HIKARI_DB_MAXIMUM_POOL_SIZE:-10}
HIKARI_DB_MINIMUM_IDLE_SIZE=${HIKARI_DB_MINIMUM_IDLE_SIZE:-$HIKARI_DB_MAXIMUM_POOL_SIZE}
JVM_NATIVE_MB=${JVM_NATIVE_MB:-120}
STOCK_SIZE_MB=$((TOMCAT_THREADS_MAX / 2 + HIKARI_DB_MAXIMUM_POOL_SIZE + JVM_NATIVE_MB))
LIMITED_MAXRAM_JAVA_SIZE_MB=$((LIMITED_RAM_CONTAINER_CGROUP_SIZE_MB - STOCK_SIZE_MB))
#
RESERVED_CODE_CACHE_SIZE_MB=${RESERVED_CODE_CACHE_SIZE_MB:-64}
MAX_METASPACE_SIZE_MB=${MAX_METASPACE_SIZE_MB:-80}
DIRECT_BYTES_BUFFERS_MB=${DIRECT_BYTES_BUFFERS_MB:-10}
COMPRESSED_CLASS_SPACE_MB=${COMPRESSED_CLASS_SPACE_MB:-16}
NON_HEAP_SIZE_MB=$((RESERVED_CODE_CACHE_SIZE_MB + MAX_METASPACE_SIZE_MB + DIRECT_BYTES_BUFFERS_MB + COMPRESSED_CLASS_SPACE_MB))
#
HEAP_GC_SIZE_MB=$((LIMITED_MAXRAM_JAVA_SIZE_MB - NON_HEAP_SIZE_MB))
OVERHEAD_GC_SIZE_PERCENT=${OVERHEAD_GC_SIZE_PERCENT:-5}
OVERHEAD_GC_SIZE_MB=$((HEAP_GC_SIZE_MB * OVERHEAD_GC_SIZE_PERCENT / 100))
HEAP_SIZE_MB=$((HEAP_GC_SIZE_MB - OVERHEAD_GC_SIZE_MB))
#
MAX_RAM_PERCENTAGE=$((HEAP_SIZE_MB * 100 / LIMITED_MAXRAM_JAVA_SIZE_MB))
# export calculated environments
export TOMCAT_THREADS_MAX=$TOMCAT_THREADS_MAX
export TOMCAT_THREADS_MIN=$TOMCAT_THREADS_MIN
export HIKARI_DB_MAXIMUM_POOL_SIZE=$HIKARI_DB_MAXIMUM_POOL_SIZE
export HIKARI_DB_MINIMUM_IDLE_SIZE=$HIKARI_DB_MINIMUM_IDLE_SIZE
if [ $VERBOSE = true ]
then
echo "LIMITED_MAXRAM_JAVA_SIZE_MB=$LIMITED_MAXRAM_JAVA_SIZE_MB"
echo "HEAP_SIZE_MB=$HEAP_SIZE_MB"
echo "MAX_RAM_PERCENTAGE=$MAX_RAM_PERCENTAGE"
fi
# shellcheck disable=SC2086
exec java \
-XX:+UseContainerSupport \
-XX:+ExitOnOutOfMemoryError \
-XX:MaxRAM="${LIMITED_MAXRAM_JAVA_SIZE_MB}m" \
-XX:MaxRAMPercentage="$MAX_RAM_PERCENTAGE" \
-XX:+SegmentedCodeCache \
-XX:ReservedCodeCacheSize="${RESERVED_CODE_CACHE_SIZE_MB}m" \
-XX:MaxMetaspaceSize="${MAX_METASPACE_SIZE_MB}m" \
$JAVA_OPTS_OVERRIDE \
-jar /app/"$JAR_FILE_NAME"

View File

@@ -7,50 +7,50 @@ spring-cloud = "4.1.5"
testcontainers = "1.19.7" testcontainers = "1.19.7"
[libraries] [libraries]
archUnit-junit = { module = "com.tngtech.archunit:archunit-junit5", version = "1.4.1" } com-fasterxml-jackson-datatype-jackson_datatype_jsr310 = { group = "com.fasterxml.jackson.datatype", name = "jackson-datatype-jsr310", version.ref = "jackson" }
flyway-core = { module = "org.flywaydb:flyway-core", version = "9.22.3" } com-fasterxml-jackson-module-jackson_module_kotlin = { group = "com.fasterxml.jackson.module", name = "jackson-module-kotlin", version.ref = "jackson" }
jackson-datatype-jsr = { module = "com.fasterxml.jackson.datatype:jackson-datatype-jsr310", version.ref = "jackson" } com-tngtech-archunit-archunit_junit5 = { group = "com.tngtech.archunit", name = "archunit-junit5", version = "1.4.1" }
jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson" } io-github-optimumcode-json_schema_validator = { group = "io.github.optimumcode", name = "json-schema-validator", version = "0.5.2" }
json-schema-validator = { module = "io.github.optimumcode:json-schema-validator", version = "0.2.3"} io-ktor-ktor_client_cio = { group = "io.ktor", name = "ktor-client-cio", version.ref = "ktor" }
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } io-ktor-ktor_client_core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" }
kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit5", version.ref = "kotlin" } io-ktor-ktor_client_mock = { group = "io.ktor", name = "ktor-client-mock", version.ref = "ktor" }
kotlinx-serialization-json = "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3" io-micrometer-micrometer_registry_prometheus = { group = "io.micrometer", name = "micrometer-registry-prometheus" }
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor"} io-micrometer-micrometer_tracing_bridge_otel = { group = "io.micrometer", name = "micrometer-tracing-bridge-otel" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor"} io-opentelemetry-opentelemetry_exporter_otlp = { group = "io.opentelemetry", name = "opentelemetry-exporter-otlp" }
ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktor"} net-logstash-logback-logstash_logback_encoder = { group = "net.logstash.logback", name = "logstash-logback-encoder", version = "8.0" }
logback-encoder = { module = "net.logstash.logback:logstash-logback-encoder", version = "8.0" } org-flywaydb-flyway_core = { group = "org.flywaydb", name = "flyway-core", version = "9.22.3" }
micrometer-bridge-otel = { module = "io.micrometer:micrometer-tracing-bridge-otel"} org-jetbrains-kotlin-kotlin_reflect = { group = "org.jetbrains.kotlin", name = "kotlin-reflect", version.ref = "kotlin" }
micrometer-registry-prometheus = { module = "io.micrometer:micrometer-registry-prometheus" } org-jetbrains-kotlin-kotlin_test_junit5 = { group = "org.jetbrains.kotlin", name = "kotlin-test-junit5", version.ref = "kotlin" }
mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version = "5.4.0" } org-jetbrains-kotlinx-kotlinx_serialization_json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version = "1.6.3" }
otel-exporter = { module = "io.opentelemetry:opentelemetry-exporter-otlp" } org-mockito-kotlin-mockito_kotlin = { group = "org.mockito.kotlin", name = "mockito-kotlin", version = "5.4.0" }
postgres = { module = "org.postgresql:postgresql", version = "42.7.5" } org-postgresql-postgresql = { group = "org.postgresql", name = "postgresql", version = "42.7.5" }
springFramework-context = { module = "org.springframework:spring-context"} org-springdoc-springdoc_openapi_starter_webmvc_ui = { group = "org.springdoc", name = "springdoc-openapi-starter-webmvc-ui", version = "2.6.0" }
springFramework-aspects = { module = "org.springframework:spring-aspects" } org-springframework-boot-spring_boot_devtools = { group = "org.springframework.boot", name = "spring-boot-devtools" }
springBoot-devtools = { module = "org.springframework.boot:spring-boot-devtools" } org-springframework-boot-spring_boot_starter_actuator = { group = "org.springframework.boot", name = "spring-boot-starter-actuator", version.ref = "spring-boot" }
springBoot-starter-actuator = { module = "org.springframework.boot:spring-boot-starter-actuator", version.ref = "spring-boot" } org-springframework-boot-spring_boot_starter_actuator_autoconfigure = { group = "org.springframework.boot", name = "spring-boot-actuator-autoconfigure" }
springBoot-starter-actuatorAutoconfigure = { module = "org.springframework.boot:spring-boot-actuator-autoconfigure" } org-springframework-boot-spring_boot_starter_data_jdbc = { group = "org.springframework.boot", name = "spring-boot-starter-data-jdbc", version.ref = "spring-boot" }
springBoot-starter-jdbc = { module = "org.springframework.boot:spring-boot-starter-data-jdbc", version.ref = "spring-boot"} org-springframework-boot-spring_boot_starter_mustache = { group = "org.springframework.boot", name = "spring-boot-starter-mustache", version.ref = "spring-boot"}
springBoot-starter-mustache = { module = "org.springframework.boot:spring-boot-starter-mustache", version.ref = "spring-boot" } org-springframework-boot-spring_boot_starter_test = { group = "org.springframework.boot", name = "spring-boot-starter-test", version.ref = "spring-boot"}
springBoot-starter-test = { module = "org.springframework.boot:spring-boot-starter-test", version.ref = "spring-boot" } org-springframework-boot-spring_boot_starter_validation = { group = "org.springframework.boot", name = "spring-boot-starter-validation", version.ref = "spring-boot"}
springBoot-starter-validation = { module = "org.springframework.boot:spring-boot-starter-validation", version.ref = "spring-boot" } org-springframework-boot-spring_boot_starter_web = { group = "org.springframework.boot", name = "spring-boot-starter-web", version.ref = "spring-boot"}
springBoot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web", version.ref = "spring-boot" } org-springframework-cloud-spring_cloud_starter_stream_kafka = { group = "org.springframework.cloud", name = "spring-cloud-starter-stream-kafka", version.ref = "spring-cloud"}
springCloud-starter-streamKafka = { module = "org.springframework.cloud:spring-cloud-starter-stream-kafka", version.ref = "spring-cloud"} org-springframework-cloud-spring_cloud_stream = { group = "org.springframework.cloud", name = "spring-cloud-stream", version.ref = "spring-cloud" }
springCloud-stream = { module = "org.springframework.cloud:spring-cloud-stream", version.ref = "spring-cloud"} org-springframework-cloud-spring_cloud_stream_test_binder = { group = "org.springframework.cloud", name = "spring-cloud-stream-test-binder", version.ref = "spring-cloud" }
springCloud-streamTestBinder = { module = "org.springframework.cloud:spring-cloud-stream-test-binder", version.ref = "spring-cloud"} org-springframework-data-spring_data_commons = { group = "org.springframework.data", name = "spring-data-commons", version.ref = "spring-boot" }
springData-commons = { module = "org.springframework.data:spring-data-commons", version.ref = "spring-boot" } org-springframework-spring_aspects = { group = "org.springframework", name = "spring-aspects" }
springDoc-openapi-starter = "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0" org-springframework-spring_context = { group = "org.springframework", name = "spring-context"}
testcontainers = { module = "org.testcontainers:testcontainers", version.ref = "testcontainers"} org-testcontainers-junit_jupiter = { group = "org.testcontainers", name = "junit-jupiter", version.ref = "testcontainers" }
testcontainers-junit-jupiter = { module = "org.testcontainers:junit-jupiter", version.ref = "testcontainers"} org-testcontainers-postgresql = { group = "org.testcontainers", name = "postgresql", version.ref = "testcontainers" }
testcontainers-postgresql = { module = "org.testcontainers:postgresql", version.ref = "testcontainers"} org-testcontainers-testcontainers = { group = "org.testcontainers", name = "testcontainers", version.ref = "testcontainers" }
[bundles] [bundles]
tracing = ["micrometer-bridge-otel", "otel-exporter"] tracing = ["io-micrometer-micrometer_tracing_bridge_otel", "io-opentelemetry-opentelemetry_exporter_otlp"]
[plugins] [plugins]
kotlin-kover = { id = "org.jetbrains.kotlinx.kover", version = "0.8.3" } io-spring-dependency-management = { id = "io.spring.dependency-management", version = "1.1.7" }
kotlin-jpa = { id = "org.jetbrains.kotlin.plugin.jpa", version.ref = "kotlin" } org-jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } org-jetbrains-kotlin-plugin-jpa = { id = "org.jetbrains.kotlin.plugin.jpa", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } org-jetbrains-kotlin-plugin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
kotlin-spring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" } org-jetbrains-kotlin-plugin-spring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" }
spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" } org-jetbrains-kotlinx-kover = { id = "org.jetbrains.kotlinx.kover", version = "0.8.3" }
spring-dependencyManagement = { id = "io.spring.dependency-management", version = "1.1.7"} org-springframework-boot = { id = "org.springframework.boot", version.ref = "spring-boot" }

View File

@@ -31,7 +31,7 @@ Demo приложение для изучения языка `kotlin` и фре
* убедиться, что все запущенные контейнеры будут видеть контейнер с приложением (например, добавить везде сеть `spring-boot-demo_default`) * убедиться, что все запущенные контейнеры будут видеть контейнер с приложением (например, добавить везде сеть `spring-boot-demo_default`)
* скопировать [.env.example](/.env.example) в [.env](/.env) и изменить конфигурацию * скопировать [.env.example](/.env.example) в [.env](/.env) и изменить конфигурацию
Перед каждым запуском необходимо собрать приложение: После каждого изменения в исходный код необходимо собрать приложение:
```shell ```shell
./gradlew assemble ./gradlew assemble
``` ```

View File

@@ -9,6 +9,8 @@ spring:
driver-class-name: org.postgresql.Driver driver-class-name: org.postgresql.Driver
hikari: hikari:
schema: ${DB_SCHEMA:public} schema: ${DB_SCHEMA:public}
maximum-pool-size: ${HIKARI_DB_MAXIMUM_POOL_SIZE}
minimum-idle: ${HIKARI_DB_MINIMUM_IDLE_SIZE}
flyway: #flyway automatically uses the datasource from the application to connect to the DB flyway: #flyway automatically uses the datasource from the application to connect to the DB
enabled: true # enables flyway database migration enabled: true # enables flyway database migration
locations: classpath:db/migration/structure, classpath:db/migration/data # the location where flyway should look for migration scripts locations: classpath:db/migration/structure, classpath:db/migration/data # the location where flyway should look for migration scripts
@@ -98,6 +100,12 @@ management:
sampling: sampling:
probability: 1.0 probability: 1.0
server:
tomcat:
threads:
max: ${TOMCAT_THREADS_MAX}
min-spare: ${TOMCAT_THREADS_MIN}
tracing: tracing:
url: ${OTLP_TRACING_HTTP_URL} url: ${OTLP_TRACING_HTTP_URL}