Skip to content

Commit

Permalink
Merge branch 'master' into dependabot/gradle/io.github.gradle-nexus.p…
Browse files Browse the repository at this point in the history
…ublish-plugin-1.3.0
  • Loading branch information
lpicanco committed Jan 16, 2024
2 parents 6b4fe5b + fa8b42d commit 83449aa
Show file tree
Hide file tree
Showing 22 changed files with 391 additions and 342 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: build
on:
push:
branches: [ "master" ]
pull_request:
pull_request_target:
branches: [ "master" ]

permissions:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Krate
Krate is a rate limiter library for Kotlin.

[![Kotlin](https://img.shields.io/badge/kotlin-1.8.0-blue.svg?logo=kotlin)](http://kotlinlang.org)
[![Kotlin](https://img.shields.io/badge/kotlin-1.9.21-blue.svg?logo=kotlin)](http://kotlinlang.org)
![GitHub](https://img.shields.io/github/license/lpicanco/krate)
![Build Status](https://img.shields.io/github/actions/workflow/status/lpicanco/krate/jdk11.yml?branch=master)
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=lpicanco_krate&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=lpicanco_krate)
Expand Down
4 changes: 2 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
kotlin("jvm") version "1.9.21"
id("org.jlleitschuh.gradle.ktlint") version "10.3.0"
id("org.jlleitschuh.gradle.ktlint") version "12.0.3"
id("org.sonarqube") version "4.4.1.3373"
jacoco
id("maven-publish")
Expand Down Expand Up @@ -52,7 +52,7 @@ subprojects {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
testImplementation("io.mockk:mockk:1.13.8")
testImplementation("io.mockk:mockk:1.13.9")
testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.1")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.1")
}
Expand Down
1 change: 1 addition & 0 deletions krate-core/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
//
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ import kotlin.math.roundToLong
* @param maxRate the maximum rate at which tokens can be consumed
*/
class RateLimiterBuilder(private val maxRate: Long) {

/**
* The maximum number of tokens that can be stored in the bucket. Defaults to [maxRate].
*/
Expand Down Expand Up @@ -63,7 +62,7 @@ class RateLimiterBuilder(private val maxRate: Long) {
capacity = maxBurst,
refillTokenInterval = Duration.ofMillis(refillTokenIntervalInMillis.roundToLong()),
stateStorage = stateStorage,
clock = clock
clock = clock,
)
}
}
Expand All @@ -86,6 +85,9 @@ class RateLimiterBuilder(private val maxRate: Long) {
* }
* }
*/
fun rateLimiter(maxRate: Long, init: RateLimiterBuilder.() -> Unit = {}): RateLimiter {
fun rateLimiter(
maxRate: Long,
init: RateLimiterBuilder.() -> Unit = {},
): RateLimiter {
return RateLimiterBuilder(maxRate).apply(init).build()
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class TokenBucketLimiter(
val capacity: Long,
val refillTokenInterval: Duration,
private val clock: Clock,
private val stateStorage: StateStorage = MemoryStateStorage()
private val stateStorage: StateStorage = MemoryStateStorage(),
) : RateLimiter {
override suspend fun tryTake(): Boolean {
return tryTakeFromState(null)
Expand Down Expand Up @@ -80,7 +80,7 @@ class TokenBucketLimiter(

current.copy(
remainingTokens = max(0, totalTokens - 1),
lastUpdated = lastUpdated
lastUpdated = lastUpdated,
)
}
}
Expand All @@ -100,5 +100,5 @@ class TokenBucketLimiter(
*/
data class BucketState(
val remainingTokens: Long,
val lastUpdated: Instant
val lastUpdated: Instant,
)
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import kotlin.time.toKotlinDuration
* A state storage is a component that allows to store the state of a rate limiter.
*/
interface StateStorage {

/**
* Get the state of the bucket for the given [key].
* @param key the key to use to get the state
Expand All @@ -43,10 +42,12 @@ interface StateStorage {
* @param key the key to use to set the state
* @param compareAndSetFunction a Compare-And-Set function that takes the current state of the bucket and returns the new state
*/
suspend fun compareAndSet(key: String, compareAndSetFunction: (current: BucketState?) -> BucketState)
suspend fun compareAndSet(
key: String,
compareAndSetFunction: (current: BucketState?) -> BucketState,
)

companion object {

// Default expiration check interval
val DEFAULT_RETRY_DELAY = Duration.ofMillis(100L).toKotlinDuration()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import java.util.concurrent.atomic.AtomicReference
* Interface the provides a map implementation to store the state of the rate limiter.
*/
interface BucketStateMap {

/**
* Returns the [AtomicReference] of the [BucketState] for the given [key].
* If the [key] is not present in the map, it returns `null`.
Expand All @@ -40,5 +39,8 @@ interface BucketStateMap {
* If the [key] is already present, it returns the [AtomicReference] of the [BucketState] for the given [key].
* If the [key] is not present in the map, it returns `null`.
*/
fun putIfAbsent(key: String, value: AtomicReference<BucketState>): AtomicReference<BucketState>?
fun putIfAbsent(
key: String,
value: AtomicReference<BucketState>,
): AtomicReference<BucketState>?
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,16 @@ import java.util.concurrent.atomic.AtomicReference
* @param bucketStateMap The [BucketStateMap] to use to store the state.
*/
class MemoryStateStorage(
private val bucketStateMap: BucketStateMap = SimpleBucketStateMap()
private val bucketStateMap: BucketStateMap = SimpleBucketStateMap(),
) : StateStorage {
override fun getBucketState(key: String): BucketState? {
return bucketStateMap.getBucketStateReference(key)?.get()
}

override suspend fun compareAndSet(key: String, compareAndSetFunction: (current: BucketState?) -> BucketState) {
override suspend fun compareAndSet(
key: String,
compareAndSetFunction: (current: BucketState?) -> BucketState,
) {
val currentState = bucketStateMap.getBucketStateReference(key)
val currentStateValue = currentState?.get()
val newStateValue = compareAndSetFunction(currentStateValue)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ class MemoryStateStorageBuilder {
* A builder for [MemoryStateStorage] instances with eviction.
*/
class MemoryStateStorageWithEvictionBuilder {

/**
* The time-to-live after the last access.
*/
Expand All @@ -66,15 +65,16 @@ class MemoryStateStorageWithEvictionBuilder {
*/
var coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Default)

fun build() = MemoryStateStorage(
SimpleBucketStateMapWithEviction(
clock,
bucketStateMap,
ttlAfterLastAccess,
expirationCheckInterval,
coroutineScope
fun build() =
MemoryStateStorage(
SimpleBucketStateMapWithEviction(
clock,
bucketStateMap,
ttlAfterLastAccess,
expirationCheckInterval,
coroutineScope,
),
)
)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,16 @@ import java.util.concurrent.atomic.AtomicReference
* This implementation is thread-safe and uses [ConcurrentHashMap] to store the state.
*/
class SimpleBucketStateMap : BucketStateMap {

internal val state: ConcurrentHashMap<String, AtomicReference<BucketState>> = ConcurrentHashMap()

override fun getBucketStateReference(key: String): AtomicReference<BucketState>? {
return state[key]
}

override fun putIfAbsent(key: String, value: AtomicReference<BucketState>): AtomicReference<BucketState>? {
override fun putIfAbsent(
key: String,
value: AtomicReference<BucketState>,
): AtomicReference<BucketState>? {
return state.putIfAbsent(key, value)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,8 @@ class SimpleBucketStateMapWithEviction(
private val stateMap: SimpleBucketStateMap = SimpleBucketStateMap(),
private val ttlAfterLastAccess: Duration = 2.hours,
expirationCheckInterval: Duration = 10.minutes,
coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Default)
coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Default),
) : BucketStateMap by stateMap {

init {
coroutineScope.launch {
while (true) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ import java.time.Duration
import java.time.temporal.ChronoUnit

internal class RateLimiterBuilderTest {

@Test
fun `should return an instance of TokenBucketLimiter with a rate of 60 per minute`() {
val rateLimiter = rateLimiter(maxRate = 60) {
maxBurst = 70
maxRateTimeUnit = ChronoUnit.MINUTES
}
val rateLimiter =
rateLimiter(maxRate = 60) {
maxBurst = 70
maxRateTimeUnit = ChronoUnit.MINUTES
}

assertTrue(rateLimiter is TokenBucketLimiter)
val tokenBucketLimiter = rateLimiter as TokenBucketLimiter
Expand All @@ -29,10 +29,11 @@ internal class RateLimiterBuilderTest {

@Test
fun `should return an instance of TokenBucketLimiter with a rate of 5 per minute`() {
val rateLimiter = rateLimiter(maxRate = 5) {
maxBurst = 70
maxRateTimeUnit = ChronoUnit.MINUTES
}
val rateLimiter =
rateLimiter(maxRate = 5) {
maxBurst = 70
maxRateTimeUnit = ChronoUnit.MINUTES
}

assertTrue(rateLimiter is TokenBucketLimiter)
val tokenBucketLimiter = rateLimiter as TokenBucketLimiter
Expand All @@ -43,10 +44,11 @@ internal class RateLimiterBuilderTest {

@Test
fun `should return an instance of TokenBucketLimiter with a rate of 5 per second`() {
val rateLimiter = rateLimiter(maxRate = 5) {
maxBurst = 10
maxRateTimeUnit = ChronoUnit.SECONDS
}
val rateLimiter =
rateLimiter(maxRate = 5) {
maxBurst = 10
maxRateTimeUnit = ChronoUnit.SECONDS
}

assertTrue(rateLimiter is TokenBucketLimiter)
val tokenBucketLimiter = rateLimiter as TokenBucketLimiter
Expand All @@ -56,11 +58,12 @@ internal class RateLimiterBuilderTest {
}

@Test
fun `should return an instance of TokenBucketLimiter a rate of 30 per second`() {
val rateLimiter = rateLimiter(maxRate = 30) {
maxBurst = 50
maxRateTimeUnit = ChronoUnit.SECONDS
}
fun `should return an instance of TokenBucketLimiter with a rate of 30 per second`() {
val rateLimiter =
rateLimiter(maxRate = 30) {
maxBurst = 50
maxRateTimeUnit = ChronoUnit.SECONDS
}

assertTrue(rateLimiter is TokenBucketLimiter)
val tokenBucketLimiter = rateLimiter as TokenBucketLimiter
Expand All @@ -70,11 +73,12 @@ internal class RateLimiterBuilderTest {
}

@Test
fun `should return an instance of TokenBucketLimiter a rate of 5 per hour`() {
val rateLimiter = rateLimiter(maxRate = 5) {
maxBurst = 5
maxRateTimeUnit = ChronoUnit.HOURS
}
fun `should return an instance of TokenBucketLimiter with a rate of 5 per hour`() {
val rateLimiter =
rateLimiter(maxRate = 5) {
maxBurst = 5
maxRateTimeUnit = ChronoUnit.HOURS
}

assertTrue(rateLimiter is TokenBucketLimiter)
val tokenBucketLimiter = rateLimiter as TokenBucketLimiter
Expand All @@ -84,16 +88,18 @@ internal class RateLimiterBuilderTest {
}

@Test
fun `should return an instance of TokenBucketLimiter with custom state storage`() = runTest {
val customStateStorage = mockk<StateStorage>(relaxed = true)
val rateLimiter = rateLimiter(maxRate = 5) {
stateStorage = customStateStorage
fun `should return an instance of TokenBucketLimiter with custom state storage`() =
runTest {
val customStateStorage = mockk<StateStorage>(relaxed = true)
val rateLimiter =
rateLimiter(maxRate = 5) {
stateStorage = customStateStorage
}

assertTrue(rateLimiter is TokenBucketLimiter)
val tokenBucketLimiter = rateLimiter as TokenBucketLimiter

tokenBucketLimiter.tryTake("42")
coVerify { customStateStorage.compareAndSet("42", any()) }
}

assertTrue(rateLimiter is TokenBucketLimiter)
val tokenBucketLimiter = rateLimiter as TokenBucketLimiter

tokenBucketLimiter.tryTake("42")
coVerify { customStateStorage.compareAndSet("42", any()) }
}
}
Loading

0 comments on commit 83449aa

Please sign in to comment.