Skip to content
Open
207 changes: 207 additions & 0 deletions grafana/provisioning/dashboards/ServicesStatistic.json
Original file line number Diff line number Diff line change
Expand Up @@ -3727,6 +3727,213 @@
],
"title": "Payment Processing Latency",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "PBFA97CFB590B2093"
},
"description": "Количество повторных запросов к платежной системе в секунду",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "Запросов/сек",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 30,
"gradientMode": "opacity",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "smooth",
"lineWidth": 2,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "yellow",
"value": 1
},
{
"color": "red",
"value": 5
}
]
},
"unit": "reqps"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 9
},
"id": 109,
"options": {
"legend": {
"calcs": [
"mean",
"lastNotNull",
"max"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "PBFA97CFB590B2093"
},
"editorMode": "code",
"expr": "rate(payment_request_retried_total{accountName=~\".*\"}[1m])",
"legendFormat": "Повторные запросы: {{accountName}}",
"range": true,
"refId": "A"
}
],
"title": "Payment Request Retries Rate (per second)",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "PBFA97CFB590B2093"
},
"description": "Общее количество повторных запросов к платежной системе",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "Количество",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 2,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "yellow",
"value": 100
},
{
"color": "red",
"value": 500
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 9
},
"id": 110,
"options": {
"legend": {
"calcs": [
"lastNotNull",
"max"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "PBFA97CFB590B2093"
},
"editorMode": "code",
"expr": "payment_request_retried_total{accountName=~\".*\"}",
"legendFormat": "Всего повторов: {{accountName}}",
"range": true,
"refId": "A"
}
],
"title": "Payment Request Retries Total Count",
"type": "timeseries"
}],
"preload": false,
"refresh": "5s",
Expand Down
5 changes: 3 additions & 2 deletions src/main/kotlin/ru/quipy/OnlineShopApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import org.slf4j.LoggerFactory
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import ru.quipy.common.utils.NamedThreadFactory
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors


Expand All @@ -14,10 +15,10 @@ class OnlineShopApplication {
val log: Logger = LoggerFactory.getLogger(OnlineShopApplication::class.java)

companion object {
val appExecutor = Executors.newFixedThreadPool(20_000, NamedThreadFactory("main-app-executor")).asCoroutineDispatcher()
val appExecutor: ExecutorService = Executors.newFixedThreadPool(64, NamedThreadFactory("main-app-executor"))
}
}

suspend fun main(args: Array<String>) {
fun main(args: Array<String>) {
runApplication<OnlineShopApplication>(*args)
}
8 changes: 4 additions & 4 deletions src/main/kotlin/ru/quipy/apigateway/APIController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import ru.quipy.common.utils.RateLimiter
import ru.quipy.exceptions.TooManyRequestsException
import ru.quipy.orders.repository.OrderRepository
import ru.quipy.payments.logic.OrderPayer
import ru.quipy.payments.logic.now
import java.time.Duration
import java.util.*
import kotlin.random.Random

@RestController
class APIController(
Expand Down Expand Up @@ -60,10 +60,10 @@ class APIController(

@PostMapping("/orders/{orderId}/payment")
fun payOrder(@PathVariable orderId: UUID, @RequestParam deadline: Long): PaymentSubmissionDto {
if (!rateLimiter.tick()) {
val retryAfterMs = 10L + Random.nextLong(10)
throw TooManyRequestsException(retryAfterMs)
while (!rateLimiter.tick() && now() < deadline) {
}
if (now() >= deadline)
throw TooManyRequestsException(10);
val paymentId = UUID.randomUUID()
val order = orderRepository.findById(orderId)?.let {
orderRepository.save(it.copy(status = OrderStatus.PAYMENT_IN_PROGRESS))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,9 @@ class GlobalExceptionHandler(

@ExceptionHandler(TooManyRequestsException::class)
fun handleTooManyRequestsRetriable(exception: TooManyRequestsException): ResponseEntity<String> {
val retryAfterSeconds = (exception.retryAfterMs / 1000.0).coerceAtLeast(0.1)
return ResponseEntity
.status(HttpStatus.TOO_MANY_REQUESTS)
.header("Retry-After", retryAfterSeconds.toString())
.header("Retry-After", "10")
.body("too many requests")
}
}
15 changes: 13 additions & 2 deletions src/main/kotlin/ru/quipy/payments/config/PaymentAccountsConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import io.micrometer.core.instrument.MeterRegistry
import kotlinx.coroutines.sync.Semaphore
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import ru.quipy.common.utils.SlidingWindowRateLimiter
import ru.quipy.core.EventSourcingService
import ru.quipy.payments.api.PaymentAggregate
import ru.quipy.payments.logic.PaymentAccountProperties
Expand All @@ -18,7 +18,9 @@ import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.time.Duration
import java.util.*
import java.util.concurrent.Semaphore


@Configuration
Expand All @@ -40,10 +42,18 @@ class PaymentAccountsConfig {
@Value("#{'\${payment.accounts}'.split(',')}")
lateinit var allowedAccounts: List<String>

@Bean
fun rateLimit(): SlidingWindowRateLimiter {
return SlidingWindowRateLimiter(
rate = 1100L,
window = Duration.ofMillis(1000),
)
}
@Bean
fun accountAdapters(
paymentService: EventSourcingService<UUID, PaymentAggregate, PaymentAggregateState>,
meterRegistry: MeterRegistry,
rateLimiter: SlidingWindowRateLimiter
): List<PaymentExternalSystemAdapter> {
val request = HttpRequest.newBuilder()
.uri(URI("http://${paymentProviderHostPort}/external/accounts?serviceName=$serviceName&token=$token"))
Expand All @@ -67,7 +77,8 @@ class PaymentAccountsConfig {
paymentProviderHostPort,
token,
meterRegistry,
Semaphore(it.parallelRequests)
Semaphore(it.parallelRequests),
rateLimiter
)
}
}
Expand Down
31 changes: 25 additions & 6 deletions src/main/kotlin/ru/quipy/payments/logic/OrderPayer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,33 @@ import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import ru.quipy.common.utils.NamedThreadFactory
import ru.quipy.common.utils.SlidingWindowRateLimiter
import ru.quipy.core.EventSourcingService
import ru.quipy.exceptions.TooManyRequestsException
import ru.quipy.payments.api.PaymentAggregate
import java.util.*
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit

@Service
class OrderPayer(meterRegistry: MeterRegistry) {
class OrderPayer(val rateLimiter : SlidingWindowRateLimiter, meterRegistry: MeterRegistry) {

companion object {
val logger: Logger = LoggerFactory.getLogger(OrderPayer::class.java)
}

private val plannedRequests = meterRegistry.counter("payment.processing.planned", "accountName", "acc-12")

private val paymentExecutor = ThreadPoolExecutor(
3600,
3600,
100L,
TimeUnit.MILLISECONDS,
LinkedBlockingQueue(100_000),
NamedThreadFactory("payment-submission-executor")
)

@Autowired
private lateinit var paymentESService: EventSourcingService<UUID, PaymentAggregate, PaymentAggregateState>
Expand All @@ -28,19 +42,24 @@ class OrderPayer(meterRegistry: MeterRegistry) {

fun processPayment(orderId: UUID, amount: Int, paymentId: UUID, deadline: Long): Long {
val createdAt = System.currentTimeMillis()
plannedRequests.increment()
val createdEvent = paymentESService.create {
while (!rateLimiter.tick() && now() < deadline) {
}
if (now() >= deadline)
throw TooManyRequestsException(10)
paymentExecutor.submit {
plannedRequests.increment()
val createdEvent = paymentESService.create {
it.create(
paymentId,
orderId,
amount
)
}

logger.trace("Payment {} for order {} created.", createdEvent.paymentId, orderId)

paymentService.submitPaymentRequest(paymentId, amount, createdAt, deadline)
logger.trace("Payment {} for order {} created.", createdEvent.paymentId, orderId)

paymentService.submitPaymentRequest(paymentId, amount, createdAt, deadline)
}
return createdAt
}
}
Loading