Skip to content

Commit 7173a8c

Browse files
committed
Synchronized now playing state on outside promise
- Avoid timing out test to gather additional data - Used static list of `ResolvablePlaybackHandler`
1 parent 7799adc commit 7173a8c

File tree

3 files changed

+75
-35
lines changed

3 files changed

+75
-35
lines changed

projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/engine/PlaybackEngine.kt

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ import com.lasthopesoftware.bluewater.client.playback.nowplaying.storage.ManageN
2222
import com.lasthopesoftware.bluewater.client.playback.nowplaying.storage.NowPlaying
2323
import com.lasthopesoftware.bluewater.client.playback.playlist.ManagePlaylistPlayback
2424
import com.lasthopesoftware.bluewater.shared.lazyLogger
25-
import com.lasthopesoftware.policies.ratelimiting.PromisingRateLimiter
2625
import com.lasthopesoftware.promises.ContinuingResult
2726
import com.lasthopesoftware.promises.extensions.ProgressingPromise
2827
import com.lasthopesoftware.promises.extensions.keepPromise
2928
import com.lasthopesoftware.promises.extensions.onEachEventually
29+
import com.lasthopesoftware.promises.extensions.regardless
3030
import com.lasthopesoftware.promises.extensions.toPromise
3131
import com.lasthopesoftware.promises.extensions.unitResponse
3232
import com.namehillsoftware.handoff.errors.RejectionDropper
@@ -69,7 +69,10 @@ class PlaybackEngine(
6969

7070
private val activeLibraryId = AtomicReference(markerLibraryId)
7171

72-
private val nowPlayingStateSync = PromisingRateLimiter<NowPlaying?>(1)
72+
private val promisedNowPlayingStateSync = Any()
73+
74+
@Volatile
75+
private var promisedNowPlayingState = Promise.empty<NowPlaying?>()
7376

7477
private val promisedPlayback = AtomicReference<ProgressingPromise<PositionedPlayingFile, Unit>?>(null)
7578

@@ -480,13 +483,9 @@ class PlaybackEngine(
480483
return playlistPlayback.resume()
481484
}
482485

483-
private fun serializedPlayerUpdate() = nowPlayingStateSync.limit {
484-
playbackBootstrapper.updateFromState(activeLibraryId.get())
485-
}
486+
private fun serializedPlayerUpdate() = updateStateSynchronously { playbackBootstrapper.updateFromState(activeLibraryId.get()) }
486487

487-
private fun promiseActiveNowPlaying() = nowPlayingStateSync.limit {
488-
nowPlayingRepository.promiseNowPlaying(activeLibraryId.get())
489-
}
488+
private fun promiseActiveNowPlaying() = updateStateSynchronously { nowPlayingRepository.promiseNowPlaying(activeLibraryId.get()) }
490489

491490
private fun saveState(
492491
libraryId: LibraryId,
@@ -508,7 +507,7 @@ class PlaybackEngine(
508507
private inline fun saveState(
509508
libraryId: LibraryId,
510509
crossinline updateFunc: NowPlaying.() -> NowPlaying
511-
): Promise<NowPlaying?> = nowPlayingStateSync.limit {
510+
): Promise<NowPlaying?> = updateStateSynchronously {
512511
nowPlayingRepository.promiseNowPlaying(libraryId).eventually {
513512
it?.let { np ->
514513
val updatedNowPlaying = updateFunc(np)
@@ -519,7 +518,25 @@ class PlaybackEngine(
519518
}
520519

521520
@Suppress("UNCHECKED_CAST")
522-
private fun saveState(nowPlaying: NowPlaying): Promise<NowPlaying?> = nowPlayingStateSync.limit {
521+
private fun saveState(nowPlaying: NowPlaying): Promise<NowPlaying?> = updateStateSynchronously {
523522
nowPlayingRepository.updateNowPlaying(nowPlaying) as Promise<NowPlaying?>
524523
}
524+
525+
/**
526+
* Updates the `promisedNowPlayingState` with the result of the provided `updateFunc`.
527+
*
528+
* This function ensures that the `promisedNowPlayingState` is updated synchronously.
529+
* Each new update is chained to the previous promise, guaranteeing that state
530+
* updates are applied in the order they are initiated.
531+
*
532+
* @param updateFunc A function that returns a [Promise] for a nullable [NowPlaying] state.
533+
* This function is responsible for fetching or modifying the state.
534+
* @return A [Promise] that resolves with the current [NowPlaying] state after the update
535+
* function has been scheduled. To wait for the update to complete, chain to the returned promise.
536+
*/
537+
private inline fun updateStateSynchronously(crossinline updateFunc: () -> Promise<NowPlaying?>): Promise<NowPlaying?> = synchronized(promisedNowPlayingStateSync) {
538+
promisedNowPlayingState
539+
.regardless { updateFunc() }
540+
.also { promisedNowPlayingState = it }
541+
}
525542
}

projectBlueWater/src/test/java/com/lasthopesoftware/bluewater/client/playback/engine/GivenAPlayingPlaybackEngine/When changing tracks many times.kt

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,20 @@ import com.lasthopesoftware.bluewater.client.playback.engine.bootstrap.ManagedPl
1111
import com.lasthopesoftware.bluewater.client.playback.engine.preparation.PreparedPlaybackQueueResourceManagement
1212
import com.lasthopesoftware.bluewater.client.playback.file.PositionedFile
1313
import com.lasthopesoftware.bluewater.client.playback.file.PositionedPlayingFile
14-
import com.lasthopesoftware.bluewater.client.playback.file.preparation.FakeMappedPlayableFilePreparationSourceProvider
14+
import com.lasthopesoftware.bluewater.client.playback.file.fakes.FakePreparedPlayableFile
15+
import com.lasthopesoftware.bluewater.client.playback.file.fakes.ResolvablePlaybackHandler
1516
import com.lasthopesoftware.bluewater.client.playback.file.preparation.queues.CompletingFileQueueProvider
1617
import com.lasthopesoftware.bluewater.client.playback.nowplaying.storage.NowPlayingRepository
1718
import com.lasthopesoftware.bluewater.client.playback.volume.PlaylistVolumeManager
19+
import com.lasthopesoftware.bluewater.shared.promises.extensions.DeferredPromise
1820
import com.lasthopesoftware.bluewater.shared.promises.extensions.toExpiringFuture
21+
import com.lasthopesoftware.promises.ForwardedResponse.Companion.thenForward
22+
import com.lasthopesoftware.promises.extensions.keepPromise
23+
import com.lasthopesoftware.promises.PromiseDelay
24+
import com.lasthopesoftware.promises.toFuture
1925
import com.namehillsoftware.handoff.promises.Promise
26+
import io.mockk.every
27+
import io.mockk.mockk
2028
import org.assertj.core.api.Assertions.assertThat
2129
import org.joda.time.Duration
2230
import org.junit.jupiter.api.BeforeAll
@@ -30,19 +38,29 @@ class `When changing tracks many times` {
3038
}
3139

3240
private val mut by lazy {
33-
val fakePlaybackPreparerProvider = FakeMappedPlayableFilePreparationSourceProvider(
34-
listOf(
35-
ServiceFile("1"),
36-
ServiceFile("2"),
37-
ServiceFile("3"),
38-
ServiceFile("4"),
39-
ServiceFile("5")
40-
)
41-
)
41+
val deferredPlaybackHandlers = listOf(
42+
ServiceFile("1"),
43+
ServiceFile("2"),
44+
ServiceFile("3"),
45+
ServiceFile("4"),
46+
ServiceFile("5")
47+
).associateWith {
48+
val playbackHandler = ResolvablePlaybackHandler()
49+
Pair(playbackHandler, DeferredPromise(FakePreparedPlayableFile(playbackHandler)))
50+
}
51+
4252
val library = Library(id = libraryId)
4353
val libraryProvider = FakeLibraryRepository(library)
44-
val preparedPlaybackQueueResourceManagement =
45-
PreparedPlaybackQueueResourceManagement(fakePlaybackPreparerProvider, FakePlaybackQueueConfiguration())
54+
val preparedPlaybackQueueResourceManagement = PreparedPlaybackQueueResourceManagement(
55+
mockk {
56+
every { providePlayableFilePreparationSource() } returns mockk {
57+
every { promisePreparedPlaybackFile(LibraryId(libraryId), any(), any()) } answers {
58+
deferredPlaybackHandlers[secondArg()]?.second.keepPromise().thenForward()
59+
}
60+
}
61+
},
62+
FakePlaybackQueueConfiguration()
63+
)
4664
val repository = NowPlayingRepository(
4765
FakeSelectedLibraryProvider(),
4866
libraryProvider,
@@ -61,7 +79,7 @@ class `When changing tracks many times` {
6179
playbackBootstrapper,
6280
playbackBootstrapper,
6381
)
64-
Pair(fakePlaybackPreparerProvider, playbackEngine)
82+
Pair(deferredPlaybackHandlers, playbackEngine)
6583
}
6684

6785
private var nextSwitchedFile: PositionedFile? = null
@@ -70,7 +88,7 @@ class `When changing tracks many times` {
7088

7189
@BeforeAll
7290
fun act() {
73-
val (fakePlaybackPreparerProvider, playbackEngine) = mut
91+
val (preparedFiles, playbackEngine) = mut
7492

7593
val promisedFinalFile = Promise {
7694
playbackEngine
@@ -82,7 +100,7 @@ class `When changing tracks many times` {
82100
}
83101
}
84102

85-
val playlist = fakePlaybackPreparerProvider.deferredResolutions.keys.toList()
103+
val playlist = preparedFiles.keys.toList()
86104

87105
val promisedStart = playbackEngine
88106
.startPlaylist(
@@ -91,7 +109,8 @@ class `When changing tracks many times` {
91109
1
92110
)
93111

94-
val playingPlaybackHandler = fakePlaybackPreparerProvider.deferredResolutions[ServiceFile("2")]?.resolve()
112+
val (playingPlaybackHandler, resolvablePlaybackHandler) = preparedFiles.getValue(ServiceFile("2"))
113+
resolvablePlaybackHandler.resolve()
95114
promisedStart.toExpiringFuture().get()
96115

97116
val promisedChanges = Promise.whenAll(
@@ -101,19 +120,22 @@ class `When changing tracks many times` {
101120
)
102121

103122
// Resolve the skipped tracks as well to ensure that they aren't the last switched track
104-
fakePlaybackPreparerProvider.deferredResolutions[playlist[2]]?.resolve()
123+
preparedFiles[playlist[2]]?.second?.resolve()
105124

106-
val finalPreparableFile = fakePlaybackPreparerProvider.deferredResolutions[playlist[4]]
107-
finalPreparableFile?.resolve()
125+
val finalPreparableFile = preparedFiles[playlist[4]]
126+
finalPreparableFile?.second?.resolve()
108127

109128
nextSwitchedFile = promisedChanges.toExpiringFuture().get()?.lastOrNull()?.second
110129

111-
// Resolve the first skipped tracks afterward to ensure a cancellation is tested.
112-
fakePlaybackPreparerProvider.deferredResolutions[playlist[0]]?.resolve()
130+
// Resolve the first skipped track afterward to ensure a cancellation is tested.
131+
preparedFiles[playlist[0]]?.second?.resolve()
113132

114-
playingPlaybackHandler?.resolve()
133+
playingPlaybackHandler.resolve()
115134

116-
latestFile = promisedFinalFile.toExpiringFuture().get()
135+
latestFile = Promise.whenAny(
136+
promisedFinalFile,
137+
PromiseDelay.delay(Duration.standardSeconds(10))
138+
).toFuture().get()
117139
}
118140

119141
@Test

projectBlueWater/src/test/java/com/lasthopesoftware/bluewater/shared/promises/extensions/DeferredPromise.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import com.namehillsoftware.handoff.cancellation.CancellationResponse
44
import com.namehillsoftware.handoff.promises.Promise
55

66
open class DeferredPromise<Resolution> : Promise<Resolution>, CancellationResponse {
7-
private val resolution: Resolution?
8-
private val error: Throwable?
7+
val resolution: Resolution?
8+
val error: Throwable?
9+
910
var isCancelled = false
1011
private set
1112

0 commit comments

Comments
 (0)