-
Notifications
You must be signed in to change notification settings - Fork 626
Iterate on client creation #7591
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
bmarty
wants to merge
6
commits into
develop
Choose a base branch
from
bma/iterateOnClientCreation
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
5ce531f
Ensure we create a fresh Client when the sessionId is know after auth…
bmarty 1f18a18
Iterate on hooks implementation
bmarty 579ec9b
Close the client used to log in before creating the client of the ses…
bmarty 59e72fc
Stop deleting the session data of a logged in account on the next login
bmarty a817b6f
Rename methods and update documentation.
bmarty 3cde152
Fix quality and tests.
bmarty File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Submodule enterprise
updated
from bb07b4 to ebe8b6
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
25 changes: 25 additions & 0 deletions
25
...ain/kotlin/io/element/android/features/enterprise/test/FakeClientBuilderEnterpriseHook.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| /* | ||
| * Copyright (c) 2026 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.features.enterprise.test | ||
|
|
||
| import io.element.android.features.enterprise.api.ClientBuilderEnterpriseHook | ||
| import io.element.android.libraries.matrix.api.MatrixClientBuilder | ||
| import io.element.android.libraries.matrix.api.core.SessionId | ||
|
|
||
| class FakeClientBuilderEnterpriseHook( | ||
| private val beforeClientCreationResult: (MatrixClientBuilder) -> MatrixClientBuilder = { it }, | ||
| private val beforeClientCreationWithSessionResult: (MatrixClientBuilder, SessionId) -> MatrixClientBuilder = { clientBuilder, _ -> clientBuilder }, | ||
| ) : ClientBuilderEnterpriseHook { | ||
| override suspend fun beforeClientCreation(clientBuilder: MatrixClientBuilder): MatrixClientBuilder { | ||
| return beforeClientCreationResult(clientBuilder) | ||
| } | ||
|
|
||
| override suspend fun beforeClientCreationWithSession(clientBuilder: MatrixClientBuilder, sessionId: SessionId): MatrixClientBuilder { | ||
| return beforeClientCreationWithSessionResult(clientBuilder, sessionId) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -44,6 +44,7 @@ import io.element.android.libraries.matrix.impl.keys.SecretGenerator | |
| import io.element.android.libraries.matrix.impl.mapper.toSessionData | ||
| import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory | ||
| import io.element.android.libraries.sessionstorage.api.LoginType | ||
| import io.element.android.libraries.sessionstorage.api.SessionData | ||
| import io.element.android.libraries.sessionstorage.api.SessionStore | ||
| import kotlinx.coroutines.CancellationException | ||
| import kotlinx.coroutines.flow.first | ||
|
|
@@ -101,17 +102,7 @@ class RustMatrixAuthenticationService( | |
| runCatchingExceptions { | ||
| val sessionData = sessionStore.getSession(sessionId.value) | ||
| if (sessionData != null) { | ||
| if (sessionData.isTokenValid) { | ||
| // Use the sessionData.passphrase, which can be null for a previously created session | ||
| if (sessionData.passphrase == null) { | ||
| Timber.w("Restoring a session without a passphrase") | ||
| } else { | ||
| Timber.w("Restoring a session with a passphrase") | ||
| } | ||
| rustMatrixClientFactory.create(sessionData) | ||
| } else { | ||
| throw SessionRestorationException.InvalidToken() | ||
| } | ||
| restoreSession(sessionData) | ||
| } else { | ||
| throw SessionRestorationException.MissingSession(sessionId) | ||
| } | ||
|
|
@@ -120,6 +111,19 @@ class RustMatrixAuthenticationService( | |
| } | ||
| } | ||
|
|
||
| private suspend fun restoreSession(sessionData: SessionData): MatrixClient { | ||
| if (!sessionData.isTokenValid) { | ||
| throw SessionRestorationException.InvalidToken() | ||
| } | ||
| // Use the sessionData.passphrase, which can be null for a previously created session | ||
| if (sessionData.passphrase == null) { | ||
| Timber.w("Restoring a session without a passphrase") | ||
| } else { | ||
| Timber.w("Restoring a session with a passphrase") | ||
| } | ||
| return rustMatrixClientFactory.create(sessionData) | ||
| } | ||
|
|
||
| private fun getDatabaseKey(): ClientSecret { | ||
| Timber.d("New sessions will be encrypted with a raw key") | ||
| return secretGenerator.generateKey() | ||
|
|
@@ -137,7 +141,7 @@ class RustMatrixAuthenticationService( | |
|
|
||
| client.homeserverLoginDetails().map() | ||
| }.onFailure { | ||
| clear(destroyClient = true) | ||
| clear() | ||
| }.mapFailure { failure -> | ||
| Timber.e(failure, "Failed to set homeserver to $homeserver") | ||
| failure.mapAuthenticationException() | ||
|
|
@@ -165,18 +169,7 @@ class RustMatrixAuthenticationService( | |
| passphrase = pendingKey.formattedAsString(), | ||
| sessionPaths = currentSessionPaths, | ||
| ) | ||
| val matrixClient = rustMatrixClientFactory.create(client, sessionData, isMessageSearchAvailable()) | ||
|
|
||
| // Apply enterprise hooks to the newly created client as soon as possible | ||
| clientEnterpriseHook(matrixClient) | ||
|
|
||
| newMatrixClientObservers.forEach { it.invoke(matrixClient) } | ||
| sessionStore.addSession(sessionData) | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Worth noting that |
||
|
|
||
| // Clean up the strong reference held here since it's no longer necessary | ||
| clear(destroyClient = false) | ||
|
|
||
| SessionId(sessionData.userId) | ||
| finalizeClientCreation(sessionData) | ||
| }.mapFailure { failure -> | ||
| Timber.e(failure, "Failed to login") | ||
| failure.mapAuthenticationException() | ||
|
|
@@ -305,20 +298,7 @@ class RustMatrixAuthenticationService( | |
| passphrase = pendingKey.formattedAsString(), | ||
| sessionPaths = currentSessionPaths, | ||
| ) | ||
| val matrixClient = rustMatrixClientFactory.create(client, sessionData, isMessageSearchAvailable()) | ||
|
|
||
| // Apply enterprise hooks to the newly created client as soon as possible | ||
| clientEnterpriseHook(matrixClient) | ||
|
|
||
| matrixClient.waitForKnownVerificationState() | ||
|
|
||
| newMatrixClientObservers.forEach { it.invoke(matrixClient) } | ||
| sessionStore.addSession(sessionData) | ||
|
|
||
| // Clean up the strong reference held here since it's no longer necessary | ||
| clear(destroyClient = false) | ||
|
|
||
| SessionId(sessionData.userId) | ||
| finalizeClientCreation(sessionData) | ||
| }.mapFailure { failure -> | ||
| Timber.e(failure, "Failed to login with OAuth") | ||
| failure.mapAuthenticationException() | ||
|
|
@@ -357,6 +337,7 @@ class RustMatrixAuthenticationService( | |
| sessionPaths = emptySessionPaths, | ||
| qrCodeData = sdkQrCodeLoginData, | ||
| ) | ||
| currentClient = client | ||
| client.newLoginWithQrCodeHandler( | ||
| oauthConfiguration = oAuthConfiguration, | ||
| ).use { | ||
|
|
@@ -374,32 +355,44 @@ class RustMatrixAuthenticationService( | |
| passphrase = pendingKey.formattedAsString(), | ||
| sessionPaths = emptySessionPaths, | ||
| ) | ||
| val matrixClient = rustMatrixClientFactory.create(client, sessionData, isMessageSearchAvailable()) | ||
|
|
||
| // Apply enterprise hooks to the newly created client as soon as possible | ||
| clientEnterpriseHook(matrixClient) | ||
|
|
||
| newMatrixClientObservers.forEach { it.invoke(matrixClient) } | ||
| sessionStore.addSession(sessionData) | ||
|
|
||
| // Clean up the strong reference held here since it's no longer necessary | ||
| clear(destroyClient = false) | ||
|
|
||
| SessionId(sessionData.userId) | ||
| finalizeClientCreation(sessionData) | ||
| }.mapFailure { | ||
| when (it) { | ||
| is QrCodeDecodeException -> QrErrorMapper.map(it) | ||
| is HumanQrLoginException -> QrErrorMapper.map(it) | ||
| else -> it | ||
| } | ||
| }.onFailure { throwable -> | ||
| // A QR code login always builds its own client, so it can be disposed of on failure. | ||
| clear() | ||
| if (throwable is CancellationException) { | ||
| throw throwable | ||
| } | ||
| Timber.e(throwable, "Failed to login with QR code") | ||
| } | ||
| } | ||
|
|
||
| private suspend fun finalizeClientCreation(sessionData: SessionData): SessionId { | ||
| // Close the client which was used to perform the login before creating the final client. | ||
| // Both use the same session paths, so their SQLite stores must never be opened at the same time. | ||
| clear() | ||
|
|
||
| val matrixClient = restoreSession(sessionData) | ||
| // Apply enterprise hooks to the newly created client as soon as possible | ||
| clientEnterpriseHook(matrixClient) | ||
|
|
||
| matrixClient.waitForKnownVerificationState() | ||
|
|
||
| newMatrixClientObservers.forEach { it.invoke(matrixClient) } | ||
| sessionStore.addSession(sessionData) | ||
|
|
||
| // The session paths now hold the data of the account which has just been logged in, so forget | ||
| // them: they must not be deleted by the rotateSessionPath() of the next login attempt. | ||
| sessionPaths = null | ||
|
|
||
| return SessionId(sessionData.userId) | ||
| } | ||
|
|
||
| private suspend fun makeClient( | ||
| sessionPaths: SessionPaths, | ||
| config: suspend ClientBuilder.() -> ClientBuilder, | ||
|
|
@@ -449,10 +442,8 @@ class RustMatrixAuthenticationService( | |
| .build() | ||
| } | ||
|
|
||
| private fun clear(destroyClient: Boolean) { | ||
| if (destroyClient) { | ||
| currentClient?.close() | ||
| } | ||
| private fun clear() { | ||
| currentClient?.close() | ||
| currentClient = null | ||
| } | ||
|
|
||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note by doing this we're not reusing an already valid client and are forcing the creation of a different client, which can mean ~1s for opening the SDK DBs again I think. It's not that important since it happens on the login flow, but that's why we had 2 methods and tried to avoid destroying the current client in the log in flows.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. So maybe we could have a second hook to update the client once it's logged in? Not sure if this will work for @richvdh .
To let this PR focused on this change, I'll extract the first commit to its own PR.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have measured 580ms spent on the new method
finalizeClientCreation, but only 80 ms for the methodrestoreSessionin it, so the impact is actually very small (?)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In theory we could work around it, but for my usecase it would be much better to run the hook before the client is created. I don't need to know the userid though, so provided the hook actually gets called whenever a client is created, that would be fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do not understand, how the hook using
MatrixClientcan be called beforeMatrixClientis created? Are you talking aboutClientEnterpriseHookor something else?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm talking about
ClientBuilderEnterpriseHook. I need a way to call methods on the underlying rust-sideClientBuildereach time we build a Client.I could probably work around it by adding methods to the rust-side
MatrixClientand calling them after the client is created, but I'd much prefer to avoid that.