Add a periodic DB vacuuming task
This commit is contained in:
parent
9c72310cb4
commit
482d7e0648
22 changed files with 172 additions and 21 deletions
|
|
@ -12,7 +12,7 @@ import dev.zacsweers.metro.ContributesBinding
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
interface VacuumStoresUseCase {
|
fun interface VacuumStoresUseCase {
|
||||||
suspend operator fun invoke()
|
suspend operator fun invoke()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -21,7 +21,7 @@ class DefaultVacuumStoresUseCase(
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
) : VacuumStoresUseCase {
|
) : VacuumStoresUseCase {
|
||||||
override suspend fun invoke() {
|
override suspend fun invoke() {
|
||||||
matrixClient.vacuumStores()
|
matrixClient.performDatabaseVacuum()
|
||||||
.onFailure { Timber.e(it, "Failed to vacuum stores") }
|
.onFailure { Timber.e(it, "Failed to vacuum stores") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import io.element.android.features.enterprise.test.FakeEnterpriseService
|
||||||
import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem
|
import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem
|
||||||
import io.element.android.features.preferences.impl.tasks.FakeClearCacheUseCase
|
import io.element.android.features.preferences.impl.tasks.FakeClearCacheUseCase
|
||||||
import io.element.android.features.preferences.impl.tasks.FakeComputeCacheSizeUseCase
|
import io.element.android.features.preferences.impl.tasks.FakeComputeCacheSizeUseCase
|
||||||
|
import io.element.android.features.preferences.impl.tasks.VacuumStoresUseCase
|
||||||
import io.element.android.features.rageshake.api.preferences.aRageshakePreferencesState
|
import io.element.android.features.rageshake.api.preferences.aRageshakePreferencesState
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
|
@ -212,6 +213,23 @@ class DeveloperSettingsPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - VacuumStores action invokes the VacuumStoresUseCase`() = runTest {
|
||||||
|
var vacuumCalled = false
|
||||||
|
val presenter = createDeveloperSettingsPresenter(
|
||||||
|
vacuumStoresUseCase = VacuumStoresUseCase {
|
||||||
|
vacuumCalled = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
val state = awaitItem()
|
||||||
|
assertThat(vacuumCalled).isFalse()
|
||||||
|
state.eventSink(DeveloperSettingsEvents.VacuumStores)
|
||||||
|
skipItems(1)
|
||||||
|
assertThat(vacuumCalled).isTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun createDeveloperSettingsPresenter(
|
private fun createDeveloperSettingsPresenter(
|
||||||
sessionId: SessionId = A_SESSION_ID,
|
sessionId: SessionId = A_SESSION_ID,
|
||||||
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(
|
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(
|
||||||
|
|
@ -230,6 +248,7 @@ class DeveloperSettingsPresenterTest {
|
||||||
preferencesStore: InMemoryAppPreferencesStore = InMemoryAppPreferencesStore(),
|
preferencesStore: InMemoryAppPreferencesStore = InMemoryAppPreferencesStore(),
|
||||||
buildMeta: BuildMeta = aBuildMeta(),
|
buildMeta: BuildMeta = aBuildMeta(),
|
||||||
enterpriseService: EnterpriseService = FakeEnterpriseService(),
|
enterpriseService: EnterpriseService = FakeEnterpriseService(),
|
||||||
|
vacuumStoresUseCase: VacuumStoresUseCase = VacuumStoresUseCase {},
|
||||||
): DeveloperSettingsPresenter {
|
): DeveloperSettingsPresenter {
|
||||||
return DeveloperSettingsPresenter(
|
return DeveloperSettingsPresenter(
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
|
|
@ -240,6 +259,7 @@ class DeveloperSettingsPresenterTest {
|
||||||
appPreferencesStore = preferencesStore,
|
appPreferencesStore = preferencesStore,
|
||||||
buildMeta = buildMeta,
|
buildMeta = buildMeta,
|
||||||
enterpriseService = enterpriseService,
|
enterpriseService = enterpriseService,
|
||||||
|
vacuumStoresUseCase = vacuumStoresUseCase,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -195,7 +195,7 @@ interface MatrixClient {
|
||||||
*/
|
*/
|
||||||
suspend fun markRoomAsFullyRead(roomId: RoomId, eventId: EventId): Result<Unit>
|
suspend fun markRoomAsFullyRead(roomId: RoomId, eventId: EventId): Result<Unit>
|
||||||
|
|
||||||
suspend fun vacuumStores(): Result<Unit>
|
suspend fun performDatabaseVacuum(): Result<Unit>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ dependencies {
|
||||||
implementation(projects.libraries.featureflag.api)
|
implementation(projects.libraries.featureflag.api)
|
||||||
implementation(projects.libraries.network)
|
implementation(projects.libraries.network)
|
||||||
implementation(projects.libraries.preferences.api)
|
implementation(projects.libraries.preferences.api)
|
||||||
|
implementation(projects.libraries.workmanager.api)
|
||||||
implementation(projects.services.analytics.api)
|
implementation(projects.services.analytics.api)
|
||||||
implementation(projects.services.toolbox.api)
|
implementation(projects.services.toolbox.api)
|
||||||
api(projects.libraries.matrix.api)
|
api(projects.libraries.matrix.api)
|
||||||
|
|
@ -49,6 +50,7 @@ dependencies {
|
||||||
testImplementation(projects.libraries.preferences.test)
|
testImplementation(projects.libraries.preferences.test)
|
||||||
testImplementation(projects.libraries.previewutils)
|
testImplementation(projects.libraries.previewutils)
|
||||||
testImplementation(projects.libraries.sessionStorage.test)
|
testImplementation(projects.libraries.sessionStorage.test)
|
||||||
|
testImplementation(projects.libraries.workmanager.test)
|
||||||
testImplementation(projects.services.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
testImplementation(projects.services.toolbox.test)
|
testImplementation(projects.services.toolbox.test)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.cancelAndDestroy
|
||||||
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
|
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.verification.RustSessionVerificationService
|
||||||
|
import io.element.android.libraries.matrix.impl.workmanager.PerformDatabaseVacuumWorkManagerRequest
|
||||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
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.analytics.api.AnalyticsService
|
||||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
@ -133,6 +136,7 @@ class RustMatrixClient(
|
||||||
timelineEventTypeFilterFactory: TimelineEventTypeFilterFactory,
|
timelineEventTypeFilterFactory: TimelineEventTypeFilterFactory,
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
|
private val workManagerScheduler: WorkManagerScheduler,
|
||||||
) : MatrixClient {
|
) : MatrixClient {
|
||||||
override val sessionId: UserId = UserId(innerClient.userId())
|
override val sessionId: UserId = UserId(innerClient.userId())
|
||||||
override val deviceId: DeviceId = DeviceId(innerClient.deviceId())
|
override val deviceId: DeviceId = DeviceId(innerClient.deviceId())
|
||||||
|
|
@ -276,6 +280,9 @@ class RustMatrixClient(
|
||||||
// Force a refresh of the profile
|
// Force a refresh of the profile
|
||||||
getUserProfile()
|
getUserProfile()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Schedule regular database vacuuming to ensure DB performance remains optimal
|
||||||
|
scheduleDatabaseVacuum()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun userIdServerName(): String {
|
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 {
|
runCatchingExceptions {
|
||||||
|
Timber.d("Performing database vacuuming for session $sessionId...")
|
||||||
innerClient.optimizeStores()
|
innerClient.optimizeStores()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -756,6 +764,15 @@ class RustMatrixClient(
|
||||||
// Delete all the files for this session
|
// Delete all the files for this session
|
||||||
sessionPathsProvider.provides(sessionId)?.deleteRecursively()
|
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(
|
private val defaultRoomCreationPowerLevels = PowerLevels(
|
||||||
|
|
|
||||||
|
|
@ -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.network.useragent.UserAgentProvider
|
||||||
import io.element.android.libraries.sessionstorage.api.SessionData
|
import io.element.android.libraries.sessionstorage.api.SessionData
|
||||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
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.analytics.api.AnalyticsService
|
||||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -63,6 +64,7 @@ class RustMatrixClientFactory(
|
||||||
private val timelineEventTypeFilterFactory: TimelineEventTypeFilterFactory,
|
private val timelineEventTypeFilterFactory: TimelineEventTypeFilterFactory,
|
||||||
private val clientBuilderProvider: ClientBuilderProvider,
|
private val clientBuilderProvider: ClientBuilderProvider,
|
||||||
private val sqliteStoreBuilderProvider: SqliteStoreBuilderProvider,
|
private val sqliteStoreBuilderProvider: SqliteStoreBuilderProvider,
|
||||||
|
private val workManagerScheduler: WorkManagerScheduler,
|
||||||
) {
|
) {
|
||||||
private val sessionDelegate = RustClientSessionDelegate(sessionStore, appCoroutineScope, coroutineDispatchers)
|
private val sessionDelegate = RustClientSessionDelegate(sessionStore, appCoroutineScope, coroutineDispatchers)
|
||||||
|
|
||||||
|
|
@ -116,6 +118,7 @@ class RustMatrixClientFactory(
|
||||||
timelineEventTypeFilterFactory = timelineEventTypeFilterFactory,
|
timelineEventTypeFilterFactory = timelineEventTypeFilterFactory,
|
||||||
featureFlagService = featureFlagService,
|
featureFlagService = featureFlagService,
|
||||||
analyticsService = analyticsService,
|
analyticsService = analyticsService,
|
||||||
|
workManagerScheduler = workManagerScheduler,
|
||||||
).also {
|
).also {
|
||||||
Timber.tag(it.toString()).d("Creating Client with access token '$anonymizedAccessToken' and refresh token '$anonymizedRefreshToken'")
|
Timber.tag(it.toString()).d("Creating Client with access token '$anonymizedAccessToken' and refresh token '$anonymizedRefreshToken'")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.api.SessionStore
|
||||||
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
||||||
import io.element.android.libraries.sessionstorage.test.aSessionData
|
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.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
|
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 io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|
@ -30,9 +33,14 @@ import java.io.File
|
||||||
class RustMatrixClientFactoryTest {
|
class RustMatrixClientFactoryTest {
|
||||||
@Test
|
@Test
|
||||||
fun test() = runTest {
|
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())
|
val result = sut.create(aSessionData())
|
||||||
|
|
||||||
assertThat(result.sessionId).isEqualTo(SessionId("@alice:server.org"))
|
assertThat(result.sessionId).isEqualTo(SessionId("@alice:server.org"))
|
||||||
|
scheduleVacuumLambda.assertions().isCalledOnce()
|
||||||
result.destroy()
|
result.destroy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -43,6 +51,7 @@ fun TestScope.createRustMatrixClientFactory(
|
||||||
updateUserProfileResult = { _, _, _ -> },
|
updateUserProfileResult = { _, _, _ -> },
|
||||||
),
|
),
|
||||||
clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(),
|
clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(),
|
||||||
|
workManagerScheduler: FakeWorkManagerScheduler = FakeWorkManagerScheduler(),
|
||||||
) = RustMatrixClientFactory(
|
) = RustMatrixClientFactory(
|
||||||
cacheDirectory = cacheDirectory,
|
cacheDirectory = cacheDirectory,
|
||||||
appCoroutineScope = backgroundScope,
|
appCoroutineScope = backgroundScope,
|
||||||
|
|
@ -57,4 +66,5 @@ fun TestScope.createRustMatrixClientFactory(
|
||||||
timelineEventTypeFilterFactory = FakeTimelineEventTypeFilterFactory(),
|
timelineEventTypeFilterFactory = FakeTimelineEventTypeFilterFactory(),
|
||||||
clientBuilderProvider = clientBuilderProvider,
|
clientBuilderProvider = clientBuilderProvider,
|
||||||
sqliteStoreBuilderProvider = FakeSqliteStoreBuilderProvider(),
|
sqliteStoreBuilderProvider = FakeSqliteStoreBuilderProvider(),
|
||||||
|
workManagerScheduler = workManagerScheduler,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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.api.SessionStore
|
||||||
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
||||||
import io.element.android.libraries.sessionstorage.test.aSessionData
|
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.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
|
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
|
@ -116,5 +117,6 @@ class RustMatrixClientTest {
|
||||||
timelineEventTypeFilterFactory = FakeTimelineEventTypeFilterFactory(),
|
timelineEventTypeFilterFactory = FakeTimelineEventTypeFilterFactory(),
|
||||||
featureFlagService = FakeFeatureFlagService(),
|
featureFlagService = FakeFeatureFlagService(),
|
||||||
analyticsService = FakeAnalyticsService(),
|
analyticsService = FakeAnalyticsService(),
|
||||||
|
workManagerScheduler = FakeWorkManagerScheduler(submitLambda = {}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,7 @@ class FakeMatrixClient(
|
||||||
private val getRecentEmojisLambda: () -> Result<List<String>> = { Result.success(emptyList()) },
|
private val getRecentEmojisLambda: () -> Result<List<String>> = { Result.success(emptyList()) },
|
||||||
private val addRecentEmojiLambda: (String) -> Result<Unit> = { Result.success(Unit) },
|
private val addRecentEmojiLambda: (String) -> Result<Unit> = { Result.success(Unit) },
|
||||||
private val markRoomAsFullyReadResult: (RoomId, EventId) -> Result<Unit> = { _, _ -> lambdaError() },
|
private val markRoomAsFullyReadResult: (RoomId, EventId) -> Result<Unit> = { _, _ -> lambdaError() },
|
||||||
|
private val performDatabaseVacuumLambda: () -> Result<Unit> = { lambdaError() },
|
||||||
) : MatrixClient {
|
) : MatrixClient {
|
||||||
var setDisplayNameCalled: Boolean = false
|
var setDisplayNameCalled: Boolean = false
|
||||||
private set
|
private set
|
||||||
|
|
@ -351,4 +352,8 @@ class FakeMatrixClient(
|
||||||
override suspend fun markRoomAsFullyRead(roomId: RoomId, eventId: EventId): Result<Unit> {
|
override suspend fun markRoomAsFullyRead(roomId: RoomId, eventId: EventId): Result<Unit> {
|
||||||
return markRoomAsFullyReadResult(roomId, eventId)
|
return markRoomAsFullyReadResult(roomId, eventId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun performDatabaseVacuum(): Result<Unit> {
|
||||||
|
return performDatabaseVacuumLambda()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.push.api.push.NotificationEventRequest
|
import io.element.android.libraries.push.api.push.NotificationEventRequest
|
||||||
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
|
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
|
||||||
import io.element.android.libraries.push.impl.workmanager.SyncNotificationWorkManagerRequest
|
import io.element.android.libraries.push.impl.workmanager.SyncNotificationWorkManagerRequest
|
||||||
import io.element.android.libraries.push.impl.workmanager.WorkerDataConverter
|
import io.element.android.libraries.push.impl.workmanager.SyncNotificationsWorkerDataConverter
|
||||||
import io.element.android.libraries.workmanager.api.WorkManagerScheduler
|
import io.element.android.libraries.workmanager.api.WorkManagerScheduler
|
||||||
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
|
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -50,7 +50,7 @@ class DefaultNotificationResolverQueue(
|
||||||
private val appCoroutineScope: CoroutineScope,
|
private val appCoroutineScope: CoroutineScope,
|
||||||
private val workManagerScheduler: WorkManagerScheduler,
|
private val workManagerScheduler: WorkManagerScheduler,
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
private val workerDataConverter: WorkerDataConverter,
|
private val workerDataConverter: SyncNotificationsWorkerDataConverter,
|
||||||
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
|
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
|
||||||
) : NotificationResolverQueue {
|
) : NotificationResolverQueue {
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ class FetchNotificationsWorker(
|
||||||
private val workManagerScheduler: WorkManagerScheduler,
|
private val workManagerScheduler: WorkManagerScheduler,
|
||||||
private val syncOnNotifiableEvent: SyncOnNotifiableEvent,
|
private val syncOnNotifiableEvent: SyncOnNotifiableEvent,
|
||||||
private val coroutineDispatchers: CoroutineDispatchers,
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
private val workerDataConverter: WorkerDataConverter,
|
private val workerDataConverter: SyncNotificationsWorkerDataConverter,
|
||||||
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
|
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
|
||||||
) : CoroutineWorker(context, workerParams) {
|
) : CoroutineWorker(context, workerParams) {
|
||||||
override suspend fun doWork(): Result = withContext(coroutineDispatchers.io) {
|
override suspend fun doWork(): Result = withContext(coroutineDispatchers.io) {
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ import java.security.InvalidParameterException
|
||||||
class SyncNotificationWorkManagerRequest(
|
class SyncNotificationWorkManagerRequest(
|
||||||
private val sessionId: SessionId,
|
private val sessionId: SessionId,
|
||||||
private val notificationEventRequests: List<NotificationEventRequest>,
|
private val notificationEventRequests: List<NotificationEventRequest>,
|
||||||
private val workerDataConverter: WorkerDataConverter,
|
private val workerDataConverter: SyncNotificationsWorkerDataConverter,
|
||||||
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
|
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
|
||||||
) : WorkManagerRequest {
|
) : WorkManagerRequest {
|
||||||
override fun build(): Result<List<WorkRequest>> {
|
override fun build(): Result<List<WorkRequest>> {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import io.element.android.libraries.push.api.push.NotificationEventRequest
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
class WorkerDataConverter(
|
class SyncNotificationsWorkerDataConverter(
|
||||||
private val json: JsonProvider,
|
private val json: JsonProvider,
|
||||||
) {
|
) {
|
||||||
fun serialize(notificationEventRequests: List<NotificationEventRequest>): Result<List<Data>> {
|
fun serialize(notificationEventRequests: List<NotificationEventRequest>): Result<List<Data>> {
|
||||||
|
|
@ -47,7 +47,7 @@ import io.element.android.libraries.push.impl.notifications.model.NotifiableEven
|
||||||
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
|
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
|
||||||
import io.element.android.libraries.push.impl.test.DefaultTestPush
|
import io.element.android.libraries.push.impl.test.DefaultTestPush
|
||||||
import io.element.android.libraries.push.impl.troubleshoot.DiagnosticPushHandler
|
import io.element.android.libraries.push.impl.troubleshoot.DiagnosticPushHandler
|
||||||
import io.element.android.libraries.push.impl.workmanager.WorkerDataConverter
|
import io.element.android.libraries.push.impl.workmanager.SyncNotificationsWorkerDataConverter
|
||||||
import io.element.android.libraries.pushproviders.api.PushData
|
import io.element.android.libraries.pushproviders.api.PushData
|
||||||
import io.element.android.libraries.pushstore.api.UserPushStore
|
import io.element.android.libraries.pushstore.api.UserPushStore
|
||||||
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
|
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
|
||||||
|
|
@ -718,7 +718,7 @@ class DefaultPushHandlerTest {
|
||||||
appCoroutineScope = backgroundScope,
|
appCoroutineScope = backgroundScope,
|
||||||
workManagerScheduler = workManagerScheduler,
|
workManagerScheduler = workManagerScheduler,
|
||||||
featureFlagService = featureFlagService,
|
featureFlagService = featureFlagService,
|
||||||
workerDataConverter = WorkerDataConverter(DefaultJsonProvider()),
|
workerDataConverter = SyncNotificationsWorkerDataConverter(DefaultJsonProvider()),
|
||||||
buildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(33),
|
buildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(33),
|
||||||
),
|
),
|
||||||
appCoroutineScope = backgroundScope,
|
appCoroutineScope = backgroundScope,
|
||||||
|
|
|
||||||
|
|
@ -177,7 +177,7 @@ class FetchNotificationWorkerTest {
|
||||||
workManagerScheduler = workManagerScheduler,
|
workManagerScheduler = workManagerScheduler,
|
||||||
syncOnNotifiableEvent = syncOnNotifiableEvent,
|
syncOnNotifiableEvent = syncOnNotifiableEvent,
|
||||||
coroutineDispatchers = testCoroutineDispatchers(),
|
coroutineDispatchers = testCoroutineDispatchers(),
|
||||||
workerDataConverter = WorkerDataConverter(DefaultJsonProvider()),
|
workerDataConverter = SyncNotificationsWorkerDataConverter(DefaultJsonProvider()),
|
||||||
buildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(33),
|
buildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(33),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ class SyncNotificationWorkManagerRequestTest {
|
||||||
val request = createSyncNotificationWorkManagerRequest(
|
val request = createSyncNotificationWorkManagerRequest(
|
||||||
sessionId = A_SESSION_ID,
|
sessionId = A_SESSION_ID,
|
||||||
notificationEventRequests = listOf(aNotificationEventRequest()),
|
notificationEventRequests = listOf(aNotificationEventRequest()),
|
||||||
workerDataConverter = WorkerDataConverter({ error("error during serialization") })
|
workerDataConverter = SyncNotificationsWorkerDataConverter({ error("error during serialization") })
|
||||||
)
|
)
|
||||||
val result = request.build()
|
val result = request.build()
|
||||||
assertThat(result.isFailure).isTrue()
|
assertThat(result.isFailure).isTrue()
|
||||||
|
|
@ -88,7 +88,7 @@ class SyncNotificationWorkManagerRequestTest {
|
||||||
private fun createSyncNotificationWorkManagerRequest(
|
private fun createSyncNotificationWorkManagerRequest(
|
||||||
sessionId: SessionId,
|
sessionId: SessionId,
|
||||||
notificationEventRequests: List<NotificationEventRequest>,
|
notificationEventRequests: List<NotificationEventRequest>,
|
||||||
workerDataConverter: WorkerDataConverter = WorkerDataConverter(DefaultJsonProvider()),
|
workerDataConverter: SyncNotificationsWorkerDataConverter = SyncNotificationsWorkerDataConverter(DefaultJsonProvider()),
|
||||||
sdkVersion: Int = 33,
|
sdkVersion: Int = 33,
|
||||||
) = SyncNotificationWorkManagerRequest(
|
) = SyncNotificationWorkManagerRequest(
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
|
|
|
||||||
|
|
@ -57,10 +57,10 @@ class WorkerDataConverterTest {
|
||||||
providerInfo = "info$it",
|
providerInfo = "info$it",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val sut = WorkerDataConverter(DefaultJsonProvider())
|
val sut = SyncNotificationsWorkerDataConverter(DefaultJsonProvider())
|
||||||
val serialized = sut.serialize(data)
|
val serialized = sut.serialize(data)
|
||||||
assertThat(serialized.getOrNull()?.size).isGreaterThan(1)
|
assertThat(serialized.getOrNull()?.size).isGreaterThan(1)
|
||||||
assertThat(serialized.getOrNull()?.size).isEqualTo(100 / WorkerDataConverter.CHUNK_SIZE)
|
assertThat(serialized.getOrNull()?.size).isEqualTo(100 / SyncNotificationsWorkerDataConverter.CHUNK_SIZE)
|
||||||
// All the items are present
|
// All the items are present
|
||||||
val deserialized = serialized.getOrNull()?.flatMap { sut.deserialize(it)!! }
|
val deserialized = serialized.getOrNull()?.flatMap { sut.deserialize(it)!! }
|
||||||
assertThat(deserialized).containsExactlyElementsIn(data)
|
assertThat(deserialized).containsExactlyElementsIn(data)
|
||||||
|
|
@ -76,10 +76,10 @@ class WorkerDataConverterTest {
|
||||||
providerInfo = "info$it",
|
providerInfo = "info$it",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val sut = WorkerDataConverter(DefaultJsonProvider())
|
val sut = SyncNotificationsWorkerDataConverter(DefaultJsonProvider())
|
||||||
val serialized = sut.serialize(data)
|
val serialized = sut.serialize(data)
|
||||||
assertThat(serialized.getOrNull()?.size).isGreaterThan(1)
|
assertThat(serialized.getOrNull()?.size).isGreaterThan(1)
|
||||||
assertThat(serialized.getOrNull()?.size).isEqualTo(100 / WorkerDataConverter.CHUNK_SIZE + 1)
|
assertThat(serialized.getOrNull()?.size).isEqualTo(100 / SyncNotificationsWorkerDataConverter.CHUNK_SIZE + 1)
|
||||||
// All the items are present
|
// All the items are present
|
||||||
val deserialized = serialized.getOrNull()?.flatMap { sut.deserialize(it)!! }
|
val deserialized = serialized.getOrNull()?.flatMap { sut.deserialize(it)!! }
|
||||||
assertThat(deserialized).containsExactlyElementsIn(data)
|
assertThat(deserialized).containsExactlyElementsIn(data)
|
||||||
|
|
@ -112,7 +112,7 @@ class WorkerDataConverterTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val data = (data1 + data2 + data3).shuffled()
|
val data = (data1 + data2 + data3).shuffled()
|
||||||
val sut = WorkerDataConverter(DefaultJsonProvider())
|
val sut = SyncNotificationsWorkerDataConverter(DefaultJsonProvider())
|
||||||
val serialized = sut.serialize(data)
|
val serialized = sut.serialize(data)
|
||||||
assertThat(serialized.getOrNull()?.size).isEqualTo(2)
|
assertThat(serialized.getOrNull()?.size).isEqualTo(2)
|
||||||
// All the items are present
|
// All the items are present
|
||||||
|
|
@ -133,7 +133,7 @@ class WorkerDataConverterTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun testIdentity(data: List<NotificationEventRequest>) {
|
private fun testIdentity(data: List<NotificationEventRequest>) {
|
||||||
val sut = WorkerDataConverter(DefaultJsonProvider())
|
val sut = SyncNotificationsWorkerDataConverter(DefaultJsonProvider())
|
||||||
val serialized = sut.serialize(data).getOrThrow()
|
val serialized = sut.serialize(data).getOrThrow()
|
||||||
val result = sut.deserialize(serialized.first())
|
val result = sut.deserialize(serialized.first())
|
||||||
assertThat(result).isEqualTo(data)
|
assertThat(result).isEqualTo(data)
|
||||||
|
|
|
||||||
|
|
@ -12,16 +12,19 @@ import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
|
||||||
interface WorkManagerScheduler {
|
interface WorkManagerScheduler {
|
||||||
fun submit(workManagerRequest: WorkManagerRequest)
|
fun submit(workManagerRequest: WorkManagerRequest)
|
||||||
|
fun hasPendingWork(sessionId: SessionId, requestType: WorkManagerRequestType): Boolean
|
||||||
fun cancel(sessionId: SessionId)
|
fun cancel(sessionId: SessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun workManagerTag(sessionId: SessionId, requestType: WorkManagerRequestType): String {
|
fun workManagerTag(sessionId: SessionId, requestType: WorkManagerRequestType): String {
|
||||||
val prefix = when (requestType) {
|
val prefix = when (requestType) {
|
||||||
WorkManagerRequestType.NOTIFICATION_SYNC -> "notifications"
|
WorkManagerRequestType.NOTIFICATION_SYNC -> "notifications"
|
||||||
|
WorkManagerRequestType.DB_VACUUM -> "db_vacuum"
|
||||||
}
|
}
|
||||||
return "$prefix-$sessionId"
|
return "$prefix-$sessionId"
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class WorkManagerRequestType {
|
enum class WorkManagerRequestType {
|
||||||
NOTIFICATION_SYNC,
|
NOTIFICATION_SYNC,
|
||||||
|
DB_VACUUM,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ package io.element.android.libraries.workmanager.impl
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import dev.zacsweers.metro.AppScope
|
import dev.zacsweers.metro.AppScope
|
||||||
import dev.zacsweers.metro.ContributesBinding
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
|
import io.element.android.libraries.core.bool.orFalse
|
||||||
import dev.zacsweers.metro.SingleIn
|
import dev.zacsweers.metro.SingleIn
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.sessionstorage.api.observer.SessionListener
|
import io.element.android.libraries.sessionstorage.api.observer.SessionListener
|
||||||
|
|
@ -51,6 +52,10 @@ class DefaultWorkManagerScheduler(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun hasPendingWork(sessionId: SessionId, requestType: WorkManagerRequestType): Boolean {
|
||||||
|
return workManager.getWorkInfosByTag(workManagerTag(sessionId, requestType)).get()?.isNotEmpty().orFalse()
|
||||||
|
}
|
||||||
|
|
||||||
override fun cancel(sessionId: SessionId) {
|
override fun cancel(sessionId: SessionId) {
|
||||||
Timber.d("Cancelling work for sessionId: $sessionId")
|
Timber.d("Cancelling work for sessionId: $sessionId")
|
||||||
for (requestType in WorkManagerRequestType.entries) {
|
for (requestType in WorkManagerRequestType.entries) {
|
||||||
|
|
|
||||||
|
|
@ -10,17 +10,23 @@ package io.element.android.libraries.workmanager.test
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.workmanager.api.WorkManagerRequest
|
import io.element.android.libraries.workmanager.api.WorkManagerRequest
|
||||||
|
import io.element.android.libraries.workmanager.api.WorkManagerRequestType
|
||||||
import io.element.android.libraries.workmanager.api.WorkManagerScheduler
|
import io.element.android.libraries.workmanager.api.WorkManagerScheduler
|
||||||
import io.element.android.tests.testutils.lambda.lambdaError
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
|
||||||
class FakeWorkManagerScheduler(
|
class FakeWorkManagerScheduler(
|
||||||
private val submitLambda: (WorkManagerRequest) -> Unit = { lambdaError() },
|
private val submitLambda: (WorkManagerRequest) -> Unit = { lambdaError() },
|
||||||
|
private val hasPendingWorkLambda: (SessionId, WorkManagerRequestType) -> Boolean = { _, _ -> false },
|
||||||
private val cancelLambda: (SessionId) -> Unit = { lambdaError() },
|
private val cancelLambda: (SessionId) -> Unit = { lambdaError() },
|
||||||
) : WorkManagerScheduler {
|
) : WorkManagerScheduler {
|
||||||
override fun submit(workManagerRequest: WorkManagerRequest) {
|
override fun submit(workManagerRequest: WorkManagerRequest) {
|
||||||
submitLambda(workManagerRequest)
|
submitLambda(workManagerRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun hasPendingWork(sessionId: SessionId, requestType: WorkManagerRequestType): Boolean {
|
||||||
|
return hasPendingWorkLambda(sessionId, requestType)
|
||||||
|
}
|
||||||
|
|
||||||
override fun cancel(sessionId: SessionId) {
|
override fun cancel(sessionId: SessionId) {
|
||||||
cancelLambda(sessionId)
|
cancelLambda(sessionId)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue