Add a periodic DB vacuuming task

This commit is contained in:
Jorge Martín 2025-12-05 14:19:36 +01:00 committed by Jorge Martin Espinosa
parent 5d6aa1fcfd
commit 734485255a
22 changed files with 172 additions and 21 deletions

View file

@ -75,7 +75,10 @@ import io.element.android.libraries.matrix.impl.util.SessionPathsProvider
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
import io.element.android.libraries.matrix.impl.verification.RustSessionVerificationService
import io.element.android.libraries.matrix.impl.workmanager.PerformDatabaseVacuumWorkManagerRequest
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.workmanager.api.WorkManagerRequestType
import io.element.android.libraries.workmanager.api.WorkManagerScheduler
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.collections.immutable.ImmutableList
@ -133,6 +136,7 @@ class RustMatrixClient(
timelineEventTypeFilterFactory: TimelineEventTypeFilterFactory,
private val featureFlagService: FeatureFlagService,
private val analyticsService: AnalyticsService,
private val workManagerScheduler: WorkManagerScheduler,
) : MatrixClient {
override val sessionId: UserId = UserId(innerClient.userId())
override val deviceId: DeviceId = DeviceId(innerClient.deviceId())
@ -276,6 +280,9 @@ class RustMatrixClient(
// Force a refresh of the profile
getUserProfile()
}
// Schedule regular database vacuuming to ensure DB performance remains optimal
scheduleDatabaseVacuum()
}
override fun userIdServerName(): String {
@ -726,8 +733,9 @@ class RustMatrixClient(
}
}
override suspend fun vacuumStores(): Result<Unit> = withContext(sessionDispatcher) {
override suspend fun performDatabaseVacuum(): Result<Unit> = withContext(sessionDispatcher) {
runCatchingExceptions {
Timber.d("Performing database vacuuming for session $sessionId...")
innerClient.optimizeStores()
}
}
@ -756,6 +764,15 @@ class RustMatrixClient(
// Delete all the files for this session
sessionPathsProvider.provides(sessionId)?.deleteRecursively()
}
private fun scheduleDatabaseVacuum() {
// If there's already a periodic work request, do not schedule another one
if (workManagerScheduler.hasPendingWork(sessionId, WorkManagerRequestType.DB_VACUUM)) return
Timber.i("Scheduling periodic database vacuuming for session $sessionId")
val request = PerformDatabaseVacuumWorkManagerRequest(sessionId)
workManagerScheduler.submit(request)
}
}
private val defaultRoomCreationPowerLevels = PowerLevels(

View file

@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.impl.util.anonymizedTokens
import io.element.android.libraries.network.useragent.UserAgentProvider
import io.element.android.libraries.sessionstorage.api.SessionData
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.workmanager.api.WorkManagerScheduler
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.CoroutineScope
@ -63,6 +64,7 @@ class RustMatrixClientFactory(
private val timelineEventTypeFilterFactory: TimelineEventTypeFilterFactory,
private val clientBuilderProvider: ClientBuilderProvider,
private val sqliteStoreBuilderProvider: SqliteStoreBuilderProvider,
private val workManagerScheduler: WorkManagerScheduler,
) {
private val sessionDelegate = RustClientSessionDelegate(sessionStore, appCoroutineScope, coroutineDispatchers)
@ -116,6 +118,7 @@ class RustMatrixClientFactory(
timelineEventTypeFilterFactory = timelineEventTypeFilterFactory,
featureFlagService = featureFlagService,
analyticsService = analyticsService,
workManagerScheduler = workManagerScheduler,
).also {
Timber.tag(it.toString()).d("Creating Client with access token '$anonymizedAccessToken' and refresh token '$anonymizedRefreshToken'")
}

View file

@ -0,0 +1,40 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.workmanager
import androidx.work.Constraints
import androidx.work.Data
import androidx.work.PeriodicWorkRequest
import androidx.work.WorkRequest
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.impl.workmanager.VacuumDatabaseWorker.Companion.SESSION_ID_PARAM
import io.element.android.libraries.workmanager.api.WorkManagerRequest
import io.element.android.libraries.workmanager.api.WorkManagerRequestType
import io.element.android.libraries.workmanager.api.workManagerTag
import java.util.concurrent.TimeUnit
class PerformDatabaseVacuumWorkManagerRequest(
private val sessionId: SessionId,
) : WorkManagerRequest {
override fun build(): Result<List<WorkRequest>> {
val data = Data.Builder().putString(SESSION_ID_PARAM, sessionId.value).build()
val workRequest = PeriodicWorkRequest.Builder(
workerClass = VacuumDatabaseWorker::class,
// Run once a day
repeatInterval = 1,
repeatIntervalTimeUnit = TimeUnit.DAYS,
)
.addTag(workManagerTag(sessionId, WorkManagerRequestType.DB_VACUUM))
.setInputData(data)
// Only run when the device is idle to avoid impacting user experience
.setConstraints(Constraints.Builder().setRequiresDeviceIdle(true).build())
.build()
return Result.success(listOf(workRequest))
}
}

View file

@ -0,0 +1,38 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.workmanager
import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.core.SessionId
@AssistedInject
class VacuumDatabaseWorker(
@Assisted workerParams: WorkerParameters,
@ApplicationContext private val context: Context,
private val matrixClientProvider: MatrixClientProvider,
) : CoroutineWorker(context, workerParams) {
companion object {
const val SESSION_ID_PARAM = "session_id"
}
override suspend fun doWork(): Result {
val sessionId = inputData.getString(SESSION_ID_PARAM)?.let(::SessionId) ?: return Result.failure()
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return Result.failure()
return client.performDatabaseVacuum()
.fold(
onSuccess = { Result.success() },
onFailure = { Result.failure() }
)
}
}

View file

@ -19,8 +19,11 @@ import io.element.android.libraries.network.useragent.SimpleUserAgentProvider
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.aSessionData
import io.element.android.libraries.workmanager.api.WorkManagerRequest
import io.element.android.libraries.workmanager.test.FakeWorkManagerScheduler
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
@ -30,9 +33,14 @@ import java.io.File
class RustMatrixClientFactoryTest {
@Test
fun test() = runTest {
val sut = createRustMatrixClientFactory()
val scheduleVacuumLambda = lambdaRecorder<WorkManagerRequest, Unit> {}
val workManagerScheduler = FakeWorkManagerScheduler(submitLambda = scheduleVacuumLambda)
val sut = createRustMatrixClientFactory(workManagerScheduler = workManagerScheduler)
val result = sut.create(aSessionData())
assertThat(result.sessionId).isEqualTo(SessionId("@alice:server.org"))
scheduleVacuumLambda.assertions().isCalledOnce()
result.destroy()
}
}
@ -43,6 +51,7 @@ fun TestScope.createRustMatrixClientFactory(
updateUserProfileResult = { _, _, _ -> },
),
clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(),
workManagerScheduler: FakeWorkManagerScheduler = FakeWorkManagerScheduler(),
) = RustMatrixClientFactory(
cacheDirectory = cacheDirectory,
appCoroutineScope = backgroundScope,
@ -57,4 +66,5 @@ fun TestScope.createRustMatrixClientFactory(
timelineEventTypeFilterFactory = FakeTimelineEventTypeFilterFactory(),
clientBuilderProvider = clientBuilderProvider,
sqliteStoreBuilderProvider = FakeSqliteStoreBuilderProvider(),
workManagerScheduler = workManagerScheduler,
)

View file

@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.aSessionData
import io.element.android.libraries.workmanager.test.FakeWorkManagerScheduler
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
import io.element.android.tests.testutils.lambda.lambdaRecorder
@ -116,5 +117,6 @@ class RustMatrixClientTest {
timelineEventTypeFilterFactory = FakeTimelineEventTypeFilterFactory(),
featureFlagService = FakeFeatureFlagService(),
analyticsService = FakeAnalyticsService(),
workManagerScheduler = FakeWorkManagerScheduler(submitLambda = {}),
)
}