11package org.gitanimals.render.infra
22
3+ import org.gitanimals.core.ratelimit.RateLimitable
34import org.gitanimals.render.app.ContributionApi
5+ import org.slf4j.LoggerFactory
6+ import org.springframework.beans.factory.annotation.Qualifier
47import org.springframework.beans.factory.annotation.Value
58import org.springframework.core.io.ClassPathResource
69import org.springframework.http.HttpHeaders
710import org.springframework.stereotype.Component
811import org.springframework.web.client.RestClient
912import java.nio.charset.Charset
13+ import java.time.LocalDateTime
1014import java.util.concurrent.CompletableFuture
1115import java.util.concurrent.Executors
1216
1317@Component
1418class 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}"
0 commit comments