Tech
08/07/2024, 4:08 AMTech
08/08/2024, 4:31 AMprivate class RedisRateLimiter(
private val key: String,
private val limit: Int,
private val refillPeriod: Duration,
private val clock: () -> Long = ::getTimeMillis,
): RateLimiter, KoinComponent {
private val jedis by inject<JedisPool>()
override suspend fun tryConsume(tokens: Int): RateLimiter.State {
val k = buildKey(key)
jedis.resource.use { j ->
val current = j.get(k)
?.toIntOrNull()
?: 0
val next = current + tokens
val now = clock()
val refillTime = j.get("$k:refill")
?.toLongOrNull()
?: now
if (now >= refillTime) {
j.setex(k, refillPeriod.inWholeSeconds, tokens.toString())
j.setex("$k:refill", refillPeriod.inWholeSeconds, nextRefillTime(now, refillTime).toString())
return RateLimiter.State.Available(
remainingTokens = limit - tokens,
limit = limit,
refillAtTimeMillis = now + refillPeriod.inWholeMilliseconds
)
}
if (next <= limit) {
j.setex(k, refillPeriod.inWholeSeconds, next.toString())
return RateLimiter.State.Available(
remainingTokens = limit - next,
limit = limit,
refillAtTimeMillis = refillTime
)
}
return RateLimiter.State.Exhausted(
toWait = (refillTime - now).toDuration(DurationUnit.MILLISECONDS)
)
}
}
private fun nextRefillTime(now: Long, refillTime: Long): Long {
return if (now >= refillTime) {
now + refillPeriod.inWholeMilliseconds
} else {
refillTime
}
}
private fun buildKey(key: String) = "$RATE_LIMIT_KEY:$key"
companion object {
private const val RATE_LIMIT_KEY = "service:rate_limit"
}
}
Obviously not the fanciest code but it seems to work.
Then when I register my ratelimiter I see the request key to the identifier of the principal of the user if I know it'll be set in the route they're in.