Retry file resource cleanup when the core stops
This commit is contained in:
1 parent
a8c9bc6049
commit
5db51e64de
4 files changed
+24
-4
No files matched your search
@@ -40,7 +40,7 @@ class FileStatusException(val status: Int) : IOException("member_files status=$s
|
||||
* URI копируется на Dispatchers.IO в стабильный upload-source. При отмене transfer ждёт terminal
|
||||
* native-событие (закрытый handle) до удаления своего source/destination; остановка core завершает ожидания.
|
||||
* Работа удерживает wake lock и CoreActivity.transfer, ожидание решения о замене снимает оба удержания.
|
||||
* Ошибки cleanup логируются независимо; неосвобождённый wake lock сохраняется для следующей попытки.
|
||||
* Ошибки cleanup логируются независимо; неосвобождённый wake lock сохраняется до следующей операции/остановки core.
|
||||
* Скачанная копия остаётся в cacheDir/member_files для внешнего приложения/повторного SAVE.
|
||||
*/
|
||||
class MemberFilesRepository(context: Context) {
|
||||
@@ -86,7 +86,7 @@ class MemberFilesRepository(context: Context) {
|
||||
private fun protect(active: Boolean) {
|
||||
if (active) {
|
||||
val lock = wakeLock ?: (app.getSystemService(Context.POWER_SERVICE) as PowerManager)
|
||||
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "utun:member_files").also { wakeLock = it }
|
||||
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "utun:member_files").also { wakeLock = it; it.setReferenceCounted(false) }
|
||||
if (!lock.isHeld) {
|
||||
lock.acquire()
|
||||
log("DEBUG", "wake lock acquired token=${_operation.value?.token}")
|
||||
@@ -337,6 +337,10 @@ class MemberFilesRepository(context: Context) {
|
||||
val error = CancellationException("core stopped")
|
||||
pending.values.toList().forEach { it.result.completeExceptionally(error) }
|
||||
transfers.values.toList().forEach { it.result.completeExceptionally(error) }
|
||||
if (task?.isCompleted != false && wakeLock != null) {
|
||||
protect(false)
|
||||
log("DEBUG", "core stop cleanup wakePending=${wakeLock != null}")
|
||||
}
|
||||
CoreActivity.reset()
|
||||
}
|
||||
fun dismiss() { if (_operation.value?.active == false) _operation.value = null; HeadlessService.refreshFiles() }
|
||||
|
||||
+3
-2
@@ -92,7 +92,8 @@ class MemberFilesViewModel(private val saved: SavedStateHandle) : ViewModel() {
|
||||
membersRepository.refresh(group)
|
||||
}
|
||||
|
||||
/** Открыть явный адрес, например из уведомления передачи, и обновить участников; server самовольно не заменяется. */
|
||||
/** Открыть явный адрес, например из уведомления передачи, и обновить участников.
|
||||
* Ненулевой server сохраняется; при server=0 выбирается первый известный хранитель. */
|
||||
fun openLocation(location: FileLocation) {
|
||||
navigate(location)
|
||||
val group = location.group.toULong().toString()
|
||||
@@ -207,7 +208,7 @@ class MemberFilesViewModel(private val saved: SavedStateHandle) : ViewModel() {
|
||||
}
|
||||
|
||||
/** MKDIR использует name, RENAME — name и entry, REMOVE — entry; работает только в собственном дереве вне avatars.
|
||||
* Конкурирующая мутация/активная передача отклоняет запуск. При успехе перечитывает текущий каталог;
|
||||
* Конкурирующая мутация/активная операция репозитория отклоняет запуск. При успехе перечитывает текущий каталог;
|
||||
* ошибка задаёт status и логируется, CancellationException передаётся дальше. Отмена не откатывает серверный commit. */
|
||||
fun mutate(command: Int, name: String = "", entry: MemberFilesProtocol.Entry? = null) {
|
||||
val location = _state.value.location ?: return
|
||||
|
||||
@@ -13,10 +13,13 @@ class PowerManager {
|
||||
return WakeLock(flags, tag, failures).also { locks.add(it) }
|
||||
}
|
||||
class WakeLock(val flags: Int, val tag: String, var failures: Set<String>) {
|
||||
var counted = true
|
||||
private set
|
||||
var timeout = 0L
|
||||
var releaseCalls = 0
|
||||
var held = false
|
||||
val isHeld: Boolean get() { check("query" !in failures) { "wake lock query unavailable" }; return held }
|
||||
fun setReferenceCounted(value: Boolean) { counted = value }
|
||||
fun acquire() = acquire(0)
|
||||
fun acquire(timeout: Long) {
|
||||
this.timeout = timeout
|
||||
|
||||
+12
@@ -147,6 +147,7 @@ class MemberFilesFailureTest {
|
||||
repository.save(export(), uri)
|
||||
pumpUntil(::finished)
|
||||
val lock = context.powerManager!!.locks.single()
|
||||
assertFalse("one repository owner must allow repeated release without reference-count underflow", lock.counted)
|
||||
assertTrue(lock.held)
|
||||
assertEquals(listOf(true, false), NativeLib.activityRequests)
|
||||
diagnostic("wake lock release failed")
|
||||
@@ -157,6 +158,17 @@ class MemberFilesFailureTest {
|
||||
assertFalse(lock.held)
|
||||
}
|
||||
|
||||
@Test fun coreStopRetriesWakeReleaseForAnAlreadyFinishedOperation() = runTest(dispatcher) {
|
||||
context.powerManager!!.failures = setOf("release")
|
||||
repository.save(export(), uri)
|
||||
pumpUntil(::finished)
|
||||
val lock = context.powerManager!!.locks.single()
|
||||
assertTrue(lock.held)
|
||||
lock.failures = emptySet()
|
||||
repository.coreStopped()
|
||||
assertFalse("core stop left a completed operation's lock held", lock.held)
|
||||
}
|
||||
|
||||
@Test fun standbyFailureDuringCleanupIsLoggedWithoutLosingCompletedExport() = runTest(dispatcher) {
|
||||
context.contentResolver.output = { NativeLib.failActivityRequest = true; java.io.ByteArrayOutputStream() }
|
||||
repository.save(export(), uri)
|
||||
|
||||
Reference in new issue
Block a user