Skip to content

Commit 2409d2f

Browse files
CopilotYiiGuxing
andcommitted
Add tryAcquire with timeout functionality to RateLimiter
Co-authored-by: YiiGuxing <[email protected]>
1 parent c467bd0 commit 2409d2f

File tree

2 files changed

+130
-0
lines changed

2 files changed

+130
-0
lines changed

src/main/kotlin/cn/yiiguxing/plugin/translate/util/RateLimiter.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cn.yiiguxing.plugin.translate.util
33
import kotlinx.coroutines.delay
44
import kotlinx.coroutines.sync.Mutex
55
import kotlinx.coroutines.sync.withLock
6+
import kotlinx.coroutines.withTimeoutOrNull
67
import kotlin.time.Duration
78
import kotlin.time.Duration.Companion.milliseconds
89
import kotlin.time.Duration.Companion.seconds
@@ -18,6 +19,16 @@ interface RateLimiter {
1819
*/
1920
suspend fun acquire()
2021

22+
/**
23+
* Attempts to acquire permission to proceed within the specified [timeout].
24+
* If the rate limit has been reached, this function will suspend until either
25+
* permission is granted or the timeout expires.
26+
*
27+
* @param timeout The maximum time to wait for permission.
28+
* @return `true` if permission was granted within the timeout, `false` otherwise.
29+
*/
30+
suspend fun tryAcquire(timeout: Duration): Boolean
31+
2132
companion object {
2233
private val NONE = NoneRateLimiter()
2334

@@ -71,8 +82,16 @@ private class RateLimiterImpl(private val interval: Long) : RateLimiter {
7182
lastCallTime = System.currentTimeMillis()
7283
}
7384
}
85+
86+
override suspend fun tryAcquire(timeout: Duration): Boolean {
87+
return withTimeoutOrNull(timeout) {
88+
acquire()
89+
true
90+
} ?: false
91+
}
7492
}
7593

7694
private class NoneRateLimiter : RateLimiter {
7795
override suspend fun acquire() = Unit
96+
override suspend fun tryAcquire(timeout: Duration) = true
7897
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package cn.yiiguxing.plugin.translate.util
2+
3+
import kotlinx.coroutines.runBlocking
4+
import org.junit.Assert
5+
import org.junit.Test
6+
import kotlin.time.Duration.Companion.milliseconds
7+
import kotlin.time.Duration.Companion.seconds
8+
9+
/**
10+
* RateLimiterTest
11+
*/
12+
class RateLimiterTest {
13+
14+
@Test
15+
fun testTryAcquireWithinTimeout() = runBlocking {
16+
val rateLimiter = RateLimiter.withInterval(100.milliseconds)
17+
18+
// First acquire should succeed immediately
19+
val result1 = rateLimiter.tryAcquire(1.seconds)
20+
Assert.assertTrue("First acquire should succeed", result1)
21+
22+
// Second acquire should succeed because we give it enough timeout
23+
val result2 = rateLimiter.tryAcquire(200.milliseconds)
24+
Assert.assertTrue("Second acquire with sufficient timeout should succeed", result2)
25+
}
26+
27+
@Test
28+
fun testTryAcquireTimeout() = runBlocking {
29+
val rateLimiter = RateLimiter.withInterval(500.milliseconds)
30+
31+
// First acquire should succeed immediately
32+
val result1 = rateLimiter.tryAcquire(1.seconds)
33+
Assert.assertTrue("First acquire should succeed", result1)
34+
35+
// Second acquire with insufficient timeout should fail
36+
val result2 = rateLimiter.tryAcquire(100.milliseconds)
37+
Assert.assertFalse("Second acquire with insufficient timeout should fail", result2)
38+
}
39+
40+
@Test
41+
fun testTryAcquireMultipleTimes() = runBlocking {
42+
val rateLimiter = RateLimiter.withInterval(50.milliseconds)
43+
44+
// Acquire multiple times with sufficient timeout
45+
for (i in 1..3) {
46+
val result = rateLimiter.tryAcquire(200.milliseconds)
47+
Assert.assertTrue("Acquire #$i should succeed", result)
48+
}
49+
}
50+
51+
@Test
52+
fun testTryAcquireWithZeroInterval() = runBlocking {
53+
val rateLimiter = RateLimiter.withInterval(0.milliseconds)
54+
55+
// NoneRateLimiter should always succeed immediately
56+
val result1 = rateLimiter.tryAcquire(1.milliseconds)
57+
Assert.assertTrue("First acquire should succeed", result1)
58+
59+
val result2 = rateLimiter.tryAcquire(1.milliseconds)
60+
Assert.assertTrue("Second acquire should succeed immediately", result2)
61+
62+
val result3 = rateLimiter.tryAcquire(1.milliseconds)
63+
Assert.assertTrue("Third acquire should succeed immediately", result3)
64+
}
65+
66+
@Test
67+
fun testTryAcquireWithRate() = runBlocking {
68+
val rateLimiter = RateLimiter.withRate(2, 1.seconds) // 2 calls per second = 500ms interval
69+
70+
// First acquire should succeed
71+
val result1 = rateLimiter.tryAcquire(1.seconds)
72+
Assert.assertTrue("First acquire should succeed", result1)
73+
74+
// Second acquire with insufficient timeout should fail
75+
val result2 = rateLimiter.tryAcquire(100.milliseconds)
76+
Assert.assertFalse("Second acquire with 100ms timeout should fail (needs ~500ms)", result2)
77+
78+
// Third acquire with sufficient timeout should succeed
79+
val result3 = rateLimiter.tryAcquire(600.milliseconds)
80+
Assert.assertTrue("Third acquire with sufficient timeout should succeed", result3)
81+
}
82+
83+
@Test
84+
fun testAcquireStillWorks() = runBlocking {
85+
val rateLimiter = RateLimiter.withInterval(50.milliseconds)
86+
87+
// Test that the original acquire() method still works
88+
rateLimiter.acquire() // Should succeed immediately
89+
rateLimiter.acquire() // Should wait ~50ms
90+
rateLimiter.acquire() // Should wait ~50ms
91+
92+
// If we get here without timeout, the test passes
93+
Assert.assertTrue(true)
94+
}
95+
96+
@Test
97+
fun testMixedAcquireAndTryAcquire() = runBlocking {
98+
val rateLimiter = RateLimiter.withInterval(100.milliseconds)
99+
100+
// Use regular acquire
101+
rateLimiter.acquire()
102+
103+
// Try to acquire with insufficient timeout
104+
val result1 = rateLimiter.tryAcquire(50.milliseconds)
105+
Assert.assertFalse("tryAcquire after acquire should fail with short timeout", result1)
106+
107+
// Try to acquire with sufficient timeout
108+
val result2 = rateLimiter.tryAcquire(150.milliseconds)
109+
Assert.assertTrue("tryAcquire with sufficient timeout should succeed", result2)
110+
}
111+
}

0 commit comments

Comments
 (0)