sync: make queue reset publication recoverable

This commit is contained in:
austinried
2026-11-14 12:05:28 +00:00
parent 705c93053f
commit 544b442f4e
6 changed files with 237 additions and 78 deletions
@@ -474,6 +474,25 @@ class QueueRepositoryTest {
repository.removeAt(snapshot, 0)
assertEquals(listOf("s2", "s3"), resolveAll(repository.snapshot()))
fun aMatchingOriginStillPublishesWhenItsEntriesDoNotChange() =
runTest {
seedLibrary()
db.libraryDao().upsertSongs(listOf(song("s0", "al1", track = 0, album = "First Album")))
setPlaylist("p1", "s1", "s2", "s3")
repository.snapshotAfter(listOf(repository.playlistEntry(1, "p1")))
repository.setCursor(1)
val point = repository.captureResetPoint()
var resets = 0
repository.setResetListener {
resets++
QueueResetPreparation(it.snapshot)
}
setPlaylist("p1", "s0", "s1", "s2", "s3")
repository.resetChangedOrigins(setOf(QueueOrigin(QueueKind.Playlist, 1, "p1")), point)
assertEquals(1, resets)
assertEquals(2L, repository.cursor())
}
@Test
@@ -387,6 +387,42 @@ class SyncManagerTest {
assertTrue(db.queueDao().pendingSyncOrigins(1).isEmpty())
}
@Test
fun aPublicationFailureRecoversFromCommittedReset() =
runBlocking {
server.dispatcher = healthyDispatcher()
sourceRepository.addSource("Local", server.url("/").toString(), "u", "p", useTokenAuth = false)
queueRepository.replace(listOf(queueRepository.albumEntry(1, "al1").copy(rangeStart = 0, rangeEnd = 0)))
val before = queueRepository.snapshot()
val recoveries = AtomicInteger()
queueRepository.setResetListener { reset ->
QueueResetPreparation(
reset.snapshot,
publish = { throw IllegalStateException("publish failed") },
recover = { published ->
assertEquals(before.version + 1, published.version)
assertEquals(before.resetVersion + 1, published.resetVersion)
recoveries.incrementAndGet()
},
)
}
manager.requestSync()
withTimeout(TEST_TIMEOUT_MS) { manager.status.first { it is SyncStatus.Success } }
assertEquals(1, recoveries.get())
assertNull(
queueRepository
.snapshot()
.entries
.single()
.entry.rangeStart,
)
assertTrue(db.queueDao().pendingSyncOrigins(1).isEmpty())
assertEquals(before.version + 1, queueRepository.contentVersion())
assertEquals(before.resetVersion + 1, queueRepository.snapshot().resetVersion)
}
@Test
fun aQueueWriteFailureAfterPendingClearRollsBackWithoutPublishing() =
runBlocking {
@@ -48,6 +48,9 @@ class FakePlayerHandle : PlayerHandle {
@Volatile
var beforeRemoveFirst: (() -> Unit)? = null
@Volatile
var beforeCurrentItem: (() -> Unit)? = null
@Volatile
var roundTripItemsThroughMedia3 = false
@@ -86,6 +89,7 @@ class FakePlayerHandle : PlayerHandle {
override val currentItem: QueueItem?
get() {
beforeCurrentItem?.invoke()
checkThread()
return items.getOrNull(index)
}
@@ -3598,6 +3598,36 @@ class PlaybackControllerTest {
assertEquals(listOf("s0", "s1", "s2", "s3", "s4", "s5"), handle.items.map { it.id })
}
@Test
fun aTransitionBetweenResetPreparationAndPublicationRebuildsFromCommittedState() {
seedPlaylist(5)
seedSong("s0", "al1")
controller.playPlaylist(1, "p1", 2)
await { handle.currentItem?.id == "s3" && controller.state.value.isPlaying }
val point = runBlocking { queues.captureResetPoint() }
var currentItemReads = 0
handle.beforeCurrentItem = {
currentItemReads++
if (currentItemReads == 2) {
handle.beforeCurrentItem = null
handle.advanceTo(3)
}
}
handle.beforeRemoveFirst = {
handle.beforeRemoveFirst = null
handle.advanceTo(3)
}
setPlaylist("s0", "s1", "s2", "s3", "s4", "s5")
runBlocking { queues.resetChangedOrigins(setOf(QueueOrigin(QueueKind.Playlist, 1, "p1")), point) }
await {
handle.currentItem?.id == "s4" && controller.state.value.position == 3L &&
handle.items.size == 5 && handle.items.distinctBy { it.id }.size == 5
}
assertEquals(listOf("s1", "s2", "s3", "s4", "s5"), handle.items.map { it.id })
}
@Test
fun aSourceResetLetsTheRemovedCurrentFinishBeforeTheClampedTrack() {
seedPlaylist(5)