mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 07:29:14 +01:00
Harden the pending-action queue after review
- Order direct stars against the queue so a successful online unstar is no longer overwritten by an older queued star, and serialize all queue mutations under one lock. - Treat a SubsonicException with a negative code as a transport failure: queue it in setStar and keep it on flush instead of dropping it. - Make coalescing atomic, keep the offline collector alive across errors, and drain from the persisted offline flag so a queued action flushes on a cold start that is already online. - Give ServerActionSink.flush a default body and drop the no-op overrides.
This commit is contained in:
@@ -5,6 +5,7 @@ import androidx.room3.Room
|
||||
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.subtracks.awaitUntil
|
||||
import com.subtracks.cancelAndJoinBlocking
|
||||
import com.subtracks.data.db.SubtracksDatabase
|
||||
import com.subtracks.data.model.PendingActionKind
|
||||
@@ -64,8 +65,6 @@ class PendingServerActionSinkTest {
|
||||
starFailure?.let { throw it }
|
||||
calls += Call.Star(type, id, starred)
|
||||
}
|
||||
|
||||
override suspend fun flush() = Unit
|
||||
}
|
||||
|
||||
private lateinit var db: SubtracksDatabase
|
||||
@@ -73,6 +72,7 @@ class PendingServerActionSinkTest {
|
||||
private lateinit var delegate: RecordingSink
|
||||
private lateinit var sink: PendingServerActionSink
|
||||
private val offline = MutableStateFlow(true)
|
||||
private val offlinePreferences = MutableStateFlow(true)
|
||||
private var sourceId = 0L
|
||||
|
||||
@Before
|
||||
@@ -86,7 +86,15 @@ class PendingServerActionSinkTest {
|
||||
scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
sourceId = runBlocking { db.sourcesDao().insertSource(Source(name = "server", address = "http://x/", createdAt = 0)) }
|
||||
delegate = RecordingSink()
|
||||
sink = PendingServerActionSink(delegate, db, activeSourceId = { sourceId }, offline = offline, scope = scope)
|
||||
sink =
|
||||
PendingServerActionSink(
|
||||
delegate = delegate,
|
||||
db = db,
|
||||
activeSourceId = { sourceId },
|
||||
offline = offline,
|
||||
offlinePreferences = offlinePreferences,
|
||||
scope = scope,
|
||||
)
|
||||
}
|
||||
|
||||
@After
|
||||
@@ -110,6 +118,19 @@ class PendingServerActionSinkTest {
|
||||
assertEquals(0, db.pendingActionDao().pending(sourceId).size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun turningOfflineOffReplaysThroughTheCollector() =
|
||||
runBlocking {
|
||||
sink.setStar(StarType.Song, "s1", true)
|
||||
|
||||
offline.value = false
|
||||
offlinePreferences.value = false
|
||||
|
||||
awaitUntil("the queued star to replay") { delegate.calls.isNotEmpty() }
|
||||
assertEquals(listOf(Call.Star(StarType.Song, "s1", true)), delegate.calls)
|
||||
assertEquals(0, db.pendingActionDao().pending(sourceId).size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun anOfflineUnstarCoalescesAwayAnEarlierStar() =
|
||||
runBlocking {
|
||||
@@ -126,6 +147,21 @@ class PendingServerActionSinkTest {
|
||||
assertEquals(listOf(Call.Star(StarType.Song, "s1", false)), delegate.calls)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aQueuedStarCannotOverwriteANewerOnlineUnstar() =
|
||||
runBlocking {
|
||||
offline.value = false
|
||||
delegate.starFailure = IOException("down")
|
||||
sink.setStar(StarType.Song, "s1", true)
|
||||
assertEquals(1, db.pendingActionDao().pending(sourceId).size)
|
||||
|
||||
delegate.starFailure = null
|
||||
sink.setStar(StarType.Song, "s1", false)
|
||||
|
||||
assertEquals(listOf(Call.Star(StarType.Song, "s1", false)), delegate.calls)
|
||||
assertEquals(0, db.pendingActionDao().pending(sourceId).size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun offlineScrobblesReplayInOrderWithTheirTimes() =
|
||||
runBlocking {
|
||||
@@ -169,6 +205,17 @@ class PendingServerActionSinkTest {
|
||||
assertEquals(0, db.pendingActionDao().pending(sourceId).size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aTransportFailureIsQueuedRatherThanRejected() =
|
||||
runBlocking {
|
||||
offline.value = false
|
||||
delegate.starFailure = SubsonicException(-1, "HTTP 503")
|
||||
|
||||
sink.setStar(StarType.Song, "s1", true)
|
||||
|
||||
assertEquals(1, db.pendingActionDao().pending(sourceId).size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun flushKeepsQueuedActionsWhileTheServerIsStillUnreachable() =
|
||||
runBlocking {
|
||||
@@ -186,4 +233,22 @@ class PendingServerActionSinkTest {
|
||||
assertEquals(listOf(Call.Star(StarType.Song, "s1", true)), delegate.calls)
|
||||
assertEquals(0, db.pendingActionDao().pending(sourceId).size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun flushKeepsQueuedActionsOnATransportFailure() =
|
||||
runBlocking {
|
||||
sink.scrobble("s1", 1_000)
|
||||
|
||||
offline.value = false
|
||||
delegate.scrobbleFailure = SubsonicException(-1, "HTTP 503")
|
||||
sink.flush()
|
||||
|
||||
assertEquals(1, db.pendingActionDao().pending(sourceId).size)
|
||||
|
||||
delegate.scrobbleFailure = null
|
||||
sink.flush()
|
||||
|
||||
assertEquals(listOf(Call.Scrobble("s1", 1_000)), delegate.calls)
|
||||
assertEquals(0, db.pendingActionDao().pending(sourceId).size)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,8 +48,6 @@ class ScrobblerTest {
|
||||
id: String,
|
||||
starred: Boolean,
|
||||
) = Unit
|
||||
|
||||
override suspend fun flush() = Unit
|
||||
}
|
||||
|
||||
private fun TestScope.testScope() = CoroutineScope(UnconfinedTestDispatcher(testScheduler) + SupervisorJob())
|
||||
|
||||
Reference in New Issue
Block a user