mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-03 16:50:40 +02:00
fix(runtime): bound resources and quarantine stalled extensions
This commit is contained in:
@@ -41,15 +41,13 @@ object NativeDownloadFinalizer {
|
||||
// Keep this schema contract in sync with Dart HistoryDatabase before bumping either side.
|
||||
const val HISTORY_SCHEMA_VERSION = 12
|
||||
internal val activeFFmpegSessionIds = mutableSetOf<Long>()
|
||||
internal val nativeFFmpegSessionIds = mutableSetOf<Long>()
|
||||
internal val nativeFFmpegSessionIds = BoundedRegistry<Long>(maxEntries = 256)
|
||||
internal val activeFFmpegSessionLock = Any()
|
||||
internal val ffmpegCompleteCallbackLock = Any()
|
||||
internal val qualityVariantNameLocks = java.util.concurrent.ConcurrentHashMap<String, Any>()
|
||||
internal val qualityVariantNameLocks = KeyedLockPool<String>()
|
||||
internal var forwardedFFmpegCompleteCallback: FFmpegSessionCompleteCallback? = null
|
||||
internal val nativeFilteringFFmpegCompleteCallback = FFmpegSessionCompleteCallback { session ->
|
||||
val isNativeSession = synchronized(activeFFmpegSessionLock) {
|
||||
nativeFFmpegSessionIds.contains(session.sessionId)
|
||||
}
|
||||
val isNativeSession = nativeFFmpegSessionIds.consume(session.sessionId)
|
||||
if (!isNativeSession) {
|
||||
val delegate = synchronized(ffmpegCompleteCallbackLock) {
|
||||
forwardedFFmpegCompleteCallback
|
||||
|
||||
@@ -104,8 +104,8 @@ internal fun NativeDownloadFinalizer.runFFmpeg(command: String, shouldCancel: ()
|
||||
val sessionId = session.sessionId
|
||||
synchronized(activeFFmpegSessionLock) {
|
||||
activeFFmpegSessionIds.add(sessionId)
|
||||
nativeFFmpegSessionIds.add(sessionId)
|
||||
}
|
||||
nativeFFmpegSessionIds.add(sessionId)
|
||||
FFmpegKitConfig.asyncFFmpegExecute(session)
|
||||
try {
|
||||
var cancelRequested = false
|
||||
|
||||
@@ -124,8 +124,7 @@ internal fun NativeDownloadFinalizer.finalizeQualityVariantFilename(
|
||||
val cleanTarget = File(source.parentFile, cleanName)
|
||||
val lockTarget = if (collisionOnly) cleanTarget else File(source.parentFile, preferredName)
|
||||
val lockKey = lockTarget.absolutePath.lowercase(Locale.ROOT)
|
||||
val lock = qualityVariantNameLocks.computeIfAbsent(lockKey) { Any() }
|
||||
synchronized(lock) {
|
||||
qualityVariantNameLocks.withLock(lockKey) {
|
||||
val selectedName = if (collisionOnly) {
|
||||
resolveQualityVariantFilename(
|
||||
fileName = logicalFileName,
|
||||
@@ -138,11 +137,11 @@ internal fun NativeDownloadFinalizer.finalizeQualityVariantFilename(
|
||||
preferredName
|
||||
}
|
||||
input.result.put("quality_variant_file_name", selectedName)
|
||||
if (selectedName == state.fileName) return@synchronized
|
||||
if (selectedName == state.fileName) return@withLock
|
||||
val target = uniqueLocalFile(source.parentFile, selectedName)
|
||||
if (!source.renameTo(target)) {
|
||||
Log.w(TAG, "Could not rename quality variant output: ${source.absolutePath}")
|
||||
return@synchronized
|
||||
return@withLock
|
||||
}
|
||||
state.filePath = target.absolutePath
|
||||
state.fileName = target.name
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.zarz.spotiflac
|
||||
|
||||
import java.util.LinkedHashSet
|
||||
|
||||
/**
|
||||
* Serializes work per key while allowing idle key entries to be reclaimed.
|
||||
* References are counted before a caller waits on the monitor, so a waiter can
|
||||
* never race with removal and end up using a different monitor for the same key.
|
||||
*/
|
||||
internal class KeyedLockPool<K> {
|
||||
private class Entry {
|
||||
val monitor = Any()
|
||||
var references = 0
|
||||
}
|
||||
|
||||
private val registryLock = Any()
|
||||
private val entries = HashMap<K, Entry>()
|
||||
|
||||
fun <T> withLock(key: K, block: () -> T): T {
|
||||
val entry = synchronized(registryLock) {
|
||||
entries.getOrPut(key) { Entry() }.also { it.references++ }
|
||||
}
|
||||
try {
|
||||
return synchronized(entry.monitor) { block() }
|
||||
} finally {
|
||||
synchronized(registryLock) {
|
||||
entry.references--
|
||||
if (entry.references == 0 && entries[key] === entry) {
|
||||
entries.remove(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun activeKeyCount(): Int = synchronized(registryLock) {
|
||||
entries.size
|
||||
}
|
||||
}
|
||||
|
||||
/** A small insertion-ordered registry that consumes completed IDs. */
|
||||
internal class BoundedRegistry<T>(private val maxEntries: Int) {
|
||||
private val lock = Any()
|
||||
private val values = LinkedHashSet<T>()
|
||||
|
||||
init {
|
||||
require(maxEntries > 0) { "maxEntries must be greater than zero" }
|
||||
}
|
||||
|
||||
fun add(value: T) = synchronized(lock) {
|
||||
values.remove(value)
|
||||
values.add(value)
|
||||
while (values.size > maxEntries) {
|
||||
val oldest = values.iterator().next()
|
||||
values.remove(oldest)
|
||||
}
|
||||
}
|
||||
|
||||
fun consume(value: T): Boolean = synchronized(lock) {
|
||||
values.remove(value)
|
||||
}
|
||||
|
||||
internal fun size(): Int = synchronized(lock) { values.size }
|
||||
}
|
||||
@@ -24,7 +24,7 @@ object SafDownloadHandler {
|
||||
// cannot interleave writes into one document. Different names keep
|
||||
// downloading in parallel; the second same-name caller blocks, then hits
|
||||
// the exists check and reports already_exists.
|
||||
private val safNameLocks = java.util.concurrent.ConcurrentHashMap<String, Any>()
|
||||
private val safNameLocks = KeyedLockPool<String>()
|
||||
|
||||
data class UniqueWriteResult(val uri: String, val fileName: String)
|
||||
data class ExistingAwareWriteResult(
|
||||
@@ -40,8 +40,7 @@ object SafDownloadHandler {
|
||||
block: () -> T
|
||||
): T {
|
||||
val key = "$treeUriStr|$relativeDir|${fileName.lowercase(Locale.ROOT)}"
|
||||
val lock = safNameLocks.computeIfAbsent(key) { Any() }
|
||||
return synchronized(lock) { block() }
|
||||
return safNameLocks.withLock(key, block)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.zarz.spotiflac
|
||||
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.concurrent.thread
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class NativeResourcePoolsTest {
|
||||
@Test
|
||||
fun keyedLockPoolReclaimsAnIdleKey() {
|
||||
val pool = KeyedLockPool<String>()
|
||||
|
||||
assertEquals("done", pool.withLock("track.flac") { "done" })
|
||||
assertEquals(0, pool.activeKeyCount())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun keyedLockPoolKeepsOneMonitorWhileAnotherCallerWaits() {
|
||||
val pool = KeyedLockPool<String>()
|
||||
val firstEntered = CountDownLatch(1)
|
||||
val releaseFirst = CountDownLatch(1)
|
||||
val secondEntered = CountDownLatch(1)
|
||||
|
||||
val first = thread {
|
||||
pool.withLock("same-name.flac") {
|
||||
firstEntered.countDown()
|
||||
releaseFirst.await(2, TimeUnit.SECONDS)
|
||||
}
|
||||
}
|
||||
assertTrue(firstEntered.await(2, TimeUnit.SECONDS))
|
||||
|
||||
val second = thread {
|
||||
pool.withLock("same-name.flac") {
|
||||
secondEntered.countDown()
|
||||
}
|
||||
}
|
||||
assertFalse(secondEntered.await(100, TimeUnit.MILLISECONDS))
|
||||
assertEquals(1, pool.activeKeyCount())
|
||||
|
||||
releaseFirst.countDown()
|
||||
first.join(2_000)
|
||||
second.join(2_000)
|
||||
assertTrue(secondEntered.await(100, TimeUnit.MILLISECONDS))
|
||||
assertEquals(0, pool.activeKeyCount())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun boundedRegistryConsumesIdsAndEvictsOldestEntries() {
|
||||
val registry = BoundedRegistry<Long>(maxEntries = 2)
|
||||
registry.add(1)
|
||||
registry.add(2)
|
||||
registry.add(3)
|
||||
|
||||
assertFalse(registry.consume(1))
|
||||
assertTrue(registry.consume(2))
|
||||
assertTrue(registry.consume(3))
|
||||
assertEquals(0, registry.size())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user