Skip to content

Commit 0ba3040

Browse files
committed
trim cache when necessary, lazily
1 parent acb3eb4 commit 0ba3040

File tree

4 files changed

+78
-35
lines changed

4 files changed

+78
-35
lines changed

src/main/kotlin/com/adamratzman/spotify/Builder.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class SpotifyApiBuilder(
2323
var tokenString: String? = null,
2424
var token: Token? = null,
2525
var useCache: Boolean = true,
26+
var cacheLimit: Int? = 1000,
2627
var automaticRefresh: Boolean = true,
2728
var retryWhenRateLimited: Boolean = false,
2829
var enableLogger: Boolean = false
@@ -69,6 +70,11 @@ class SpotifyApiBuilder(
6970
*/
7071
fun useCache(useCache: Boolean) = apply { this.useCache = useCache }
7172

73+
/**
74+
* Set the maximum allowed amount of cached requests at one time. Null means no limit
75+
*/
76+
fun cacheLimit(cacheLimit: Int?) = apply { this.cacheLimit = cacheLimit }
77+
7278
/**
7379
* Set the application [redirect uri](https://developer.spotify.com/documentation/general/guides/authorization-guide/)
7480
*/
@@ -200,6 +206,7 @@ class SpotifyUserAuthorizationBuilder(
200206
* @property authentication A holder for authentication methods. At least one needs to be provided in order to create
201207
* a **client** api
202208
* @property useCache Set whether to cache requests. Default: true
209+
* @property cacheLimit The maximum amount of cached requests allowed at one time. Null means no limit
203210
* @property automaticRefresh Enable or disable automatic refresh of the Spotify access token
204211
* @property retryWhenRateLimited Set whether to block the current thread and wait until the API can retry the request
205212
* @property enableLogger Set whether to enable to the exception logger
@@ -208,6 +215,7 @@ class SpotifyApiBuilderDsl {
208215
private var credentials: SpotifyCredentials = SpotifyCredentials(null, null, null)
209216
private var authentication = SpotifyUserAuthorizationBuilder()
210217
var useCache: Boolean = true
218+
var cacheLimit: Int? = 1000
211219
var automaticRefresh: Boolean = true
212220
var retryWhenRateLimited: Boolean = false
213221
var enableLogger: Boolean = false

src/main/kotlin/com/adamratzman/spotify/SpotifyAPI.kt

Lines changed: 43 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ internal val base = "https://api.spotify.com/v1"
4646
* @property token The access token associated with this API instance
4747
* @property useCache Whether to use the built-in cache to avoid making unnecessary calls to
4848
* the Spotify API
49+
* @property cacheLimit The maximum amount of cached requests allowed at one time. Null means no limit
4950
*
5051
* @property search Provides access to the Spotify [search endpoint](https://developer.spotify.com/documentation/web-api/reference/search/search/)
5152
* @property albums Provides access to Spotify [album endpoints](https://developer.spotify.com/documentation/web-api/reference/albums/)
@@ -62,6 +63,7 @@ abstract class SpotifyAPI internal constructor(
6263
val clientSecret: String,
6364
var token: Token,
6465
useCache: Boolean,
66+
var cacheLimit: Int?,
6567
var automaticRefresh: Boolean,
6668
var retryWhenRateLimited: Boolean,
6769
enableLogger: Boolean
@@ -73,6 +75,11 @@ abstract class SpotifyAPI internal constructor(
7375
field = value
7476
}
7577

78+
/**
79+
* Obtain a map of all currently-cached requests
80+
*/
81+
fun getCache() = endpoints.map { it.cache.cachedRequests.toList() }.flatten().toMap()
82+
7683
val expireTime: Long get() = System.currentTimeMillis() + token.expiresIn * 1000
7784

7885
val executor: ScheduledExecutorService = Executors.newScheduledThreadPool(0)
@@ -99,10 +106,15 @@ abstract class SpotifyAPI internal constructor(
99106
*/
100107
abstract fun refreshToken(): Token
101108

109+
/**
110+
* A list of all endpoints included in this api type
111+
*/
112+
abstract val endpoints: List<SpotifyEndpoint>
113+
102114
/**
103115
* If the cache is enabled, clear all stored queries in the cache
104116
*/
105-
abstract fun clearCache()
117+
fun clearCache() = clearCaches(*endpoints.toTypedArray())
106118

107119
/**
108120
* Return a new [SpotifyApiBuilder] with the parameters provided to this api instance
@@ -114,7 +126,7 @@ abstract class SpotifyAPI internal constructor(
114126
*/
115127
abstract fun getApiBuilderDsl(): SpotifyApiBuilderDsl
116128

117-
internal fun clearAllCaches(vararg endpoints: SpotifyEndpoint) {
129+
private fun clearCaches(vararg endpoints: SpotifyEndpoint) {
118130
endpoints.forEach { it.cache.clear() }
119131
}
120132

@@ -150,10 +162,11 @@ class SpotifyAppAPI internal constructor(
150162
clientSecret: String,
151163
token: Token,
152164
useCache: Boolean,
165+
cacheLimit: Int?,
153166
automaticRefresh: Boolean,
154167
retryWhenRateLimited: Boolean,
155168
enableLogger: Boolean
156-
) : SpotifyAPI(clientId, clientSecret, token, useCache, automaticRefresh, retryWhenRateLimited, enableLogger) {
169+
) : SpotifyAPI(clientId, clientSecret, token, useCache, cacheLimit, automaticRefresh, retryWhenRateLimited, enableLogger) {
157170

158171
override val search: SearchAPI = SearchAPI(this)
159172
override val albums: AlbumAPI = AlbumAPI(this)
@@ -189,16 +202,17 @@ class SpotifyAppAPI internal constructor(
189202
throw BadRequestException("Either the client id or the client secret is not set")
190203
}
191204

192-
override fun clearCache() = clearAllCaches(
193-
search,
194-
albums,
195-
browse,
196-
artists,
197-
playlists,
198-
users,
199-
tracks,
200-
following
201-
)
205+
override val endpoints: List<SpotifyEndpoint>
206+
get() = listOf(
207+
search,
208+
albums,
209+
browse,
210+
artists,
211+
playlists,
212+
users,
213+
tracks,
214+
following
215+
)
202216

203217
override fun getApiBuilder() = SpotifyApiBuilder(clientId, clientSecret, useCache)
204218

@@ -223,9 +237,10 @@ class SpotifyClientAPI internal constructor(
223237
automaticRefresh: Boolean,
224238
var redirectUri: String,
225239
useCache: Boolean,
240+
cacheLimit: Int?,
226241
retryWhenRateLimited: Boolean,
227242
enableLogger: Boolean
228-
) : SpotifyAPI(clientId, clientSecret, token, useCache, automaticRefresh, retryWhenRateLimited, enableLogger) {
243+
) : SpotifyAPI(clientId, clientSecret, token, useCache, cacheLimit, automaticRefresh, retryWhenRateLimited, enableLogger) {
229244
override val search: SearchAPI = SearchAPI(this)
230245
override val albums: AlbumAPI = AlbumAPI(this)
231246
override val browse: BrowseAPI = BrowseAPI(this)
@@ -316,19 +331,20 @@ class SpotifyClientAPI internal constructor(
316331
} else throw BadRequestException(response.body.toObject<AuthenticationError>(this))
317332
}
318333

319-
override fun clearCache() = clearAllCaches(
320-
search,
321-
albums,
322-
browse,
323-
artists,
324-
playlists,
325-
users,
326-
tracks,
327-
following,
328-
personalization,
329-
library,
330-
player
331-
)
334+
override val endpoints: List<SpotifyEndpoint>
335+
get() = listOf(
336+
search,
337+
albums,
338+
browse,
339+
artists,
340+
playlists,
341+
users,
342+
tracks,
343+
following,
344+
personalization,
345+
library,
346+
player
347+
)
332348

333349
override fun getApiBuilder() = SpotifyApiBuilder(clientId, clientSecret, redirectUri, useCache = useCache)
334350

src/main/kotlin/com/adamratzman/spotify/http/Endpoints.kt

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ import java.net.HttpURLConnection
1515
import java.net.URLEncoder
1616
import java.util.Base64
1717
import java.util.function.Supplier
18+
import kotlin.math.ceil
1819

1920
abstract class SpotifyEndpoint(val api: SpotifyAPI) {
20-
internal val cache = SpotifyCache()
21+
val cache = SpotifyCache()
2122

2223
internal fun get(url: String): String {
2324
return execute(url)
@@ -139,17 +140,18 @@ internal class EndpointBuilder(private val path: String) {
139140
override fun toString() = builder.toString()
140141
}
141142

142-
internal class SpotifyCache {
143-
private val cachedRequests = hashMapOf<SpotifyRequest, CacheState>()
143+
class SpotifyCache {
144+
val cachedRequests = mutableMapOf<SpotifyRequest, CacheState>()
144145

145146
internal operator fun get(request: SpotifyRequest): CacheState? {
146147
checkCache(request)
147148
return cachedRequests[request]
148149
}
149150

150151
internal operator fun set(request: SpotifyRequest, state: CacheState) {
151-
checkCache(request)
152152
if (request.api.useCache) cachedRequests[request] = state
153+
154+
checkCache(request)
153155
}
154156

155157
internal operator fun minusAssign(request: SpotifyRequest) {
@@ -161,17 +163,34 @@ internal class SpotifyCache {
161163

162164
private fun checkCache(request: SpotifyRequest) {
163165
if (!request.api.useCache) clear()
166+
else {
167+
cachedRequests.entries.removeIf { !it.value.isStillValid() }
168+
169+
val cacheLimit = request.api.cacheLimit
170+
val cacheUse = request.api.endpoints.sumBy { it.cache.cachedRequests.size }
171+
172+
if (cacheLimit != null && cacheUse > cacheLimit) {
173+
val amountRemoveFromEach = ceil((cacheUse - cacheLimit).toDouble() / request.api.endpoints.size).toInt()
174+
175+
request.api.endpoints.forEach { endpoint ->
176+
val entries = endpoint.cache.cachedRequests.entries
177+
val toRemove = entries.sortedBy { it.value.expireBy }.take(amountRemoveFromEach)
178+
179+
if (toRemove.isNotEmpty()) entries.removeAll(toRemove)
180+
}
181+
}
182+
}
164183
}
165184
}
166185

167-
internal data class SpotifyRequest(
186+
data class SpotifyRequest(
168187
val url: String,
169188
val method: HttpRequestMethod,
170189
val body: String?,
171190
val api: SpotifyAPI
172191
)
173192

174-
internal data class CacheState(val data: String, val eTag: String?, val expireBy: Long = 0) {
193+
data class CacheState(val data: String, val eTag: String?, val expireBy: Long = 0) {
175194
private val cacheRegex = "max-age=(\\d+)".toRegex()
176195
internal fun isStillValid(): Boolean = System.currentTimeMillis() <= this.expireBy
177196

src/main/kotlin/com/adamratzman/spotify/http/HttpConnection.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import java.net.URL
88
import java.util.concurrent.Executors
99
import java.util.concurrent.TimeUnit
1010

11-
internal enum class HttpRequestMethod { GET, POST, PUT, DELETE }
12-
internal data class HttpHeader(val key: String, val value: String)
11+
enum class HttpRequestMethod { GET, POST, PUT, DELETE }
12+
data class HttpHeader(val key: String, val value: String)
1313

1414
internal class HttpConnection(
1515
private val url: String,

0 commit comments

Comments
 (0)