Add a cache for Element .well-known file.

This commit is contained in:
Benoit Marty 2026-04-29 22:05:14 +02:00
parent 29e0a08dd9
commit 8512279d96
3 changed files with 173 additions and 11 deletions

View file

@ -33,9 +33,13 @@ dependencies {
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.network)
implementation(projects.libraries.cachestore.api)
implementation(projects.services.toolbox.api)
testCommonDependencies(libs)
testImplementation(libs.coroutines.core)
testImplementation(projects.libraries.cachestore.test)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.wellknown.test)
testImplementation(projects.services.toolbox.test)
}

View file

@ -10,29 +10,70 @@ package io.element.android.libraries.wellknown.impl
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.androidutils.json.JsonProvider
import io.element.android.libraries.cachestore.api.CacheData
import io.element.android.libraries.cachestore.api.CacheStore
import io.element.android.libraries.core.extensions.mapCatchingExceptions
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.exception.ClientException
import io.element.android.libraries.wellknown.api.ElementWellKnown
import io.element.android.libraries.wellknown.api.SessionWellknownRetriever
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
@ContributesBinding(SessionScope::class)
class DefaultSessionWellknownRetriever(
private val matrixClient: MatrixClient,
private val json: JsonProvider,
private val cacheStore: CacheStore,
private val systemClock: SystemClock,
@SessionCoroutineScope
private val sessionCoroutineScope: CoroutineScope,
) : SessionWellknownRetriever {
private val domain by lazy { matrixClient.userIdServerName() }
override suspend fun getElementWellKnown(): WellknownRetrieverResult<ElementWellKnown> {
val url = "https://$domain/.well-known/element/element.json"
val cacheData = cacheStore.getData(url)
if (cacheData != null) {
Timber.d("Element .well-known data retrieved from cache for $domain")
// If the cache is outdated, trigger a refresh in background but still return the cached value
if (systemClock.epochMillis() > cacheData.updatedAt + CACHE_VALIDITY_MILLIS) {
sessionCoroutineScope.launch {
fetchElementWellKnown(url)
}
}
try {
val parsed = json().decodeFromString<InternalElementWellKnown>(cacheData.value).map()
return WellknownRetrieverResult.Success(parsed)
} catch (e: Exception) {
Timber.e(e, "Failed to parse cached Element .well-known data for $domain, deleting cache")
cacheStore.deleteData(url)
}
}
return fetchElementWellKnown(url)
}
private suspend fun fetchElementWellKnown(url: String): WellknownRetrieverResult<ElementWellKnown> {
return matrixClient
.getUrl(url)
.mapCatchingExceptions {
val data = String(it)
json().decodeFromString<InternalElementWellKnown>(data).map()
val parsed = json().decodeFromString<InternalElementWellKnown>(data).map()
// Also store in cache, if valid
cacheStore.storeData(
key = url,
data = CacheData(
value = data,
updatedAt = systemClock.epochMillis(),
)
)
parsed
}
.toWellknownRetrieverResult()
}
@ -51,4 +92,9 @@ class DefaultSessionWellknownRetriever(
}
}
)
companion object {
// 1 day
private const val CACHE_VALIDITY_MILLIS = 1 * 24 * 60 * 60 * 1000L
}
}

View file

@ -6,16 +6,30 @@
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalCoroutinesApi::class)
package io.element.android.libraries.wellknown.impl
import com.google.common.truth.Truth.assertThat
import io.element.android.features.wellknown.test.anElementWellKnown
import io.element.android.libraries.androidutils.json.DefaultJsonProvider
import io.element.android.libraries.androidutils.json.JsonProvider
import io.element.android.libraries.cachestore.api.CacheData
import io.element.android.libraries.cachestore.api.CacheStore
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.sessionstorage.test.InMemoryCacheStore
import io.element.android.libraries.wellknown.api.ElementWellKnown
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
import io.element.android.services.toolbox.api.systemclock.SystemClock
import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -49,14 +63,7 @@ class DefaultSessionWellknownRetrieverTest {
val sut = createDefaultSessionWellknownRetriever(
getUrlLambda = {
Result.success(
"""{
"registration_helper_url": "a_registration_url",
"enforce_element_pro": true,
"rageshake_url": "a_rageshake_url",
"brand_color": "#FF0000",
"notification_sound": "a_notification_sound.flac",
"idp_app_scheme": "an_app_scheme"
}""".trimIndent().toByteArray()
WELLKNOWN_CONTENT.toByteArray()
)
}
)
@ -127,13 +134,118 @@ class DefaultSessionWellknownRetrieverTest {
assertThat(sut.getElementWellKnown()).isInstanceOf(WellknownRetrieverResult.Error::class.java)
}
private fun createDefaultSessionWellknownRetriever(
@Test
fun `get element wellknown hitting cache`() = runTest {
val sut = createDefaultSessionWellknownRetriever(
getUrlLambda = { lambdaError() },
cacheStore = InMemoryCacheStore(
initialData = mapOf(
WELLKNOWN_URL to CacheData(
value = WELLKNOWN_CONTENT,
updatedAt = A_FAKE_TIMESTAMP,
)
)
)
)
assertThat(sut.getElementWellKnown()).isEqualTo(
WellknownRetrieverResult.Success(
ElementWellKnown(
registrationHelperUrl = "a_registration_url",
enforceElementPro = true,
rageshakeUrl = "a_rageshake_url",
brandColor = "#FF0000",
notificationSound = "a_notification_sound.flac",
identityProviderAppScheme = "an_app_scheme",
)
)
)
}
@Test
fun `get element wellknown hitting cache containing invalid json`() = runTest {
val cacheStore = InMemoryCacheStore(
initialData = mapOf(
WELLKNOWN_URL to CacheData(
value = WELLKNOWN_CONTENT,
updatedAt = A_FAKE_TIMESTAMP,
)
)
)
val sut = createDefaultSessionWellknownRetriever(
getUrlLambda = {
Result.success("{}".toByteArray())
},
cacheStore = cacheStore,
jsonProvider = JsonProvider { throw Exception("Failed to parse JSON") }
)
assertThat(sut.getElementWellKnown()).isInstanceOf(WellknownRetrieverResult.Error::class.java)
// Ensure that the cache is deleted after the failure to parse it
assertThat(cacheStore.dataMap).isEmpty()
}
@Test
fun `get element wellknown hitting outdated cache`() = runTest {
val sut = createDefaultSessionWellknownRetriever(
getUrlLambda = {
Result.success("{}".toByteArray())
},
cacheStore = InMemoryCacheStore(
initialData = mapOf(
WELLKNOWN_URL to CacheData(
value = WELLKNOWN_CONTENT,
updatedAt = 0L,
)
),
),
// 3 days later, so the cache is outdated
systemClock = FakeSystemClock(3 * 24 * 60 * 60 * 1000L)
)
assertThat(sut.getElementWellKnown()).isEqualTo(
WellknownRetrieverResult.Success(
ElementWellKnown(
registrationHelperUrl = "a_registration_url",
enforceElementPro = true,
rageshakeUrl = "a_rageshake_url",
brandColor = "#FF0000",
notificationSound = "a_notification_sound.flac",
identityProviderAppScheme = "an_app_scheme",
)
)
)
// Next call returns the updated value
runCurrent()
assertThat(sut.getElementWellKnown()).isEqualTo(
WellknownRetrieverResult.Success(
anElementWellKnown()
)
)
}
private fun TestScope.createDefaultSessionWellknownRetriever(
getUrlLambda: (String) -> Result<ByteArray>,
jsonProvider: JsonProvider = DefaultJsonProvider(),
cacheStore: CacheStore = InMemoryCacheStore(),
systemClock: SystemClock = FakeSystemClock(),
) = DefaultSessionWellknownRetriever(
matrixClient = FakeMatrixClient(
userIdServerNameLambda = { "user.domain.org" },
getUrlLambda = getUrlLambda,
),
json = DefaultJsonProvider(),
json = jsonProvider,
cacheStore = cacheStore,
systemClock = systemClock,
sessionCoroutineScope = backgroundScope,
)
companion object {
private const val WELLKNOWN_URL = "https://user.domain.org/.well-known/element/element.json"
private const val WELLKNOWN_CONTENT = """{
"registration_helper_url": "a_registration_url",
"enforce_element_pro": true,
"rageshake_url": "a_rageshake_url",
"brand_color": "#FF0000",
"notification_sound": "a_notification_sound.flac",
"idp_app_scheme": "an_app_scheme"
}"""
}
}