Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ target/
*.iws
*.iml
*.ipr
./http-client.env.json
http-client.env.json

### NetBeans ###
/nbproject/private/
Expand All @@ -33,3 +33,6 @@ build/

### VS Code ###
.vscode/

grafana/data/*
prometheus/data/*
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ RUN mvn dependency:go-offline
COPY src src
RUN mvn package

FROM openjdk:17-jdk-slim
FROM eclipse-temurin:17-alpine-3.22

COPY --from=build /app/target/*.jar /high-load-course.jar

Expand Down
92 changes: 34 additions & 58 deletions grafana/provisioning/dashboards/ServicesStatistic.json
Original file line number Diff line number Diff line change
Expand Up @@ -1545,16 +1545,13 @@
"viz": false
},
"insertNulls": false,
"lineInterpolation": "smooth",
"lineStyle": {
"fill": "solid"
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
Expand All @@ -1570,90 +1567,69 @@
"steps": [
{
"color": "green",
"value": null
"value": 0
},
{
"color": "red",
"value": 80
}
]
},
"unit": "ms"
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "success"
},
"properties": [
{
"id": "color",
"value": {
"fixedColor": "green",
"mode": "fixed"
}
}
]
},
{
"matcher": {
"id": "byName",
"options": "fail"
},
"properties": [
{
"id": "color",
"value": {
"fixedColor": "red",
"mode": "fixed"
}
}
]
}
]
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"x": 0,
"y": 4
},
"id": 14,
"id": 101,
"options": {
"legend": {
"calcs": [
"max"
],
"displayMode": "table",
"placement": "right",
"showLegend": true,
"sortBy": "Max",
"sortDesc": true
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "11.4.0",
"pluginVersion": "12.1.1",
"targets": [
{
"disableTextWrap": false,
"editorMode": "builder",
"expr": "rate(http_requests_served_total{endpoint=~\"pay_order\"}[$__rate_interval])",
"fullMetaSearch": false,
"includeNullMetadata": true,
"legendFormat": "__auto",
"range": true,
"refId": "A",
"useBackend": false
},
{
"datasource": {
"type": "prometheus",
"uid": "PBFA97CFB590B2093"
},
"editorMode": "code",
"exemplar": true,
"expr": "rate(http_external_duration_sum{service=~\"$service\"}[1m]) / rate(http_external_duration_count{service=~\"$service\"}[1m])",
"disableTextWrap": false,
"editorMode": "builder",
"expr": "rate(payment_service_sanded_total{service=~\"$service\"}[$__rate_interval])",
"fullMetaSearch": false,
"hide": false,
"interval": "",
"legendFormat": "{{method}} - {{result}}",
"includeNullMetadata": true,
"instant": false,
"legendFormat": "__auto",
"range": true,
"refId": "C"
"refId": "B",
"useBackend": false
}
],
"title": "(Clients -> Online shop) http requests duration. Average",
"title": "sending rate & processing speed",
"type": "timeseries"
},
{
Expand Down
68 changes: 68 additions & 0 deletions http-client.env.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{
"Lab4 :: acc-18": {
"accounts": "acc-18",
"serviceName": "m3403-8",
"token": "bTgVPIlM5Jbr0T03=8",
"ratePerSecond": "200",
"testCount": "6000",
"processingTimeMillis": "3000"
},
"Lab4 :: acc-23": {
"accounts": "acc-23",
"serviceName": "m3403-8",
"token": "bTgVPIlM5Jbr0T03=8",
"ratePerSecond": "16",
"testCount": "1600",
"processingTimeMillis": "30000"
},
"Lab5 :: acc-23 :: v1": {
"accounts": "acc-23",
"serviceName": "m3403-8",
"token": "bTgVPIlM5Jbr0T03=8",
"ratePerSecond": "15",
"testCount": "3000",
"processingTimeMillis": "2500"
},
"Lab5 :: acc-23 :: v2": {
"accounts": "acc-23",
"serviceName": "m3403-8",
"token": "bTgVPIlM5Jbr0T03=8",
"ratePerSecond": "11",
"testCount": "2200",
"processingTimeMillis": "13000",
"profile": "s_0.7_60"
},
"Lab5 :: acc-23 :: v3": {
"accounts": "acc-23",
"serviceName": "m3403-8",
"token": "bTgVPIlM5Jbr0T03=8",
"ratePerSecond": "3",
"testCount": "1150",
"processingTimeMillis": "26000",
"runits": "90"
},
"Lab6 :: acc-8 :: v3": {
"accounts": "acc-8",
"serviceName": "m3403-8",
"token": "bTgVPIlM5Jbr0T03=8",
"ratePerSecond": "7",
"testCount": "800",
"processingTimeMillis": "3500"
},
"Lab8 :: acc-9": {
"accounts": "acc-9",
"serviceName": "m3403-8",
"token": "bTgVPIlM5Jbr0T03=8",
"ratePerSecond": "100",
"testCount": "5000",
"processingTimeMillis": "20000"
},
"Lab9 :: acc-12": {
"accounts": "acc-12",
"serviceName": "m3403-8",
"token": "bTgVPIlM5Jbr0T03=8",
"ratePerSecond": "1000",
"testCount": "200000",
"processingTimeMillis": "50000"
}
}
6 changes: 5 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus-simpleclient</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test</artifactId>
Expand Down Expand Up @@ -175,7 +179,7 @@
<compilerPlugins>
<plugin>spring</plugin>
</compilerPlugins>
<jvmTarget>1.8</jvmTarget>
<jvmTarget>17</jvmTarget>
</configuration>
<dependencies>
<dependency>
Expand Down
31 changes: 28 additions & 3 deletions src/main/kotlin/ru/quipy/apigateway/APIController.kt
Original file line number Diff line number Diff line change
@@ -1,18 +1,34 @@
package ru.quipy.apigateway

import io.micrometer.core.instrument.Counter
import io.micrometer.core.instrument.MeterRegistry
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.bind.annotation.*
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import ru.quipy.common.utils.TooManyRequestsException
import ru.quipy.orders.repository.OrderRepository
import ru.quipy.payments.logic.OrderPayer
import java.util.*
import java.util.UUID

@RestController
class APIController {
class APIController(@Autowired val meterRegistry: MeterRegistry) {

val logger: Logger = LoggerFactory.getLogger(APIController::class.java)

private val orderCounter: Counter = Counter.builder("http_requests_served")
.description("Total number of served http requests for order payment")
.tag("endpoint", "pay_order")
.register(meterRegistry)

@Autowired
private lateinit var orderRepository: OrderRepository

Expand All @@ -21,6 +37,7 @@ class APIController {

@PostMapping("/users")
fun createUser(@RequestBody req: CreateUserRequest): User {
orderCounter.increment()
return User(UUID.randomUUID(), req.name)
}

Expand All @@ -30,6 +47,7 @@ class APIController {

@PostMapping("/orders")
fun createOrder(@RequestParam userId: UUID, @RequestParam price: Int): Order {
orderCounter.increment()
val order = Order(
UUID.randomUUID(),
userId,
Expand Down Expand Up @@ -57,6 +75,7 @@ class APIController {
@PostMapping("/orders/{orderId}/payment")
fun payOrder(@PathVariable orderId: UUID, @RequestParam deadline: Long): PaymentSubmissionDto {
val paymentId = UUID.randomUUID()
orderCounter.increment()
val order = orderRepository.findById(orderId)?.let {
orderRepository.save(it.copy(status = OrderStatus.PAYMENT_IN_PROGRESS))
it
Expand All @@ -67,6 +86,12 @@ class APIController {
return PaymentSubmissionDto(createdAt, paymentId)
}

@ExceptionHandler(TooManyRequestsException::class)
fun tooManyRequestsExceptionHandler(exception: TooManyRequestsException) =
ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
.headers(HttpHeaders().apply { add(HttpHeaders.RETRY_AFTER, exception.delay.toString()) })
.body(exception.message)

class PaymentSubmissionDto(
val timestamp: Long,
val transactionId: UUID
Expand Down
46 changes: 46 additions & 0 deletions src/main/kotlin/ru/quipy/common/utils/InstantRateLimitSemaphore.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package ru.quipy.common.utils

import kotlinx.coroutines.sync.Semaphore
import java.time.Duration
import java.time.Instant
import java.util.concurrent.TimeUnit
import kotlin.math.floor

/**
* Обёртка над семафором, которая учитывает сколько семафоров можно взять в единицу времени и если до указанного
* момента осталось меньше, чем limitDuration единиц времени, то ограничивает возможность взятия семафора
*/
class InstantRateLimitSemaphore(
limitDuration: Duration,
private val rateDuration: Duration,
private val unitsPerRate: Int
): RateLimitSemaphore<Instant>(Semaphore(durationCapacity(limitDuration, rateDuration, unitsPerRate)) ) {

constructor(limitDuration: Duration, timeUnit: TimeUnit, unitsPerRate: Int) :
this(limitDuration, Duration.ofNanos(timeUnit.toNanos(1)), unitsPerRate)

/**
* Функция вычисляет допустимый limit взятий семафора на заданный период
*/
override fun limit(volume: Instant): Int =
Duration.between(Instant.now(), volume)
?.takeIf { it.toNanos() > 0 }
?.let { durationCapacity(it, rateDuration, unitsPerRate) }
?: 0

companion object {
/**
* Вычисление количества ресурса на Duration при известном объёме на единицу TimeUnit
* @param duration Длительность
* @param rateDuration Длительность периода
* @param unitsPerRate Количество ресурса на период
* @return количество элементов, доступных на duration
*/
private fun durationCapacity(
duration: Duration,
rateDuration: Duration,
unitsPerRate: Int) =
floor(unitsPerRate.toDouble() * duration.toNanos() / rateDuration.toNanos()).toInt()
}

}
Loading