Skip to content

Commit 17e3732

Browse files
committed
Don't hold db conns when creating a device on the compat login API
1 parent 345f6f2 commit 17e3732

File tree

3 files changed

+61
-39
lines changed

3 files changed

+61
-39
lines changed

crates/handlers/src/compat/login.rs

Lines changed: 53 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@ use mas_axum_utils::record_error;
1414
use mas_data_model::{CompatSession, CompatSsoLoginState, Device, SiteConfig, TokenType, User};
1515
use mas_matrix::HomeserverConnection;
1616
use mas_storage::{
17-
BoxClock, BoxRepository, BoxRng, Clock, RepositoryAccess,
17+
BoxClock, BoxRepository, BoxRepositoryFactory, BoxRng, Clock, RepositoryAccess,
1818
compat::{
1919
CompatAccessTokenRepository, CompatRefreshTokenRepository, CompatSessionRepository,
2020
CompatSsoLoginRepository,
2121
},
22+
queue::{QueueJobRepositoryExt as _, SyncDevicesJob},
2223
user::{UserPasswordRepository, UserRepository},
2324
};
2425
use opentelemetry::{Key, KeyValue, metrics::Counter};
@@ -268,7 +269,7 @@ pub(crate) async fn post(
268269
mut rng: BoxRng,
269270
clock: BoxClock,
270271
State(password_manager): State<PasswordManager>,
271-
mut repo: BoxRepository,
272+
State(repository_factory): State<BoxRepositoryFactory>,
272273
activity_tracker: BoundActivityTracker,
273274
State(homeserver): State<Arc<dyn HomeserverConnection>>,
274275
State(site_config): State<SiteConfig>,
@@ -279,6 +280,7 @@ pub(crate) async fn post(
279280
) -> Result<impl IntoResponse, RouteError> {
280281
let user_agent = user_agent.map(|ua| ua.as_str().to_owned());
281282
let login_type = input.credentials.login_type();
283+
let mut repo = repository_factory.create().await?;
282284
let (mut session, user) = match (password_manager.is_enabled(), input.credentials) {
283285
(
284286
true,
@@ -301,15 +303,17 @@ pub(crate) async fn post(
301303
}
302304
};
303305

306+
// Try getting the localpart out of the MXID
307+
let username = homeserver.localpart(&user).unwrap_or(&user);
308+
304309
user_password_login(
305310
&mut rng,
306311
&clock,
307312
&password_manager,
308313
&limiter,
309314
requester,
310315
&mut repo,
311-
&homeserver,
312-
user,
316+
username,
313317
password,
314318
input.device_id, // TODO check for validity
315319
input.initial_device_display_name,
@@ -322,7 +326,6 @@ pub(crate) async fn post(
322326
&mut rng,
323327
&clock,
324328
&mut repo,
325-
&homeserver,
326329
&token,
327330
input.device_id,
328331
input.initial_device_display_name,
@@ -374,6 +377,47 @@ pub(crate) async fn post(
374377
.record_compat_session(&clock, &session)
375378
.await;
376379

380+
// This session will have for sure the device on it, both methods create a
381+
// device
382+
let Some(device) = &session.device else {
383+
unreachable!()
384+
};
385+
386+
// Now we can create the device on the homeserver, without holding the
387+
// transaction
388+
if let Err(err) = homeserver
389+
.create_device(&user_id, device.as_str(), session.human_name.as_deref())
390+
.await
391+
{
392+
// Uh-oh. Something went wrong, let's end this session and schedule a device
393+
// sync
394+
let mut repo = repository_factory.create().await?;
395+
let session = repo.compat_session().finish(&clock, session).await?;
396+
397+
repo.queue_job()
398+
.schedule_job(
399+
&mut rng,
400+
&clock,
401+
SyncDevicesJob::new_for_id(session.user_id),
402+
)
403+
.await?;
404+
405+
repo.save().await?;
406+
407+
return Err(RouteError::ProvisionDeviceFailed(err));
408+
}
409+
410+
// We always trigger a device sync job, to clean up any races we ended up with
411+
let mut repo = repository_factory.create().await?;
412+
repo.queue_job()
413+
.schedule_job(
414+
&mut rng,
415+
&clock,
416+
SyncDevicesJob::new_for_id(session.user_id),
417+
)
418+
.await?;
419+
repo.save().await?;
420+
377421
LOGIN_COUNTER.add(
378422
1,
379423
&[
@@ -395,7 +439,6 @@ async fn token_login(
395439
rng: &mut (dyn RngCore + Send),
396440
clock: &dyn Clock,
397441
repo: &mut BoxRepository,
398-
homeserver: &dyn HomeserverConnection,
399442
token: &str,
400443
requested_device_id: Option<String>,
401444
initial_device_display_name: Option<String>,
@@ -461,30 +504,19 @@ async fn token_login(
461504
return Err(RouteError::InvalidLoginToken);
462505
}
463506

464-
// Lock the user sync to make sure we don't get into a race condition
465-
repo.user()
466-
.acquire_lock_for_sync(&browser_session.user)
467-
.await?;
468-
469507
let device = if let Some(requested_device_id) = requested_device_id {
470508
Device::from(requested_device_id)
471509
} else {
472510
Device::generate(rng)
473511
};
474-
let mxid = homeserver.mxid(&browser_session.user.username);
475-
homeserver
476-
.create_device(
477-
&mxid,
478-
device.as_str(),
479-
initial_device_display_name.as_deref(),
480-
)
481-
.await
482-
.map_err(RouteError::ProvisionDeviceFailed)?;
483512

484513
repo.app_session()
485514
.finish_sessions_to_replace_device(clock, &browser_session.user, &device)
486515
.await?;
487516

517+
// We first create the session in the database, commit the transaction, then
518+
// create it on the homeserver, scheduling a device sync job afterwards to
519+
// make sure we don't end up in an inconsistent state.
488520
let compat_session = repo
489521
.compat_session()
490522
.add(
@@ -512,15 +544,11 @@ async fn user_password_login(
512544
limiter: &Limiter,
513545
requester: RequesterFingerprint,
514546
repo: &mut BoxRepository,
515-
homeserver: &dyn HomeserverConnection,
516-
username: String,
547+
username: &str,
517548
password: String,
518549
requested_device_id: Option<String>,
519550
initial_device_display_name: Option<String>,
520551
) -> Result<(CompatSession, User), RouteError> {
521-
// Try getting the localpart out of the MXID
522-
let username = homeserver.localpart(&username).unwrap_or(&username);
523-
524552
// Find the user
525553
let user = repo
526554
.user()
@@ -566,25 +594,12 @@ async fn user_password_login(
566594
.await?;
567595
}
568596

569-
// Lock the user sync to make sure we don't get into a race condition
570-
repo.user().acquire_lock_for_sync(&user).await?;
571-
572-
let mxid = homeserver.mxid(&user.username);
573-
574597
// Now that the user credentials have been verified, start a new compat session
575598
let device = if let Some(requested_device_id) = requested_device_id {
576599
Device::from(requested_device_id)
577600
} else {
578601
Device::generate(&mut rng)
579602
};
580-
homeserver
581-
.create_device(
582-
&mxid,
583-
device.as_str(),
584-
initial_device_display_name.as_deref(),
585-
)
586-
.await
587-
.map_err(RouteError::ProvisionDeviceFailed)?;
588603

589604
repo.app_session()
590605
.finish_sessions_to_replace_device(clock, &user, &device)

crates/handlers/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ use mas_keystore::{Encrypter, Keystore};
4242
use mas_matrix::HomeserverConnection;
4343
use mas_policy::Policy;
4444
use mas_router::{Route, UrlBuilder};
45-
use mas_storage::{BoxClock, BoxRepository, BoxRng};
45+
use mas_storage::{BoxClock, BoxRepository, BoxRepositoryFactory, BoxRng};
4646
use mas_templates::{ErrorContext, NotFoundContext, TemplateContext, Templates};
4747
use opentelemetry::metrics::Meter;
4848
use sqlx::PgPool;
@@ -265,6 +265,7 @@ where
265265
Arc<dyn HomeserverConnection>: FromRef<S>,
266266
PasswordManager: FromRef<S>,
267267
Limiter: FromRef<S>,
268+
BoxRepositoryFactory: FromRef<S>,
268269
BoundActivityTracker: FromRequestParts<S>,
269270
RequesterFingerprint: FromRequestParts<S>,
270271
BoxRepository: FromRequestParts<S>,

crates/handlers/src/test_utils.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,12 @@ impl FromRef<TestState> for PgPool {
453453
}
454454
}
455455

456+
impl FromRef<TestState> for BoxRepositoryFactory {
457+
fn from_ref(input: &TestState) -> Self {
458+
input.repository_factory.clone().boxed()
459+
}
460+
}
461+
456462
impl FromRef<TestState> for graphql::Schema {
457463
fn from_ref(input: &TestState) -> Self {
458464
input.graphql_schema.clone()

0 commit comments

Comments
 (0)