Причина синхронизации и временная метка в логах
This commit is contained in:
@@ -4,6 +4,7 @@ import com.github.nullptroma.wallenc.domain.datatypes.StorageSyncPaths
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager
|
||||
import com.github.nullptroma.wallenc.ui.R
|
||||
import com.github.nullptroma.wallenc.ui.resources.UiStringResolver
|
||||
import com.github.nullptroma.wallenc.domain.tasks.StorageSyncTriggerReason
|
||||
import com.github.nullptroma.wallenc.usecases.RunStorageSyncUseCase
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -61,7 +62,7 @@ class StorageSyncBootstrap @Inject constructor(
|
||||
}
|
||||
syncRunner.enqueue(
|
||||
displayTitle = uiStrings(R.string.task_title_storage_sync_background),
|
||||
logReason = "debounce",
|
||||
reason = StorageSyncTriggerReason.Debounce,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import androidx.hilt.work.HiltWorker
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.github.nullptroma.wallenc.domain.tasks.StorageSyncTriggerReason
|
||||
import com.github.nullptroma.wallenc.usecases.RunStorageSyncUseCase
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
@@ -19,7 +20,7 @@ class StorageSyncWorker @AssistedInject constructor(
|
||||
override suspend fun doWork(): Result {
|
||||
Timber.d("Periodic storage sync started (attempt %d)", runAttemptCount)
|
||||
return runCatching {
|
||||
syncRunner.runBlocking()
|
||||
syncRunner.runBlocking(StorageSyncTriggerReason.Background)
|
||||
Timber.d("Periodic storage sync finished")
|
||||
Result.success()
|
||||
}.getOrElse { error ->
|
||||
|
||||
@@ -20,4 +20,7 @@ interface ITaskOrchestrator {
|
||||
fun cancel(taskId: TaskId): Boolean
|
||||
|
||||
fun cancelAll()
|
||||
|
||||
/** Запись в общий лог пайплайна вне контекста [TaskContext] (например, WorkManager sync). */
|
||||
fun appendPipelineLog(level: TaskLogLevel, key: TaskLogKey)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import java.util.UUID
|
||||
data class PipelineTask(
|
||||
val id: TaskId,
|
||||
val title: String,
|
||||
val enqueuedAtMs: Long,
|
||||
val dispatcher: CoroutineDispatcher,
|
||||
val state: TaskRunState,
|
||||
/** UUID storage, для которого идёт задача (кнопки только этой строки в UI). */
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.github.nullptroma.wallenc.domain.tasks
|
||||
|
||||
/** Источник запуска синхронизации хранилищ (для логов пайплайна задач). */
|
||||
enum class StorageSyncTriggerReason {
|
||||
Debounce,
|
||||
SyncTab,
|
||||
Background,
|
||||
}
|
||||
@@ -3,7 +3,10 @@ package com.github.nullptroma.wallenc.domain.tasks
|
||||
import com.github.nullptroma.wallenc.domain.errors.WallencException
|
||||
|
||||
sealed class TaskLogKey {
|
||||
data object SyncStarted : TaskLogKey()
|
||||
data object SyncFinished : TaskLogKey()
|
||||
data class SyncFailed(val error: WallencException) : TaskLogKey()
|
||||
data class SyncStarted(val reason: StorageSyncTriggerReason) : TaskLogKey()
|
||||
data class SyncFinished(val reason: StorageSyncTriggerReason) : TaskLogKey()
|
||||
data class SyncFailed(
|
||||
val error: WallencException,
|
||||
val reason: StorageSyncTriggerReason,
|
||||
) : TaskLogKey()
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ class TaskOrchestrator(
|
||||
val task = PipelineTask(
|
||||
id = id,
|
||||
title = title,
|
||||
enqueuedAtMs = System.currentTimeMillis(),
|
||||
dispatcher = dispatcher,
|
||||
state = TaskRunState.Queued,
|
||||
busyStorageUuid = busyStorageUuid,
|
||||
@@ -104,6 +105,10 @@ class TaskOrchestrator(
|
||||
}
|
||||
}
|
||||
|
||||
override fun appendPipelineLog(level: TaskLogLevel, key: TaskLogKey) {
|
||||
appendLogLine(level, message = "", logKey = key)
|
||||
}
|
||||
|
||||
private fun replaceTask(id: TaskId, fn: (PipelineTask) -> PipelineTask) {
|
||||
synchronized(tasksById) {
|
||||
val cur = tasksById[id] ?: return
|
||||
|
||||
@@ -1,13 +1,31 @@
|
||||
package com.github.nullptroma.wallenc.ui.resources
|
||||
|
||||
import com.github.nullptroma.wallenc.domain.tasks.StorageSyncTriggerReason
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskLogKey
|
||||
import com.github.nullptroma.wallenc.ui.R
|
||||
|
||||
fun TaskLogKey.resolve(resolver: UiStringResolver): String = when (this) {
|
||||
TaskLogKey.SyncStarted -> resolver(R.string.task_log_sync_started)
|
||||
TaskLogKey.SyncFinished -> resolver(R.string.task_log_sync_finished)
|
||||
is TaskLogKey.SyncStarted -> resolver(
|
||||
R.string.task_log_sync_started,
|
||||
resolver.resolveSyncTriggerReason(reason),
|
||||
)
|
||||
is TaskLogKey.SyncFinished -> resolver(
|
||||
R.string.task_log_sync_finished,
|
||||
resolver.resolveSyncTriggerReason(reason),
|
||||
)
|
||||
is TaskLogKey.SyncFailed -> {
|
||||
val notification = error.toUserNotification().resolve(resolver)
|
||||
resolver(R.string.task_log_sync_failed, notification)
|
||||
resolver(
|
||||
R.string.task_log_sync_failed,
|
||||
resolver.resolveSyncTriggerReason(reason),
|
||||
notification,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun UiStringResolver.resolveSyncTriggerReason(reason: StorageSyncTriggerReason): String =
|
||||
when (reason) {
|
||||
StorageSyncTriggerReason.Debounce -> this(R.string.task_sync_trigger_debounce)
|
||||
StorageSyncTriggerReason.SyncTab -> this(R.string.task_sync_trigger_sync_tab)
|
||||
StorageSyncTriggerReason.Background -> this(R.string.task_sync_trigger_background)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.github.nullptroma.wallenc.ui.resources
|
||||
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
private val pipelineTimeFormatter: DateTimeFormatter =
|
||||
DateTimeFormatter.ofPattern("HH:mm:ss")
|
||||
|
||||
fun formatTaskPipelineTime(timestampMs: Long): String =
|
||||
Instant.ofEpochMilli(timestampMs)
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.format(pipelineTimeFormatter)
|
||||
@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.AlertDialog
|
||||
@@ -36,7 +37,9 @@ import com.github.nullptroma.wallenc.domain.tasks.PipelineTask
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskLogLevel
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskRunState
|
||||
import com.github.nullptroma.wallenc.ui.R
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskLogLine
|
||||
import com.github.nullptroma.wallenc.ui.resources.displayText
|
||||
import com.github.nullptroma.wallenc.ui.resources.formatTaskPipelineTime
|
||||
import com.github.nullptroma.wallenc.ui.resources.resolveText
|
||||
import com.github.nullptroma.wallenc.ui.resources.toUserNotification
|
||||
|
||||
@@ -91,18 +94,7 @@ fun TaskPipelineScreen(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
items(logs.size) { i ->
|
||||
val line = logs[i]
|
||||
val prefix = when (line.level) {
|
||||
TaskLogLevel.Debug -> "D"
|
||||
TaskLogLevel.Info -> "I"
|
||||
TaskLogLevel.Warn -> "W"
|
||||
TaskLogLevel.Error -> "E"
|
||||
}
|
||||
Text(
|
||||
"[$prefix] ${line.displayText()}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
PipelineLogRow(line = logs[i])
|
||||
}
|
||||
}
|
||||
Button(
|
||||
@@ -178,14 +170,59 @@ fun TaskPipelineScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PipelineLogRow(line: TaskLogLine) {
|
||||
val prefix = when (line.level) {
|
||||
TaskLogLevel.Debug -> "D"
|
||||
TaskLogLevel.Info -> "I"
|
||||
TaskLogLevel.Warn -> "W"
|
||||
TaskLogLevel.Error -> "E"
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.Top,
|
||||
) {
|
||||
Text(
|
||||
text = "[$prefix] ${line.displayText()}",
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 8.dp),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Text(
|
||||
text = formatTaskPipelineTime(line.timestampMs),
|
||||
modifier = Modifier.widthIn(min = 56.dp),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TaskRow(task: PipelineTask, isRunning: Boolean) {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
task.title,
|
||||
text = task.title,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 8.dp),
|
||||
style = if (isRunning) MaterialTheme.typography.titleSmall
|
||||
else MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Text(
|
||||
text = formatTaskPipelineTime(task.enqueuedAtMs),
|
||||
modifier = Modifier.widthIn(min = 56.dp),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
val runningProgress = (task.state as? TaskRunState.Running)?.progress
|
||||
val progressLabel = runningProgress?.label?.resolveText()
|
||||
val stateLabel = when (val s = task.state) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.github.nullptroma.wallenc.ui.resources.resolve
|
||||
import com.github.nullptroma.wallenc.ui.resources.UserNotification
|
||||
import com.github.nullptroma.wallenc.usecases.AddStorageToSyncGroupResult
|
||||
import com.github.nullptroma.wallenc.usecases.ManageStorageSyncGroupsUseCase
|
||||
import com.github.nullptroma.wallenc.domain.tasks.StorageSyncTriggerReason
|
||||
import com.github.nullptroma.wallenc.usecases.RunStorageSyncUseCase
|
||||
import com.github.nullptroma.wallenc.usecases.StorageSyncCompatibilityInput
|
||||
import com.github.nullptroma.wallenc.usecases.isStorageCompatibleWithGroup
|
||||
@@ -209,7 +210,7 @@ class StorageSyncViewModel @Inject constructor(
|
||||
fun runSyncNow() {
|
||||
val started = runStorageSyncUseCase.enqueue(
|
||||
displayTitle = uiStrings(R.string.task_title_storage_sync),
|
||||
logReason = "sync-tab",
|
||||
reason = StorageSyncTriggerReason.SyncTab,
|
||||
)
|
||||
if (!started) {
|
||||
updateState(
|
||||
|
||||
@@ -289,9 +289,12 @@
|
||||
<string name="sync_progress_group_renewing_locks">Синхронизация: группа «%1$s» — продление блокировок</string>
|
||||
<string name="sync_progress_group_lock_renewal_failed">Синхронизация: группа «%1$s» — не удалось продлить блокировку</string>
|
||||
<string name="task_progress_clear_content">%1$d / %2$d</string>
|
||||
<string name="task_log_sync_started">Синхронизация хранилищ запущена</string>
|
||||
<string name="task_log_sync_finished">Синхронизация хранилищ завершена</string>
|
||||
<string name="task_log_sync_failed">Синхронизация не удалась: %1$s</string>
|
||||
<string name="task_log_sync_started">Синхронизация хранилищ запущена (%1$s)</string>
|
||||
<string name="task_log_sync_finished">Синхронизация хранилищ завершена (%1$s)</string>
|
||||
<string name="task_log_sync_failed">Синхронизация не удалась (%1$s): %2$s</string>
|
||||
<string name="task_sync_trigger_debounce">debounce</string>
|
||||
<string name="task_sync_trigger_sync_tab">sync-tab</string>
|
||||
<string name="task_sync_trigger_background">background</string>
|
||||
<string name="task_log_enumerating">Перечисление файлов и папок…</string>
|
||||
<string name="task_log_creating_storage">Создание хранилища…</string>
|
||||
<string name="task_log_storage_created">Хранилище создано</string>
|
||||
|
||||
@@ -289,9 +289,12 @@
|
||||
<string name="sync_progress_group_renewing_locks">Storage sync: group "%1$s" renewing locks</string>
|
||||
<string name="sync_progress_group_lock_renewal_failed">Storage sync: group "%1$s" lock renewal failed</string>
|
||||
<string name="task_progress_clear_content">%1$d / %2$d</string>
|
||||
<string name="task_log_sync_started">Storage sync started</string>
|
||||
<string name="task_log_sync_finished">Storage sync finished</string>
|
||||
<string name="task_log_sync_failed">Storage sync failed: %1$s</string>
|
||||
<string name="task_log_sync_started">Storage sync started (%1$s)</string>
|
||||
<string name="task_log_sync_finished">Storage sync finished (%1$s)</string>
|
||||
<string name="task_log_sync_failed">Storage sync failed (%1$s): %2$s</string>
|
||||
<string name="task_sync_trigger_debounce">debounce</string>
|
||||
<string name="task_sync_trigger_sync_tab">sync-tab</string>
|
||||
<string name="task_sync_trigger_background">background</string>
|
||||
<string name="task_log_enumerating">Enumerating files and directories…</string>
|
||||
<string name="task_log_creating_storage">Creating storage…</string>
|
||||
<string name="task_log_storage_created">Storage created</string>
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.github.nullptroma.wallenc.usecases
|
||||
import com.github.nullptroma.wallenc.domain.errors.toWallencException
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IStorageSyncEngine
|
||||
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
|
||||
import com.github.nullptroma.wallenc.domain.tasks.StorageSyncTriggerReason
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskId
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskLogKey
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskLogLevel
|
||||
@@ -31,10 +32,10 @@ class RunStorageSyncUseCase @Inject constructor(
|
||||
|
||||
/**
|
||||
* @param displayTitle заголовок задачи в UI (локализованный на стороне вызова)
|
||||
* @param logReason техническая метка для логов (не для UI)
|
||||
* @param reason источник запуска — попадает в лог пайплайна
|
||||
* @return false, если синхронизация уже в очереди или выполняется — новая задача не создана
|
||||
*/
|
||||
fun enqueue(displayTitle: String, logReason: String): Boolean {
|
||||
fun enqueue(displayTitle: String, reason: StorageSyncTriggerReason): Boolean {
|
||||
if (!running.compareAndSet(false, true)) {
|
||||
return false
|
||||
}
|
||||
@@ -45,36 +46,68 @@ class RunStorageSyncUseCase @Inject constructor(
|
||||
dispatcher = Dispatchers.IO,
|
||||
work = { ctx ->
|
||||
try {
|
||||
ctx.log(TaskLogLevel.Info, TaskLogKey.SyncStarted)
|
||||
ctx.reportProgress(null, TaskProgressLabel.SyncStarted)
|
||||
syncEngine.syncAllGroups { fraction, label ->
|
||||
executeSync(
|
||||
reason = reason,
|
||||
reportProgress = { fraction, label ->
|
||||
ctx.reportProgress(fraction, label)
|
||||
}
|
||||
ctx.log(TaskLogLevel.Info, TaskLogKey.SyncFinished)
|
||||
ctx.reportProgress(null, TaskProgressLabel.SyncCompleted)
|
||||
},
|
||||
log = { level, key -> ctx.log(level, key) },
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
val err = e.toWallencException()
|
||||
ctx.log(TaskLogLevel.Error, TaskLogKey.SyncFailed(err))
|
||||
ctx.fail(err)
|
||||
ctx.fail(e.toWallencException())
|
||||
} finally {
|
||||
running.set(false)
|
||||
_syncRunning.value = false
|
||||
_activeSyncTaskId.value = null
|
||||
clearRunningState()
|
||||
}
|
||||
},
|
||||
)
|
||||
_activeSyncTaskId.value = taskId
|
||||
return true
|
||||
} catch (t: Throwable) {
|
||||
running.set(false)
|
||||
_syncRunning.value = false
|
||||
_activeSyncTaskId.value = null
|
||||
clearRunningState()
|
||||
throw t
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun runBlocking() {
|
||||
suspend fun runBlocking(reason: StorageSyncTriggerReason) {
|
||||
if (!running.compareAndSet(false, true)) {
|
||||
return
|
||||
}
|
||||
_syncRunning.value = true
|
||||
try {
|
||||
executeSync(
|
||||
reason = reason,
|
||||
reportProgress = { _, _ -> },
|
||||
log = { level, key -> orchestrator.appendPipelineLog(level, key) },
|
||||
)
|
||||
} finally {
|
||||
clearRunningState()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun executeSync(
|
||||
reason: StorageSyncTriggerReason,
|
||||
reportProgress: suspend (fraction: Float?, label: TaskProgressLabel?) -> Unit,
|
||||
log: (TaskLogLevel, TaskLogKey) -> Unit,
|
||||
) {
|
||||
syncReadiness.awaitReady()
|
||||
syncEngine.syncAllGroups()
|
||||
log(TaskLogLevel.Info, TaskLogKey.SyncStarted(reason))
|
||||
reportProgress(null, TaskProgressLabel.SyncStarted)
|
||||
try {
|
||||
syncEngine.syncAllGroups { fraction, label ->
|
||||
reportProgress(fraction, label)
|
||||
}
|
||||
log(TaskLogLevel.Info, TaskLogKey.SyncFinished(reason))
|
||||
reportProgress(null, TaskProgressLabel.SyncCompleted)
|
||||
} catch (e: Exception) {
|
||||
val err = e.toWallencException()
|
||||
log(TaskLogLevel.Error, TaskLogKey.SyncFailed(err, reason))
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearRunningState() {
|
||||
running.set(false)
|
||||
_syncRunning.value = false
|
||||
_activeSyncTaskId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user