Skip to content

Commit 13ada9c

Browse files
authored
release: 2.3.2 (#413)
2 parents 7336ff3 + 5a49314 commit 13ada9c

File tree

18 files changed

+425
-37
lines changed

18 files changed

+425
-37
lines changed

gradle.properties

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,6 @@ jacksonVersion=2.15.3
5252

5353
### Redisson ###
5454
redissonVersion=3.47.0
55+
56+
### Slf4jKotinx ###
57+
slf4jKotlinxVersion = 1.9.0

gradle/core.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ dependencies {
33
implementation "com.github.ben-manes.caffeine:caffeine:${caffeineCacheVersion}"
44
implementation "com.fasterxml.jackson.module:jackson-module-kotlin:${jacksonVersion}"
55
implementation "org.redisson:redisson-spring-boot-starter:${redissonVersion}"
6+
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-slf4j:${slf4jKotlinxVersion}"
67
}

src/main/kotlin/org/gitanimals/core/appender/SlackAppender.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class SlackAppender : AppenderBase<ILoggingEvent>() {
2929
val request: ChatPostMessageRequest = ChatPostMessageRequest.builder()
3030
.channel(channel)
3131
.text(eventObject.formattedMessage)
32-
.build();
32+
.build()
3333
slack.chatPostMessage(request)
3434
}
3535
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package org.gitanimals.core.ratelimit
2+
3+
import org.gitanimals.core.instant
4+
import java.time.Instant
5+
import java.time.LocalDateTime
6+
7+
interface RateLimitable {
8+
9+
fun <T> acquire(limitPercent: Double = 0.0, action: suspend (RateLimit?) -> T): T
10+
11+
fun update(rateLimit: RateLimit)
12+
13+
data class RateLimit(
14+
val limit: Int,
15+
val remaining: Int,
16+
val resetAt: LocalDateTime,
17+
val used: Int,
18+
val requestedAt: Instant = instant(),
19+
) {
20+
fun getRemainPercentage(): Double {
21+
val percentage = (remaining.toDouble() / limit.toDouble()) * 100.0
22+
return percentage.coerceIn(0.0, 100.0)
23+
}
24+
}
25+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package org.gitanimals.core.slack
2+
3+
import com.slack.api.Slack
4+
import com.slack.api.methods.MethodsClient
5+
import com.slack.api.methods.request.chat.ChatPostMessageRequest
6+
import org.springframework.beans.factory.annotation.Value
7+
import org.springframework.stereotype.Component
8+
9+
@Component
10+
class SlackSender(
11+
@Value("\${slack.token}") slackToken: String,
12+
) {
13+
14+
private val slack: MethodsClient by lazy {
15+
Slack.getInstance().methods(slackToken)
16+
}
17+
18+
fun send(channel: String, message: String) {
19+
val request: ChatPostMessageRequest = ChatPostMessageRequest.builder()
20+
.channel(channel)
21+
.text(message)
22+
.build()
23+
24+
slack.chatPostMessage(request)
25+
}
26+
}

src/main/kotlin/org/gitanimals/rank/infra/github/RankGithubContributionApi.kt

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
package org.gitanimals.rank.infra.github
22

3+
import org.gitanimals.core.ratelimit.RateLimitable
34
import org.gitanimals.rank.app.RankContributionApi
5+
import org.springframework.beans.factory.annotation.Qualifier
46
import org.springframework.beans.factory.annotation.Value
57
import org.springframework.core.io.ClassPathResource
68
import org.springframework.http.HttpHeaders
79
import org.springframework.stereotype.Component
810
import org.springframework.web.client.RestClient
911
import java.nio.charset.Charset
1012
import java.time.LocalDate
13+
import java.time.LocalDateTime
1114

1215
@Component
1316
class RankGithubContributionApi(
14-
@Value("\${github.token}") private val token: String
17+
@Value("\${github.token}") private val token: String,
18+
@Qualifier("inmemoryGithubRateLimiter") private val rateLimiter: RateLimitable,
1519
) : RankContributionApi {
1620

1721
private val restClient = RestClient.create("https://api.github.com/graphql")
@@ -20,11 +24,11 @@ class RankGithubContributionApi(
2024
username: String,
2125
from: LocalDate,
2226
to: LocalDate
23-
): Int {
27+
): Int = rateLimiter.acquire {
2428
val fromString = from.toString()
2529
val toString = from.toString()
2630

27-
return restClient.post()
31+
return@acquire restClient.post()
2832
.header(HttpHeaders.AUTHORIZATION, "Bearer $token")
2933
.body(
3034
mapOf(
@@ -36,9 +40,19 @@ class RankGithubContributionApi(
3640
).exchange { _, response ->
3741
assertIsSuccess(response)
3842

39-
response.bodyTo(ContributionCountByYearAndWeekQueryResponse::class.java)!!
40-
.data
41-
.user
43+
val data =
44+
response.bodyTo(ContributionCountByYearAndWeekQueryResponse::class.java)!!.data
45+
46+
rateLimiter.update(
47+
RateLimitable.RateLimit(
48+
limit = data.rateLimit.limit,
49+
remaining = data.rateLimit.remaining,
50+
resetAt = data.rateLimit.resetAt,
51+
used = data.rateLimit.used,
52+
)
53+
)
54+
55+
return@exchange data.user
4256
.contributionsCollection
4357
.contributionCalendar
4458
.totalContributions
@@ -56,7 +70,10 @@ class RankGithubContributionApi(
5670
}
5771

5872
private data class ContributionCountByYearAndWeekQueryResponse(val data: Data) {
59-
class Data(val user: User) {
73+
class Data(
74+
val rateLimit: RateLimit,
75+
val user: User,
76+
) {
6077
class User(val contributionsCollection: ContributionsCollection) {
6178
class ContributionsCollection(
6279
val contributionCalendar: ContributionCalendar,
@@ -70,6 +87,14 @@ class RankGithubContributionApi(
7087
}
7188
}
7289

90+
class RateLimit(
91+
val limit: Int,
92+
val cost: Int,
93+
val remaining: Int,
94+
val resetAt: LocalDateTime,
95+
val used: Int,
96+
)
97+
7398
companion object {
7499
private const val NAME_FIX = "*{name}"
75100
private const val DATE_START_FIX = "*{yyyy-mm-dd-start}"

src/main/kotlin/org/gitanimals/render/domain/User.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,13 +145,13 @@ class User(
145145
}
146146

147147
@JsonIgnore
148-
fun isContributionUpdatedBeforeOneHour(): Boolean {
148+
fun isContributionUpdatedLongAgo(): Boolean {
149149
val currentYear = instant().toZonedDateTime(ZoneId.of("UTC")).year
150150
val currentYearContribution =
151151
contributions.firstOrNull { it.year == currentYear } ?: return true
152152

153153
return currentYearContribution.lastUpdatedContribution.isBefore(
154-
Instant.now().minus(1, ChronoUnit.HOURS)
154+
Instant.now().minus(10, ChronoUnit.MINUTES)
155155
)
156156
}
157157

src/main/kotlin/org/gitanimals/render/domain/UserService.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ class UserService(
4545
return increasedContributionCount
4646
}
4747

48-
fun isContributionUpdatedBeforeOneHour(name: String): Boolean =
49-
getUserByName(name).isContributionUpdatedBeforeOneHour()
48+
fun isContributionUpdatedLongAgo(name: String): Boolean =
49+
getUserByName(name).isContributionUpdatedLongAgo()
5050

5151
fun getUserByName(name: String): User = userRepository.findByName(name)
5252
?: throw IllegalArgumentException("Cannot find exists user by name \"$name\"")

src/main/kotlin/org/gitanimals/render/infra/GithubContributionApi.kt

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,40 @@
11
package org.gitanimals.render.infra
22

3+
import org.gitanimals.core.ratelimit.RateLimitable
34
import org.gitanimals.render.app.ContributionApi
5+
import org.slf4j.LoggerFactory
6+
import org.springframework.beans.factory.annotation.Qualifier
47
import org.springframework.beans.factory.annotation.Value
58
import org.springframework.core.io.ClassPathResource
69
import org.springframework.http.HttpHeaders
710
import org.springframework.stereotype.Component
811
import org.springframework.web.client.RestClient
912
import java.nio.charset.Charset
13+
import java.time.LocalDateTime
1014
import java.util.concurrent.CompletableFuture
1115
import java.util.concurrent.Executors
1216

1317
@Component
1418
class GithubContributionApi(
15-
@Value("\${github.token}") private val token: String
19+
@Value("\${github.token}") private val token: String,
20+
@Qualifier("inmemoryGithubRateLimiter") private val rateLimiter: RateLimitable,
1621
) : ContributionApi {
1722

1823
private val restClient = RestClient.create("https://api.github.com/graphql")
1924
private val executors = Executors.newFixedThreadPool(50)
2025

26+
private val logger = LoggerFactory.getLogger(this::class.simpleName)
27+
2128
override fun getContributionCount(username: String, years: List<Int>): Map<Int, Int> {
22-
val contributionCountResponses = callGetContributionCountApis(years, username)
29+
val contributionCountResponses = runCatching {
30+
callGetContributionCountApis(years, username)
31+
}.getOrElse {
32+
logger.warn(
33+
"[GithubContributionApi] Fail to retrieve user info from github. cause: ${it.message}",
34+
it
35+
)
36+
throw it
37+
}
2338

2439
val ans = mutableMapOf<Int, Int>()
2540
years.withIndex().forEach {
@@ -35,7 +50,7 @@ class GithubContributionApi(
3550
private fun callGetContributionCountApis(
3651
years: List<Int>,
3752
username: String
38-
): MutableList<CompletableFuture<Int>> {
53+
): MutableList<CompletableFuture<Int>> = rateLimiter.acquire {
3954
val completableFutures = mutableListOf<CompletableFuture<Int>>()
4055
years.forEach { year ->
4156
val completableFuture = CompletableFuture.supplyAsync({
@@ -51,9 +66,20 @@ class GithubContributionApi(
5166
.exchange { _, response ->
5267
assertIsSuccess(response)
5368

54-
response.bodyTo(ContributionCountByYearQueryResponse::class.java)!!
55-
.data
56-
.user
69+
val data =
70+
response.bodyTo(ContributionCountByYearQueryResponse::class.java)!!
71+
.data
72+
73+
rateLimiter.update(
74+
RateLimitable.RateLimit(
75+
limit = data.rateLimit.limit,
76+
remaining = data.rateLimit.remaining,
77+
resetAt = data.rateLimit.resetAt,
78+
used = data.rateLimit.used,
79+
)
80+
)
81+
82+
return@exchange data.user
5783
.contributionsCollection
5884
.contributionCalendar
5985
.totalContributions
@@ -62,19 +88,29 @@ class GithubContributionApi(
6288

6389
completableFutures.add(completableFuture)
6490
}
65-
return completableFutures
91+
return@acquire completableFutures
6692
}
6793

68-
override fun getAllContributionYears(username: String): List<Int> {
69-
return restClient.post()
94+
override fun getAllContributionYears(username: String): List<Int> = rateLimiter.acquire {
95+
return@acquire restClient.post()
7096
.header(HttpHeaders.AUTHORIZATION, "Bearer $token")
7197
.body(mapOf("query" to contributionYearQuery.replace(NAME_FIX, username)))
7298
.exchange { _, response ->
7399
assertIsSuccess(response)
74100

75-
response.bodyTo(ContributionYearQueryResponse::class.java)!!
101+
val data = response.bodyTo(ContributionYearQueryResponse::class.java)!!
76102
.data
77-
.user
103+
104+
rateLimiter.update(
105+
RateLimitable.RateLimit(
106+
limit = data.rateLimit.limit,
107+
remaining = data.rateLimit.remaining,
108+
resetAt = data.rateLimit.resetAt,
109+
used = data.rateLimit.used,
110+
)
111+
)
112+
113+
return@exchange data.user
78114
.contributionsCollection
79115
.contributionYears
80116
}
@@ -91,7 +127,10 @@ class GithubContributionApi(
91127
}
92128

93129
private class ContributionYearQueryResponse(val data: Data) {
94-
class Data(val user: User) {
130+
class Data(
131+
val rateLimit: RateLimit,
132+
val user: User,
133+
) {
95134
class User(val contributionsCollection: ContributionsCollection) {
96135
class ContributionsCollection(
97136
val contributionYears: List<Int>,
@@ -101,7 +140,11 @@ class GithubContributionApi(
101140
}
102141

103142
private class ContributionCountByYearQueryResponse(val data: Data) {
104-
class Data(val user: User) {
143+
class Data(
144+
val rateLimit: RateLimit,
145+
val user: User,
146+
) {
147+
105148
class User(val contributionsCollection: ContributionsCollection) {
106149
class ContributionsCollection(
107150
val contributionCalendar: ContributionCalendar,
@@ -115,6 +158,14 @@ class GithubContributionApi(
115158
}
116159
}
117160

161+
class RateLimit(
162+
val limit: Int,
163+
val cost: Int,
164+
val remaining: Int,
165+
val resetAt: LocalDateTime,
166+
val used: Int,
167+
)
168+
118169
companion object {
119170
private const val NAME_FIX = "*{name}"
120171
private const val YEAR_FIX = "*{year}"

src/main/kotlin/org/gitanimals/render/infra/VisitedEventListener.kt

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import org.rooftop.netx.meta.SagaHandler
1111
import org.slf4j.LoggerFactory
1212
import org.slf4j.MDC
1313
import org.springframework.context.event.EventListener
14+
import org.springframework.dao.CannotAcquireLockException
1415
import org.springframework.scheduling.annotation.Async
1516
import java.time.ZoneId
1617
import java.time.ZonedDateTime
@@ -32,9 +33,9 @@ class VisitedEventListener(
3233
val username = visited.username
3334
userService.increaseVisit(username)
3435

35-
logger.info("Increase visit to user. username: \"$username\"")
36+
logger.info("[VisitedEventListener] Increase visit to user. username: \"$username\"")
3637

37-
if (!userService.isContributionUpdatedBeforeOneHour(username)) {
38+
if (userService.isContributionUpdatedLongAgo(username).not()) {
3839
return
3940
}
4041

@@ -49,13 +50,16 @@ class VisitedEventListener(
4950
point = increaseContributionCount * 100,
5051
idempotencyKey = IdGenerator.generate().toString(),
5152
)
52-
logger.info("Increase point to user. username: \"$username\", point:\"${increaseContributionCount * 100}\"")
53+
logger.info("[VisitedEventListener] Increase point to user. username: \"$username\", point:\"${increaseContributionCount * 100}\"")
5354
}.onFailure {
5455
if (it !is IllegalArgumentException) {
5556
logger.error(
56-
"Cannot increase visit or point to user. username: \"${visited.username}\"", it
57+
"[VisitedEventListener] Cannot increase visit or point to user. username: \"${visited.username}\"", it
5758
)
5859
}
60+
if (it !is CannotAcquireLockException) {
61+
logger.warn("[VisitedEventListener] Deadlock found.", it)
62+
}
5963
}.also {
6064
MDC.remove(TRACE_ID)
6165
}

0 commit comments

Comments
 (0)