mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-11 08:47:33 +00:00
refactor(media-servers): cache health probes, prune stale status, save tab
Follow-up on the audit of the Media Servers redesign: - Add a process-wide TTL cache (60s) to MediaServerHealthProbe so probe results survive the screen's ViewModel being recreated on each open; the ViewModel reuses fresh cached status instead of re-hitting the network, and skips launching a probe when one is already in flight. - Prune the _health map on refresh/remove so it can't grow unbounded as servers are added and removed within a session. - Persist the selected tab across configuration changes (rememberSaveable). - Restore the screen's intro caption, reusing the previously orphaned set_preferred_media_servers string. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GJ77Hm5L7fXEbPWbUds1iA
This commit is contained in:
+10
-3
@@ -42,7 +42,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -108,11 +108,18 @@ fun MediaServersScaffold(
|
||||
).consumeWindowInsets(padding)
|
||||
.imePadding(),
|
||||
) {
|
||||
var selectedTab by remember { mutableIntStateOf(TAB_SERVERS) }
|
||||
var selectedTab by rememberSaveable { mutableIntStateOf(TAB_SERVERS) }
|
||||
val tabs = listOf(R.string.media_servers_tab_servers, R.string.media_servers_tab_cache)
|
||||
|
||||
Text(
|
||||
text = stringRes(id = R.string.set_preferred_media_servers),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
modifier = Modifier.padding(top = 10.dp),
|
||||
)
|
||||
|
||||
SingleChoiceSegmentedButtonRow(
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 12.dp, bottom = 8.dp),
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 10.dp, bottom = 8.dp),
|
||||
) {
|
||||
tabs.forEachIndexed { index, labelRes ->
|
||||
SegmentedButton(
|
||||
|
||||
+19
-1
@@ -77,6 +77,7 @@ class BlossomServersViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
}
|
||||
pruneHealth()
|
||||
}
|
||||
|
||||
/** Moves a server to a new position; list order is the upload/fallback priority. */
|
||||
@@ -91,13 +92,23 @@ class BlossomServersViewModel : ViewModel() {
|
||||
isModified = true
|
||||
}
|
||||
|
||||
/** Re-probes every server currently in the list. */
|
||||
/** Re-probes every server currently in the list. Fresh cached results are reused. */
|
||||
fun checkAllHealth() {
|
||||
_fileServers.value.forEach { probeServer(it.baseUrl) }
|
||||
}
|
||||
|
||||
private fun probeServer(serverUrl: String) {
|
||||
val builder = httpClientBuilder ?: return
|
||||
|
||||
// A probe is already in flight for this URL — don't launch a duplicate.
|
||||
if (_health.value[serverUrl] == ServerHealth.Checking) return
|
||||
|
||||
// Reuse a still-fresh cached status instead of hitting the network again.
|
||||
MediaServerHealthProbe.cached(serverUrl)?.let { cachedStatus ->
|
||||
_health.update { it + (serverUrl to cachedStatus) }
|
||||
return
|
||||
}
|
||||
|
||||
_health.update { it + (serverUrl to ServerHealth.Checking) }
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val result = MediaServerHealthProbe.probe(serverUrl, builder::okHttpClientForPreview)
|
||||
@@ -105,6 +116,12 @@ class BlossomServersViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
/** Drops health entries for servers no longer in the list so the map can't grow unbounded. */
|
||||
private fun pruneHealth() {
|
||||
val liveUrls = _fileServers.value.mapTo(HashSet()) { it.baseUrl }
|
||||
_health.update { statuses -> statuses.filterKeys { it in liveUrls } }
|
||||
}
|
||||
|
||||
fun addServerList(serverList: List<String>) {
|
||||
serverList.forEach { serverUrl ->
|
||||
addServer(serverUrl)
|
||||
@@ -157,6 +174,7 @@ class BlossomServersViewModel : ViewModel() {
|
||||
ServerName(serverName, serverUrl, ServerType.Blossom),
|
||||
)
|
||||
}
|
||||
pruneHealth()
|
||||
isModified = true
|
||||
}
|
||||
}
|
||||
|
||||
+31
@@ -25,6 +25,7 @@ import kotlinx.coroutines.CancellationException
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.coroutines.executeAsync
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
@@ -63,9 +64,39 @@ object MediaServerHealthProbe {
|
||||
const val SLOW_THRESHOLD_MS: Long = 1_000L
|
||||
private const val PROBE_TIMEOUT_MS: Long = 5_000L
|
||||
|
||||
/**
|
||||
* How long a probe result is reused before the server is re-checked. The cache is
|
||||
* process-wide (this is a singleton) so results survive the screen's ViewModel being
|
||||
* recreated on each open, mirroring [com.vitorpamplona.amethyst.service.uploads.blossom.bud10.LocalBlossomCacheProbe].
|
||||
*/
|
||||
private const val CACHE_TTL_MS: Long = 60_000L
|
||||
|
||||
private class CachedResult(
|
||||
val status: ServerHealth,
|
||||
val atMs: Long,
|
||||
)
|
||||
|
||||
private val cache = ConcurrentHashMap<String, CachedResult>()
|
||||
|
||||
/** The cached status for [baseUrl] if still within [CACHE_TTL_MS], else null. */
|
||||
fun cached(baseUrl: String): ServerHealth? {
|
||||
val entry = cache[baseUrl] ?: return null
|
||||
return if (TimeUtils.nowMillis() - entry.atMs < CACHE_TTL_MS) entry.status else null
|
||||
}
|
||||
|
||||
suspend fun probe(
|
||||
baseUrl: String,
|
||||
clientForUrl: (String) -> OkHttpClient,
|
||||
): ServerHealth {
|
||||
cached(baseUrl)?.let { return it }
|
||||
val result = runProbe(baseUrl, clientForUrl)
|
||||
cache[baseUrl] = CachedResult(result, TimeUtils.nowMillis())
|
||||
return result
|
||||
}
|
||||
|
||||
private suspend fun runProbe(
|
||||
baseUrl: String,
|
||||
clientForUrl: (String) -> OkHttpClient,
|
||||
): ServerHealth =
|
||||
try {
|
||||
val client =
|
||||
|
||||
Reference in New Issue
Block a user