Last updated on
Music streaming service
- Points
- 40 points
- Topics
- Futures, Modularity, Specifications, Refactoring
- Files to submit
src/main/scala/provider/RacingProvider.scala
src/main/scala/provider/SearchProvider.scala
src/main/scala/provider/PlaylistResolver.scala
Introduction
Here is a representation of the architecture of the system:
The system has two main components:
- Providers, which give access to songs and other functionality. Databases are the simplest kind of provider.
- Clients, which connect to the providers
The songs are split across two databases (one in Europe, one in the US), with some songs on both. When a key is present in both databases, its contents are the same in both.
Clients send requests to a provider to retrieve a song, and the provider then retrieves the song and serves it to the client.
As the databases are located on different continents, the latency between the providers and the databases is variable. So, when a client requests a song that is available in both databases, we want the first available result to be returned to the client, without waiting for the other database.
Some important types used by the system are shown below:
-
MusicEntry
, which represents a song in our system. Thedata
field contains the actual music content.case class MusicEntry( key: MusicKey, artist: String, album: String, title: String, year: Int, data: MusicData ) case class MusicKey(val key: String) case class MusicData(val data: Vector[Byte])
src/main/scala/provider/MusicProvider.scala
-
Provider
, the interface that clients interact with.retrieve
fetches an entry from the underlying databases, andretrieveKeys
lists all available keys.trait Provider[Key, Entry]: def retrieve(key: Key): Future[Entry] def retrieveKeys: Future[Set[Key]]
src/main/scala/provider/Provider.scala
-
MusicProvider
, a specialization of the genericProvider
interface toMusicEntry
objects:type MusicProvider = Provider[MusicKey, MusicEntry]
src/main/scala/provider/MusicProvider.scala
Part 1: Racing provider (15 pts)
The current implementation of the simplest provider, RacingProviderSync
, uses the old synchronous interface. This prevents it from returning songs quickly to users.
Your task in this part is to implement the RacingProvider
class in src/main/scala/provider/RacingProvider.scala
without using the deprecated (synchronous) methods of the MusicDatabase
object:
class RacingProvider[Key, Entry](
val providerEU: Provider[Key, Entry],
val providerUS: Provider[Key, Entry]
) extends Provider[Key, Entry]:
def retrieve(key: Key): Future[Entry] =
???
def retrieveKeys: Future[Set[Key]] =
???
src/main/scala/provider/RacingProvider.scala
Use testOnly -- provider.RacingProviderTest.*
to test your implementation.
You may find functions firstCompletedOf
and firstSuccessOf
in src/main/scala/provider/ProviderUtils.scala
useful.
Part 2: Playlist provider (10 pts)
We now want to add a new feature: a playlist system. The idea is that users can create playlists, which contain a list of MusicKey
s. For this, we need:
-
An interface to retrieve playlists:
type PlaylistProvider[Item] = Provider[PlaylistKey, List[Item]] case class PlaylistKey(name: String)
src/main/scala/provider/PlaylistProvider.scala
-
A way to resolve a playlist by retrieving all the songs corresponding to the
MusicKey
s in a playlist.
We can expose this resolution facility as a provider: given a playlist key, it returns a playlist whose items are MusicEntry
s instead of MusicKey
s:
class PlaylistResolver(
musicProvider: MusicProvider,
playlistProvider: PlaylistProvider[MusicKey]
) extends PlaylistProvider[MusicEntry]:
def retrieve(key: PlaylistKey): Future[List[MusicEntry]] =
???
def retrieveKeys: Future[Set[PlaylistKey]] =
???
src/main/scala/provider/PlaylistResolver.scala
Implement the PlaylistResolver
class in src/main/scala/provider/PlaylistResolver.scala
.
Use testOnly -- provider.PlaylistResolverTest.*
to test your implementation.
-
You can draw inspiration from the
PlaylistResolverSync
that an intern in your company wrote during an internship. -
You can use the
Future.sequence
function to transform anIterable[Future[T]]
into aFuture[Iterable[T]]
, or theFuture.traverse
function for more complex iteration.
Part 3: Search provider (15 pts)
We now want to add a new search feature to the service. The idea is that users can search for songs by title, artist, album, etc. The providers should then be able to retrieve the songs from the databases, and return them to the client.
Ideally, this feature would be provided by the databases themselves, but their implementation is out of our control. Hence, the team has decided to implement the search feature at the provider level instead. The new SearchProvider
should implement the search feature by filtering the results of retrieving all songs with their keys:
class SearchProvider[Key, Entry](p: RacingProvider[Key, Entry])
extends Provider[SearchQuery[Entry], Set[Entry]]:
def retrieve(query: SearchQuery[Entry]): Future[Set[Entry]] =
???
def retrieveKeys: Future[Set[SearchQuery[Entry]]] =
throw UnsupportedOperationException(
"Cannot retrieve keys of search provider"
)
src/main/scala/provider/SearchProvider.scala
Implement the SearchProvider
class in src/main/scala/provider/SearchProvider.scala
.
- You can draw inspiration from the
SearchProviderSync
that an intern in your company wrote during an internship, but make sure that you do not unnecessarily wait or sequence operations that don’t depend on each other!
Use testOnly -- provider.SearchProviderTest.*
to test your implementation.