Skip to content

Commit a884da4

Browse files
committed
fix: match cjit from cached state, refresh on miss
1 parent ae7baa7 commit a884da4

2 files changed

Lines changed: 59 additions & 12 deletions

File tree

app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -130,18 +130,34 @@ class BlocktankRepo @Inject constructor(
130130
suspend fun getCjitEntry(channel: ChannelDetails): IcJitEntry? = withContext(bgDispatcher) {
131131
val fundingTxId = channel.fundingTxo?.txid ?: return@withContext null
132132

133-
// Refresh from the server so a freshly opened CJIT channel association is up to date before matching.
134-
val entries = runCatching {
135-
withTimeout(CJIT_REFRESH_TIMEOUT) {
136-
coreService.blocktank.cjitEntries(refresh = true)
137-
}
138-
}.getOrElse {
139-
if (it is CancellationException && it !is TimeoutCancellationException) throw it
140-
Logger.warn("Failed to refresh CJIT entries; using cached state", it, context = TAG)
141-
_blocktankState.value.cjitEntries
133+
fun cachedMatch(): IcJitEntry? = _blocktankState.value.cjitEntries
134+
.firstOrNull { it.channel?.fundingTx?.id == fundingTxId }
135+
136+
// Use cached state first; only refresh from the server when the freshly opened channel isn't associated yet.
137+
return@withContext cachedMatch() ?: run {
138+
refreshCjitEntries()
139+
cachedMatch()
142140
}
141+
}
143142

144-
return@withContext entries.firstOrNull { it.channel?.fundingTx?.id == fundingTxId }
143+
private suspend fun refreshCjitEntries() {
144+
repeat(CJIT_REFRESH_ATTEMPTS) { attempt ->
145+
runCatching {
146+
withTimeout(CJIT_REFRESH_TIMEOUT) {
147+
coreService.blocktank.cjitEntries(refresh = true)
148+
}
149+
}.onSuccess { entries ->
150+
_blocktankState.update { it.copy(cjitEntries = entries.toImmutableList()) }
151+
return
152+
}.onFailure {
153+
if (it is CancellationException && it !is TimeoutCancellationException) throw it
154+
if (attempt == CJIT_REFRESH_ATTEMPTS - 1) {
155+
Logger.warn("Failed to refresh CJIT entries; using cached state", it, context = TAG)
156+
return
157+
}
158+
}
159+
delay(CJIT_REFRESH_RETRY_DELAY)
160+
}
145161
}
146162

147163
suspend fun refreshInfo() = withContext(bgDispatcher) {
@@ -566,7 +582,9 @@ class BlocktankRepo @Inject constructor(
566582
private const val PEER_CONNECTION_DELAY_MS = 2_000L
567583
private val TIMEOUT_GIFT_CODE = 30.seconds
568584
private val GIFT_PAYMENT_RECEIVE_TIMEOUT = 45.seconds
585+
private const val CJIT_REFRESH_ATTEMPTS = 3
569586
private val CJIT_REFRESH_TIMEOUT = 5.seconds
587+
private val CJIT_REFRESH_RETRY_DELAY = 1.seconds
570588
}
571589
}
572590

app/src/test/java/to/bitkit/repositories/BlocktankRepoTest.kt

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ class BlocktankRepoTest : BaseUnitTest() {
259259
}
260260

261261
@Test
262-
fun `getCjitEntry falls back to cached entries when refresh fails`() = test {
262+
fun `getCjitEntry returns cached entry without refreshing when already associated`() = test {
263263
sut = createSut()
264264
val fundingTxId = "cached-funding-tx"
265265
val channel = mock<IBtChannel>()
@@ -271,11 +271,40 @@ class BlocktankRepoTest : BaseUnitTest() {
271271
BlocktankBackupV1(createdAt = 0L, orders = emptyList(), cjitEntries = listOf(cachedEntry)),
272272
)
273273

274+
val channelDetails = mock<ChannelDetails>()
275+
whenever(channelDetails.fundingTxo).thenReturn(OutPoint(txid = fundingTxId, vout = 0u))
276+
277+
// A server refresh would return no entries (setUp default), so a non-null result can only come
278+
// from the cached state short-circuit, proving the server is not hit when the entry is already known.
279+
assertEquals(cachedEntry, sut.getCjitEntry(channelDetails))
280+
}
281+
282+
@Test
283+
fun `getCjitEntry returns null when cache misses and refresh fails`() = test {
284+
sut = createSut()
274285
whenever(coreService.blocktank.cjitEntries(refresh = true)).thenThrow(RuntimeException("Network error"))
275286

287+
val channelDetails = mock<ChannelDetails>()
288+
whenever(channelDetails.fundingTxo).thenReturn(OutPoint(txid = "missing-funding-tx", vout = 0u))
289+
290+
assertNull(sut.getCjitEntry(channelDetails))
291+
}
292+
293+
@Test
294+
fun `getCjitEntry retries the refresh and matches after a transient failure`() = test {
295+
sut = createSut()
296+
val fundingTxId = "cjit-funding-tx"
297+
val channel = mock<IBtChannel>()
298+
whenever(channel.fundingTx).thenReturn(FundingTx(id = fundingTxId, vout = 0u))
299+
val entry = mock<IcJitEntry>()
300+
whenever(entry.channel).thenReturn(channel)
301+
whenever(coreService.blocktank.cjitEntries(refresh = true))
302+
.thenThrow(RuntimeException("transient"))
303+
.thenReturn(listOf(entry))
304+
276305
val channelDetails = mock<ChannelDetails>()
277306
whenever(channelDetails.fundingTxo).thenReturn(OutPoint(txid = fundingTxId, vout = 0u))
278307

279-
assertEquals(cachedEntry, sut.getCjitEntry(channelDetails))
308+
assertEquals(entry, sut.getCjitEntry(channelDetails))
280309
}
281310
}

0 commit comments

Comments
 (0)