diff --git a/build.gradle b/build.gradle index 911c623..a0d7c45 100644 --- a/build.gradle +++ b/build.gradle @@ -55,6 +55,7 @@ dependencies { implementation("io.springfox:springfox-boot-starter:3.0.0") implementation('io.springfox:springfox-swagger-ui:3.0.0') implementation("org.eclipse.jgit:org.eclipse.jgit:6.3.0.202209071007-r") + implementation("com.github.vladimir-bukhtoyarov:bucket4j-core:7.6.0") // Foundation implementation('org.webjars:foundation:6.4.3') diff --git a/src/main/kotlin/de/paulbrejla/holidays/application/api/RateLimitService.kt b/src/main/kotlin/de/paulbrejla/holidays/application/api/RateLimitService.kt new file mode 100644 index 0000000..644cb78 --- /dev/null +++ b/src/main/kotlin/de/paulbrejla/holidays/application/api/RateLimitService.kt @@ -0,0 +1,9 @@ +package de.paulbrejla.holidays.application.api + +import io.github.bucket4j.Bucket + +interface RateLimitService { + fun resolveBucket(bucketId: String): Bucket + fun fetchBucket(bucketId: String): Bucket? + fun isWithinQuota(maxRequests: Int, currentRequests: Int): Boolean +} \ No newline at end of file diff --git a/src/main/kotlin/de/paulbrejla/holidays/application/impl/GlobalRateLimitServiceImpl.kt b/src/main/kotlin/de/paulbrejla/holidays/application/impl/GlobalRateLimitServiceImpl.kt new file mode 100644 index 0000000..25cec8a --- /dev/null +++ b/src/main/kotlin/de/paulbrejla/holidays/application/impl/GlobalRateLimitServiceImpl.kt @@ -0,0 +1,56 @@ +package de.paulbrejla.holidays.application.impl + +import de.paulbrejla.holidays.application.api.RateLimitService +import io.github.bucket4j.Bandwidth +import io.github.bucket4j.Bucket +import io.github.bucket4j.Refill +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component +import java.time.Duration +import java.util.concurrent.ConcurrentHashMap + +@Component("rateLimitService") +class GlobalRateLimitServiceImpl : RateLimitService { + // For now we create one global bucket that all requests consume from. + @Value("\${rateLimit.globalBucket.id}") + var globalBucketId: String = "" + + @Value("\${rateLimit.globalBucket.capacity}") + var globalBucketCapacity: Long = 0 + + + private var buckets: MutableMap = ConcurrentHashMap() + override fun resolveBucket(bucketId: String): Bucket { + return if (buckets.containsKey(bucketId)) { + buckets[bucketId]!! + } else { + createBucket().let { + buckets[bucketId] = it + it + } + } + } + + override fun fetchBucket(bucketId: String): Bucket? { + return buckets[bucketId] + } + + override fun isWithinQuota(maxRequests: Int, currentRequests: Int): Boolean { + TODO("Not yet implemented") + } + + /** + * We only allow 1000 requests per hour from now on. These 1000 requests are refilled when the new + * hour starts. + */ + private fun createBucket(): Bucket { + return Bucket.builder() + .addLimit( + Bandwidth.classic( + globalBucketCapacity, + Refill.intervally(globalBucketCapacity, Duration.ofHours(1)) + ) + ) + .build() + } +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 95e7bcc..092614e 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -9,10 +9,14 @@ spring: enabled: false path: /h2-console server: - port: ${APPLICATION_PORT:80} + port: ${SERVER_PORT:80} loader: source: ${LOADER_SOURCE} remoteURL: ${LOADER_REMOTE_URL} branch: ${LOADER_BRANCH} filePath: ${LOADER_FILE_PATH} - authToken: ${LOADER_AUTH_TOKEN} \ No newline at end of file + authToken: ${LOADER_AUTH_TOKEN} +rateLimit: + globalBucket: + id: ${RATE_LIMIT_GLOBAL_BUCKET_ID:global} + capacity: ${RATE_LIMIT_GLOBAL_BUCKET_CAPACITY:500}