Skip to content

Commit 3d70116

Browse files
committed
Enhance KrescentLockProviderContract tests for lock behavior and timeout handling
1 parent c550deb commit 3d70116

File tree

3 files changed

+112
-38
lines changed

3 files changed

+112
-38
lines changed

krescent-redisson/src/main/kotlin/dev/helight/krescent/redisson/RedisLockProvider.kt

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import kotlinx.coroutines.future.asDeferred
66
import kotlinx.coroutines.withTimeout
77
import org.redisson.api.RLock
88
import org.redisson.api.RedissonClient
9+
import java.util.*
910
import java.util.concurrent.TimeUnit
1011
import java.util.concurrent.atomic.AtomicLong
1112
import kotlin.time.Duration
@@ -20,22 +21,23 @@ class RedisLockProvider(
2021

2122
override suspend fun getLock(identity: String): KrescentLock {
2223
val rlock = client.getLock(prefix + identity)
23-
return RedisLockImpl(rlock, lockIdCounter.getAndIncrement())
24+
return RedisLockImpl(rlock)
2425
}
2526

2627
override suspend fun getMultiLock(identities: Collection<String>): KrescentLock {
2728
val lockList = identities.map { identity ->
2829
client.getLock(prefix + identity)
2930
}.toTypedArray()
3031
val multiLock = client.getMultiLock(*lockList)
31-
return RedisLockImpl(multiLock, lockIdCounter.getAndIncrement())
32+
return RedisLockImpl(multiLock)
3233
}
3334

3435
private inner class RedisLockImpl(
3536
val lock: RLock,
36-
val lockId: Long,
3737
) : KrescentLock {
3838

39+
private val claims: Stack<Long> = Stack()
40+
3941
override suspend fun acquire(timeout: Duration?) {
4042
if (timeout != null) {
4143
withTimeout(timeout) { acquire() }
@@ -45,27 +47,20 @@ class RedisLockProvider(
4547
}
4648

4749
private suspend fun acquire() {
50+
val claimId = lockIdCounter.getAndIncrement()
4851
lock.lockAsync(
4952
when {
5053
leaseTime.isInfinite() -> -1L
5154
else -> leaseTime.inWholeMilliseconds
52-
}, TimeUnit.MILLISECONDS, lockId
55+
}, TimeUnit.MILLISECONDS, claimId
5356
).asDeferred().await()
57+
claims.push(claimId)
5458
}
5559

5660
override suspend fun release() {
57-
lock.unlockAsync(lockId).asDeferred().await()
58-
}
59-
60-
override fun equals(other: Any?): Boolean {
61-
if (this === other) return true
62-
if (javaClass != other?.javaClass) return false
63-
other as RedisLockImpl
64-
return lockId == other.lockId
65-
}
66-
67-
override fun hashCode(): Int {
68-
return lockId.hashCode()
61+
if (claims.isEmpty()) return
62+
val claimId = claims.pop()
63+
lock.unlockAsync(claimId).asDeferred().await()
6964
}
7065
}
7166
}

krescent-redisson/src/test/kotlin/dev/helight/krescent/redisson/RedisLockProvider.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ class RedisLockProviderTest : KrescentLockProviderContract {
2828
})
2929
}
3030

31+
override val latency: Long
32+
get() = 100L
33+
3134
override fun getProvider(): KrescentLockProvider {
3235
val db = connect()
3336
val uid = UUID.randomUUID().toString()

krescent-test/src/main/kotlin/dev/helight/krescent/test/KrescentLockProviderContract.kt

Lines changed: 98 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,71 @@ package dev.helight.krescent.test
22

33
import dev.helight.krescent.synchronization.KrescentLockProvider
44
import dev.helight.krescent.synchronization.KrescentLockProvider.Extensions.getMultiLock
5-
import dev.helight.krescent.synchronization.LocalLockProvider
6-
import kotlinx.coroutines.async
7-
import kotlinx.coroutines.awaitAll
8-
import kotlinx.coroutines.delay
9-
import kotlinx.coroutines.runBlocking
5+
import kotlinx.coroutines.*
6+
import org.junit.jupiter.api.Assertions.assertEquals
7+
import java.util.concurrent.atomic.AtomicInteger
108
import kotlin.test.Test
119
import kotlin.test.assertTrue
10+
import kotlin.time.DurationUnit
11+
import kotlin.time.toDuration
1212

1313
interface KrescentLockProviderContract {
1414

1515
fun getProvider(): KrescentLockProvider
16+
val latency: Long
17+
get() = 50L
1618

1719
@Test
1820
fun `Acquired locks work as expected`() = runBlocking {
1921
val provider = getProvider()
20-
val timeStart = System.currentTimeMillis()
22+
System.currentTimeMillis()
23+
val resource = LockResource()
2124
listOf(
2225
async {
2326
val lock = provider.getLock("test-identity")
2427
lock.runGuarded {
25-
delay(50)
28+
resource.claim()
29+
delay(latency)
30+
resource.release()
2631
}
2732
},
2833
async {
2934
val lock = provider.getLock("test-identity")
3035
lock.runGuarded {
31-
delay(50)
36+
resource.claim()
37+
delay(latency)
38+
resource.release()
3239
}
3340
}
3441
).awaitAll()
35-
val timeAfter = System.currentTimeMillis()
36-
assertTrue(timeAfter - timeStart >= 95)
42+
return@runBlocking
3743
}
3844

45+
@Test
46+
fun `Reused lock instance still works and is not reentrant`() = runBlocking {
47+
val provider = getProvider()
48+
val resource = LockResource()
49+
val lock = provider.getLock("test-identity")
50+
listOf(
51+
async {
52+
lock.runGuarded {
53+
resource.claim()
54+
delay(latency)
55+
resource.release()
56+
}
57+
},
58+
async {
59+
lock.runGuarded {
60+
resource.claim()
61+
delay(latency)
62+
resource.release()
63+
}
64+
}
65+
).awaitAll()
66+
return@runBlocking
67+
}
68+
69+
3970
@Test
4071
fun `Locks do not conflict with each other`() = runBlocking {
4172
val provider = getProvider()
@@ -44,51 +75,96 @@ interface KrescentLockProviderContract {
4475
async {
4576
val lock = provider.getLock("test-identity")
4677
lock.runGuarded {
47-
delay(50)
78+
delay(latency)
4879
}
4980
},
5081
async {
5182
val lock = provider.getLock("another-identity")
5283
lock.runGuarded {
53-
delay(50)
84+
delay(latency)
5485
}
55-
}
86+
},
5687
).awaitAll()
5788
val timeAfter = System.currentTimeMillis()
58-
assertTrue(timeAfter - timeStart < 95)
89+
assertTrue(timeAfter - timeStart < latency * 2)
90+
}
91+
92+
@Test
93+
fun `Test lock timeouts`() = runBlocking {
94+
val provider = getProvider()
95+
val lock = provider.getLock("timeout-identity")
96+
lock.acquire()
97+
var timeoutThrown = false
98+
withTimeout(1000L) {
99+
try {
100+
lock.acquire(latency.toDuration(DurationUnit.MILLISECONDS))
101+
} catch (e: Exception) {
102+
timeoutThrown = true
103+
}
104+
println("Acquired lock after timeout period")
105+
}
106+
assertTrue(timeoutThrown, "Timeout was not thrown when expected")
59107
}
60108

61109
@Test
62110
fun `Multilocks work and do not overblock`() = runBlocking {
63-
val provider = LocalLockProvider()
64-
val timeStart = System.currentTimeMillis()
111+
val provider = getProvider()
112+
val aRes = LockResource()
113+
val bRes = LockResource()
114+
val cRes = LockResource()
115+
val dRes = LockResource()
65116
listOf(
66117
async {
67118
val lock = provider.getMultiLock("a", "b")
68119
lock.runGuarded {
69-
delay(50)
120+
aRes.claim()
121+
bRes.claim()
122+
delay(latency)
123+
bRes.release()
124+
aRes.release()
70125
}
71126
},
72127
async {
73128
val lock = provider.getMultiLock("a", "c")
74129
lock.runGuarded {
75-
delay(50)
130+
aRes.claim()
131+
cRes.claim()
132+
delay(latency)
133+
cRes.release()
134+
aRes.release()
76135
}
77136
},
78137
async {
79138
val lock = provider.getMultiLock("d", "b")
80139
lock.runGuarded {
81-
delay(50)
140+
dRes.claim()
141+
bRes.claim()
142+
delay(latency)
143+
bRes.release()
144+
dRes.release()
82145
}
83146
},
84147
async {
85148
val lock = provider.getMultiLock("x", "y")
86149
lock.runGuarded {
87-
delay(50)
150+
delay(latency)
88151
}
89152
},
90153
).awaitAll()
91-
val timeAfter = System.currentTimeMillis()
92-
assertTrue(timeAfter - timeStart >= 95 && timeAfter - timeStart <= 150)
154+
return@runBlocking
155+
}
156+
157+
class LockResource {
158+
private val atomic: AtomicInteger = AtomicInteger(1)
159+
160+
fun claim() {
161+
val v = atomic.decrementAndGet()
162+
assertEquals(v, 0, "Resource is already claimed!")
163+
}
164+
165+
fun release() {
166+
val v = atomic.incrementAndGet()
167+
assertEquals(v, 1, "Resource was not claimed!")
168+
}
93169
}
94170
}

0 commit comments

Comments
 (0)