Skip to content

Commit

Permalink
feat: CircuitBreaker 패턴 적용 QUZ-122
Browse files Browse the repository at this point in the history
  • Loading branch information
HMWG committed Dec 6, 2024
1 parent 722dd48 commit 12684cd
Show file tree
Hide file tree
Showing 6 changed files with 215 additions and 9 deletions.
6 changes: 5 additions & 1 deletion gateway-service/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,12 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-security")
testImplementation("org.springframework.security:spring-security-test")

//redis
// Redis
implementation("org.springframework.boot:spring-boot-starter-data-redis-reactive")

// Circuit Breaker
implementation("org.springframework.cloud:spring-cloud-starter-circuitbreaker-reactor-resilience4j:3.1.0")

}

tasks.named<BootJar>("bootJar") {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.grepp.quizy.api

import com.grepp.quizy.common.api.ApiResponse
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/fallback")
class GatewayFallbackController {
@GetMapping("/user")
fun userServiceFallback(): ResponseEntity<ApiResponse<Unit>> {
return ResponseEntity
.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(ApiResponse.error(HttpStatus.SERVICE_UNAVAILABLE.name, "User service is temporarily unavailable"))
}

@GetMapping("/quiz")
fun quizServiceFallback(): ResponseEntity<ApiResponse<Unit>> {
return ResponseEntity
.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(ApiResponse.error(HttpStatus.SERVICE_UNAVAILABLE.name, "Quiz service is temporarily unavailable"))
}

@GetMapping("/game")
fun gameServiceFallback(): ResponseEntity<ApiResponse<Unit>> {
return ResponseEntity
.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(ApiResponse.error(HttpStatus.SERVICE_UNAVAILABLE.name, "Game service is temporarily unavailable"))
}

@GetMapping("/ws")
fun webSocketServiceFallback(): ResponseEntity<ApiResponse<Unit>> {
return ResponseEntity
.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(
ApiResponse.error(
HttpStatus.SERVICE_UNAVAILABLE.name,
"Game webSocket service is temporarily unavailable"
)
)
}

@GetMapping("/matching")
fun matchingServiceFallback(): ResponseEntity<ApiResponse<Unit>> {
return ResponseEntity
.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(ApiResponse.error(HttpStatus.SERVICE_UNAVAILABLE.name, "Matching service is temporarily unavailable"))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import com.grepp.quizy.jwt.JwtValidator
import com.grepp.quizy.user.RedisTokenRepository
import com.grepp.quizy.user.UserId
import com.grepp.quizy.user.api.global.util.CookieUtils
import com.grepp.quizy.web.UserClient
import com.grepp.quizy.webclient.UserClient
import org.slf4j.LoggerFactory
import org.springframework.cloud.gateway.filter.GatewayFilterChain
import org.springframework.cloud.gateway.filter.GlobalFilter
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.grepp.quizy.webclient

import reactor.core.publisher.Mono

interface UserClient {
fun validateUser(userId: Long): Mono<Unit>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.grepp.quizy.webclient

import com.grepp.quizy.exception.CustomJwtException
import com.grepp.quizy.user.RedisTokenRepository
import com.grepp.quizy.user.UserId
import org.springframework.beans.factory.annotation.Value
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.client.WebClient
import reactor.core.publisher.Mono

@Component
class UserClientImpl(
private val webClient: WebClient,
private val redisTokenRepository: RedisTokenRepository
) : UserClient {

@Value("\${service.user.url}")
private lateinit var BASE_URL: String


override fun validateUser(userId: Long): Mono<Unit> {
if (redisTokenRepository.isExistUser(UserId(userId))) {
return Mono.just(Unit)
}

return webClient.get()
.uri("$BASE_URL/api/internal/user/validate/$userId")
.retrieve()
.toEntity(Unit::class.java)
.handle<Unit> { response, sink ->
when (response.statusCode) {
HttpStatus.OK -> sink.next(Unit)
HttpStatus.UNAUTHORIZED -> sink.error(CustomJwtException.JwtUnknownException)
else -> sink.error(CustomJwtException.NotExistUserException)
}
}
}
}
118 changes: 111 additions & 7 deletions gateway-service/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ management:
endpoints:
web:
exposure:
include: health, info
include: health, info, circuitbreakers
springdoc:
swagger-ui:
use-root-path: true
Expand Down Expand Up @@ -46,6 +46,46 @@ spring:
- "*"
allowCredentials: true

resilience4j:
circuitbreaker:
configs:
default:
slidingWindowSize: 10
failureRateThreshold: 50
waitDurationInOpenState: 60000
permittedNumberOfCallsInHalfOpenState: 3
record-exceptions:
- java.io.IOException
- java.util.concurrent.TimeoutException
instances:
userServiceCircuitBreaker:
baseConfig: default
gameServiceCircuitBreaker:
baseConfig: default
matchingServiceCircuitBreaker:
baseConfig: default
quizServiceCircuitBreaker:
baseConfig: default
wsCircuitBreaker:
baseConfig: default
timelimiter:
configs:
default:
timeoutDuration: 4s
SSE:
timeoutDuration: 86400s # 24시간 또는 적절한 값으로 설정
instances:
userServiceCircuitBreaker:
baseConfig: default
gameServiceCircuitBreaker:
baseConfig: default
matchingServiceCircuitBreaker:
baseConfig: SSE
quizServiceCircuitBreaker:
baseConfig: default
wsCircuitBreaker:
baseConfig: SSE

---
spring:
config:
Expand All @@ -57,29 +97,53 @@ spring:
- id: game
uri: http://localhost:8081
predicates:
- Path=/api/game/**, /ws/**
- Path=/api/game/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
redis-rate-limiter.requestedTokens: 1
key-resolver: "#{@userKeyResolver}"
- name: CircuitBreaker
args:
name: gameServiceCircuitBreaker
fallbackUri: forward:/fallback/game

- id: webSocket
uri: http://localhost:8081
predicates:
- Path=/ws/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
redis-rate-limiter.requestedTokens: 1
key-resolver: "#{@userKeyResolver}"
- name: CircuitBreaker
args:
name: wsCircuitBreaker
fallbackUri: forward:/fallback/ws

- id: matching
uri: http://localhost:8082
predicates:
- Path=/api/matching/**
metadata:
response-timeout: 300000
connect-timeout: 300000
response-timeout: 330000
connect-timeout: 330000
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
redis-rate-limiter.requestedTokens: 1
key-resolver: "#{@userKeyResolver}"
- name: CircuitBreaker
args:
name: matchingServiceCircuitBreaker
fallbackUri: forward:/fallback/matching

- id: quiz
uri: http://localhost:8083
Expand All @@ -92,6 +156,10 @@ spring:
redis-rate-limiter.burstCapacity: 20
redis-rate-limiter.requestedTokens: 1
key-resolver: "#{@userKeyResolver}"
- name: CircuitBreaker
args:
name: quizServiceCircuitBreaker
fallbackUri: forward:/fallback/quiz

- id: user
uri: http://localhost:8085
Expand All @@ -104,6 +172,10 @@ spring:
redis-rate-limiter.burstCapacity: 20
redis-rate-limiter.requestedTokens: 1
key-resolver: "#{@userKeyResolver}"
- name: CircuitBreaker
args:
name: userServiceCircuitBreaker
fallbackUri: forward:/fallback/user

data:
redis:
Expand Down Expand Up @@ -151,29 +223,53 @@ spring:
- id: game
uri: http://dev-game-service:8080
predicates:
- Path=/api/game/**, /ws/**
- Path=/api/game/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
redis-rate-limiter.requestedTokens: 1
key-resolver: "#{@userKeyResolver}"
- name: CircuitBreaker
args:
name: gameServiceCircuitBreaker
fallbackUri: forward:/fallback/game

- id: webSocket
uri: http://dev-game-service:8080
predicates:
- Path=/ws/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
redis-rate-limiter.requestedTokens: 1
key-resolver: "#{@userKeyResolver}"
- name: CircuitBreaker
args:
name: wsCircuitBreaker
fallbackUri: forward:/fallback/ws

- id: matching
uri: http://dev-matching-service:8080
predicates:
- Path=/api/matching/**
metadata:
response-timeout: 300000
connect-timeout: 300000
response-timeout: 330000
connect-timeout: 330000
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
redis-rate-limiter.requestedTokens: 1
key-resolver: "#{@userKeyResolver}"
- name: CircuitBreaker
args:
name: matchingServiceCircuitBreaker
fallbackUri: forward:/fallback/matching

- id: quiz
uri: http://dev-quiz-service:8080
Expand All @@ -186,6 +282,10 @@ spring:
redis-rate-limiter.burstCapacity: 20
redis-rate-limiter.requestedTokens: 1
key-resolver: "#{@userKeyResolver}"
- name: CircuitBreaker
args:
name: quizServiceCircuitBreaker
fallbackUri: forward:/fallback/quiz

- id: user
uri: http://dev-user-service:8080
Expand All @@ -198,6 +298,10 @@ spring:
redis-rate-limiter.burstCapacity: 20
redis-rate-limiter.requestedTokens: 1
key-resolver: "#{@userKeyResolver}"
- name: CircuitBreaker
args:
name: userServiceCircuitBreaker
fallbackUri: forward:/fallback/user

kubernetes:
discovery:
Expand Down

0 comments on commit 12684cd

Please sign in to comment.