1use std::time::Instant;
15
16use chrono::{DateTime, Utc};
17use compact_str::CompactString;
18use futures_util::{SinkExt, StreamExt as _, TryFutureExt, TryStreamExt as _};
19use mas_data_model::{Clock, UlidExt as _};
20use rand::{RngCore, SeedableRng};
21use thiserror::Error;
22use thiserror_ext::ContextInto;
23use tokio_util::sync::PollSender;
24use tracing::{Instrument as _, Level, info};
25use ulid::Ulid;
26use uuid::{NonNilUuid, Uuid};
27
28use crate::{
29 HashMap, ProgressCounter, RandomState, SynapseReader,
30 mas_writer::{
31 self, MasNewCompatAccessToken, MasNewCompatRefreshToken, MasNewCompatSession,
32 MasNewEmailThreepid, MasNewUnsupportedThreepid, MasNewUpstreamOauthLink, MasNewUser,
33 MasNewUserPassword, MasWriteBuffer, MasWriter,
34 },
35 progress::{EntityType, Progress},
36 synapse_reader::{
37 self, ExtractLocalpartError, FullUserId, SynapseAccessToken, SynapseDevice,
38 SynapseExternalId, SynapseRefreshableTokenPair, SynapseThreepid, SynapseUser,
39 },
40};
41
42#[derive(Debug, Error, ContextInto)]
43pub enum Error {
44 #[error("error when reading synapse DB ({context}): {source}")]
45 Synapse {
46 source: synapse_reader::Error,
47 context: String,
48 },
49 #[error("error when writing to MAS DB ({context}): {source}")]
50 Mas {
51 source: mas_writer::Error,
52 context: String,
53 },
54 #[error("failed to extract localpart of {user:?}: {source}")]
55 ExtractLocalpart {
56 source: ExtractLocalpartError,
57 user: FullUserId,
58 },
59 #[error("channel closed")]
60 ChannelClosed,
61
62 #[error("task failed ({context}): {source}")]
63 Join {
64 source: tokio::task::JoinError,
65 context: String,
66 },
67
68 #[error("user {user} was not found for migration but a row in {table} was found for them")]
69 MissingUserFromDependentTable { table: String, user: FullUserId },
70 #[error(
71 "missing a mapping for the auth provider with ID {synapse_id:?} (used by {user} and maybe other users)"
72 )]
73 MissingAuthProviderMapping {
74 synapse_id: String,
77 user: FullUserId,
79 },
80}
81
82bitflags::bitflags! {
83 #[derive(Debug, Clone, Copy)]
84 struct UserFlags: u8 {
85 const IS_SYNAPSE_ADMIN = 0b0000_0001;
86 const IS_DEACTIVATED = 0b0000_0010;
87 const IS_GUEST = 0b0000_0100;
88 const IS_APPSERVICE = 0b0000_1000;
89 }
90}
91
92impl UserFlags {
93 const fn is_deactivated(self) -> bool {
94 self.contains(UserFlags::IS_DEACTIVATED)
95 }
96
97 const fn is_guest(self) -> bool {
98 self.contains(UserFlags::IS_GUEST)
99 }
100
101 const fn is_synapse_admin(self) -> bool {
102 self.contains(UserFlags::IS_SYNAPSE_ADMIN)
103 }
104
105 const fn is_appservice(self) -> bool {
106 self.contains(UserFlags::IS_APPSERVICE)
107 }
108}
109
110#[derive(Debug, Clone, Copy)]
111struct UserInfo {
112 mas_user_id: Option<NonNilUuid>,
113 flags: UserFlags,
114}
115
116struct MigrationState {
117 server_name: String,
119
120 users: HashMap<CompactString, UserInfo>,
122
123 devices_to_compat_sessions: HashMap<(NonNilUuid, CompactString), Uuid>,
125
126 provider_id_mapping: std::collections::HashMap<String, Uuid>,
129}
130
131#[expect(clippy::implicit_hasher)]
144#[expect(clippy::too_many_arguments)]
145pub async fn migrate(
146 mut synapse: SynapseReader<'_>,
147 mas: MasWriter,
148 server_name: String,
149 clock: &dyn Clock,
150 rng: &mut impl RngCore,
151 provider_id_mapping: std::collections::HashMap<String, Uuid>,
152 progress: &Progress,
153 ignore_missing_auth_providers: bool,
154) -> Result<(), Error> {
155 let counts = synapse.count_rows().await.into_synapse("counting users")?;
156
157 let state = MigrationState {
158 server_name,
159 users: HashMap::with_capacity_and_hasher(counts.users * 9 / 8, RandomState::default()),
162 devices_to_compat_sessions: HashMap::with_capacity_and_hasher(
163 counts.devices * 9 / 8,
164 RandomState::default(),
165 ),
166 provider_id_mapping,
167 };
168
169 let progress_counter = progress.migrating_data(EntityType::Users, counts.users);
170 let (mas, state) = migrate_users(&mut synapse, mas, state, rng, progress_counter).await?;
171
172 let progress_counter = progress.migrating_data(EntityType::ThreePids, counts.threepids);
173 let (mas, state) = migrate_threepids(&mut synapse, mas, rng, state, progress_counter).await?;
174
175 let progress_counter = progress.migrating_data(EntityType::ExternalIds, counts.external_ids);
176 let (mas, state) = migrate_external_ids(
177 &mut synapse,
178 mas,
179 rng,
180 state,
181 progress_counter,
182 ignore_missing_auth_providers,
183 )
184 .await?;
185
186 let progress_counter = progress.migrating_data(
187 EntityType::NonRefreshableAccessTokens,
188 counts.access_tokens - counts.refresh_tokens,
189 );
190 let (mas, state) =
191 migrate_unrefreshable_access_tokens(&mut synapse, mas, clock, rng, state, progress_counter)
192 .await?;
193
194 let progress_counter =
195 progress.migrating_data(EntityType::RefreshableTokens, counts.refresh_tokens);
196 let (mas, state) =
197 migrate_refreshable_token_pairs(&mut synapse, mas, clock, rng, state, progress_counter)
198 .await?;
199
200 let progress_counter = progress.migrating_data(EntityType::Devices, counts.devices);
201 let (mas, _state) = migrate_devices(&mut synapse, mas, rng, state, progress_counter).await?;
202
203 synapse
204 .finish()
205 .await
206 .into_synapse("failed to close Synapse reader")?;
207
208 mas.finish(progress)
209 .await
210 .into_mas("failed to finalise MAS database")?;
211
212 Ok(())
213}
214
215#[tracing::instrument(skip_all, level = Level::INFO)]
216async fn migrate_users(
217 synapse: &mut SynapseReader<'_>,
218 mut mas: MasWriter,
219 mut state: MigrationState,
220 rng: &mut impl RngCore,
221 progress_counter: ProgressCounter,
222) -> Result<(MasWriter, MigrationState), Error> {
223 let start = Instant::now();
224 let progress_counter_ = progress_counter.clone();
225
226 let (tx, mut rx) = tokio::sync::mpsc::channel::<SynapseUser>(100 * 1024);
227
228 let mut rng = rand_chacha::ChaChaRng::from_rng(rng).expect("failed to seed rng");
231 let task = tokio::spawn(
232 async move {
233 let mut user_buffer = MasWriteBuffer::new(&mas);
234 let mut password_buffer = MasWriteBuffer::new(&mas);
235
236 while let Some(user) = rx.recv().await {
237 if user.appservice_id.is_some()
240 && user
241 .name
242 .0
243 .strip_suffix(&format!(":{}", state.server_name))
244 .is_some_and(|localpart| localpart.contains(':'))
245 {
246 tracing::warn!("AS user {} has invalid localpart, ignoring!", user.name.0);
247 continue;
248 }
249
250 let (mas_user, mas_password_opt) =
251 transform_user(&user, &state.server_name, &mut rng)?;
252
253 let mut flags = UserFlags::empty();
254 if bool::from(user.admin) {
255 flags |= UserFlags::IS_SYNAPSE_ADMIN;
256 }
257 if bool::from(user.deactivated) {
258 flags |= UserFlags::IS_DEACTIVATED;
259 }
260 if bool::from(user.is_guest) {
261 flags |= UserFlags::IS_GUEST;
262 }
263 if user.appservice_id.is_some() {
264 flags |= UserFlags::IS_APPSERVICE;
265
266 progress_counter.increment_skipped();
267
268 state.users.insert(
271 CompactString::new(&mas_user.username),
272 UserInfo {
273 mas_user_id: None,
274 flags,
275 },
276 );
277 continue;
278 }
279
280 state.users.insert(
281 CompactString::new(&mas_user.username),
282 UserInfo {
283 mas_user_id: Some(mas_user.user_id),
284 flags,
285 },
286 );
287
288 user_buffer
289 .write(&mut mas, mas_user)
290 .await
291 .into_mas("writing user")?;
292
293 if let Some(mas_password) = mas_password_opt {
294 password_buffer
295 .write(&mut mas, mas_password)
296 .await
297 .into_mas("writing password")?;
298 }
299
300 progress_counter.increment_migrated();
301 }
302
303 user_buffer
304 .finish(&mut mas)
305 .await
306 .into_mas("writing users")?;
307 password_buffer
308 .finish(&mut mas)
309 .await
310 .into_mas("writing passwords")?;
311
312 Ok((mas, state))
313 }
314 .instrument(tracing::info_span!("ingest_task")),
315 );
316
317 let res = synapse
320 .read_users()
321 .map_err(|e| e.into_synapse("reading users"))
322 .forward(PollSender::new(tx).sink_map_err(|_| Error::ChannelClosed))
323 .inspect_err(|e| tracing::error!(error = e as &dyn std::error::Error))
324 .await;
325
326 let (mas, state) = task.await.into_join("user write task")??;
327
328 res?;
329
330 info!(
331 "{} users migrated ({} skipped) in {:.1}s",
332 progress_counter_.migrated(),
333 progress_counter_.skipped(),
334 Instant::now().duration_since(start).as_secs_f64()
335 );
336
337 Ok((mas, state))
338}
339
340#[tracing::instrument(skip_all, level = Level::INFO)]
341async fn migrate_threepids(
342 synapse: &mut SynapseReader<'_>,
343 mut mas: MasWriter,
344 rng: &mut impl RngCore,
345 state: MigrationState,
346 progress_counter: ProgressCounter,
347) -> Result<(MasWriter, MigrationState), Error> {
348 let start = Instant::now();
349 let progress_counter_ = progress_counter.clone();
350
351 let (tx, mut rx) = tokio::sync::mpsc::channel::<SynapseThreepid>(100 * 1024);
352
353 let mut rng = rand_chacha::ChaChaRng::from_rng(rng).expect("failed to seed rng");
356 let task = tokio::spawn(
357 async move {
358 let mut email_buffer = MasWriteBuffer::new(&mas);
359 let mut unsupported_buffer = MasWriteBuffer::new(&mas);
360
361 while let Some(threepid) = rx.recv().await {
362 let SynapseThreepid {
363 user_id: synapse_user_id,
364 medium,
365 address,
366 added_at,
367 } = threepid;
368 let created_at: DateTime<Utc> = added_at.into();
369
370 let username = synapse_user_id
371 .extract_localpart(&state.server_name)
372 .into_extract_localpart(synapse_user_id.clone())?
373 .to_owned();
374 let Some(user_infos) = state.users.get(username.as_str()).copied() else {
375 return Err(Error::MissingUserFromDependentTable {
376 table: "user_threepids".to_owned(),
377 user: synapse_user_id,
378 });
379 };
380
381 let Some(mas_user_id) = user_infos.mas_user_id else {
382 progress_counter.increment_skipped();
383 continue;
384 };
385
386 if medium == "email" {
387 email_buffer
388 .write(
389 &mut mas,
390 MasNewEmailThreepid {
391 user_id: mas_user_id,
392 user_email_id: Uuid::from(Ulid::from_datetime_with_rng(
393 created_at, &mut rng,
394 )),
395 email: address,
396 created_at,
397 },
398 )
399 .await
400 .into_mas("writing email")?;
401 } else {
402 unsupported_buffer
403 .write(
404 &mut mas,
405 MasNewUnsupportedThreepid {
406 user_id: mas_user_id,
407 medium,
408 address,
409 created_at,
410 },
411 )
412 .await
413 .into_mas("writing unsupported threepid")?;
414 }
415
416 progress_counter.increment_migrated();
417 }
418
419 email_buffer
420 .finish(&mut mas)
421 .await
422 .into_mas("writing email threepids")?;
423 unsupported_buffer
424 .finish(&mut mas)
425 .await
426 .into_mas("writing unsupported threepids")?;
427
428 Ok((mas, state))
429 }
430 .instrument(tracing::info_span!("ingest_task")),
431 );
432
433 let res = synapse
436 .read_threepids()
437 .map_err(|e| e.into_synapse("reading threepids"))
438 .forward(PollSender::new(tx).sink_map_err(|_| Error::ChannelClosed))
439 .inspect_err(|e| tracing::error!(error = e as &dyn std::error::Error))
440 .await;
441
442 let (mas, state) = task.await.into_join("threepid write task")??;
443
444 res?;
445
446 info!(
447 "{} third-party IDs migrated ({} skipped) in {:.1}s",
448 progress_counter_.migrated(),
449 progress_counter_.skipped(),
450 Instant::now().duration_since(start).as_secs_f64()
451 );
452
453 Ok((mas, state))
454}
455
456#[tracing::instrument(skip_all, level = Level::INFO)]
457async fn migrate_external_ids(
458 synapse: &mut SynapseReader<'_>,
459 mut mas: MasWriter,
460 rng: &mut impl RngCore,
461 state: MigrationState,
462 progress_counter: ProgressCounter,
463 ignore_missing_auth_providers: bool,
464) -> Result<(MasWriter, MigrationState), Error> {
465 let start = Instant::now();
466 let progress_counter_ = progress_counter.clone();
467
468 let (tx, mut rx) = tokio::sync::mpsc::channel::<SynapseExternalId>(100 * 1024);
469
470 let mut rng = rand_chacha::ChaChaRng::from_rng(rng).expect("failed to seed rng");
473 let task = tokio::spawn(
474 async move {
475 let mut write_buffer = MasWriteBuffer::new(&mas);
476
477 while let Some(extid) = rx.recv().await {
478 let SynapseExternalId {
479 user_id: synapse_user_id,
480 auth_provider,
481 external_id: subject,
482 } = extid;
483 let username = synapse_user_id
484 .extract_localpart(&state.server_name)
485 .into_extract_localpart(synapse_user_id.clone())?
486 .to_owned();
487 let Some(user_infos) = state.users.get(username.as_str()).copied() else {
488 return Err(Error::MissingUserFromDependentTable {
489 table: "user_external_ids".to_owned(),
490 user: synapse_user_id,
491 });
492 };
493
494 let Some(mas_user_id) = user_infos.mas_user_id else {
495 progress_counter.increment_skipped();
496 continue;
497 };
498
499 let Some(&upstream_provider_id) = state.provider_id_mapping.get(&auth_provider)
500 else {
501 if ignore_missing_auth_providers {
502 progress_counter.increment_skipped();
503 continue;
504 }
505 return Err(Error::MissingAuthProviderMapping {
506 synapse_id: auth_provider,
507 user: synapse_user_id,
508 });
509 };
510
511 let user_created_ts = Ulid::from(mas_user_id.get()).datetime_utc();
514
515 let link_id: Uuid = Ulid::from_datetime_with_rng(user_created_ts, &mut rng).into();
516
517 write_buffer
518 .write(
519 &mut mas,
520 MasNewUpstreamOauthLink {
521 link_id,
522 user_id: mas_user_id,
523 upstream_provider_id,
524 subject,
525 created_at: user_created_ts,
526 },
527 )
528 .await
529 .into_mas("failed to write upstream link")?;
530
531 progress_counter.increment_migrated();
532 }
533
534 write_buffer
535 .finish(&mut mas)
536 .await
537 .into_mas("writing upstream links")?;
538
539 Ok((mas, state))
540 }
541 .instrument(tracing::info_span!("ingest_task")),
542 );
543
544 let res = synapse
547 .read_user_external_ids()
548 .map_err(|e| e.into_synapse("reading external ID"))
549 .forward(PollSender::new(tx).sink_map_err(|_| Error::ChannelClosed))
550 .inspect_err(|e| tracing::error!(error = e as &dyn std::error::Error))
551 .await;
552
553 let (mas, state) = task.await.into_join("external IDs write task")??;
554
555 res?;
556
557 info!(
558 "{} upstream links (external IDs) migrated ({} skipped) in {:.1}s",
559 progress_counter_.migrated(),
560 progress_counter_.skipped(),
561 Instant::now().duration_since(start).as_secs_f64()
562 );
563
564 Ok((mas, state))
565}
566
567#[tracing::instrument(skip_all, level = Level::INFO)]
576async fn migrate_devices(
577 synapse: &mut SynapseReader<'_>,
578 mut mas: MasWriter,
579 rng: &mut impl RngCore,
580 mut state: MigrationState,
581 progress_counter: ProgressCounter,
582) -> Result<(MasWriter, MigrationState), Error> {
583 let start = Instant::now();
584 let progress_counter_ = progress_counter.clone();
585
586 let (tx, mut rx) = tokio::sync::mpsc::channel(100 * 1024);
587
588 let mut rng = rand_chacha::ChaChaRng::from_rng(rng).expect("failed to seed rng");
591 let task = tokio::spawn(
592 async move {
593 let mut write_buffer = MasWriteBuffer::new(&mas);
594
595 while let Some(device) = rx.recv().await {
596 let SynapseDevice {
597 user_id: synapse_user_id,
598 device_id,
599 display_name,
600 last_seen,
601 ip,
602 user_agent,
603 } = device;
604 let username = synapse_user_id
605 .extract_localpart(&state.server_name)
606 .into_extract_localpart(synapse_user_id.clone())?
607 .to_owned();
608 let Some(user_infos) = state.users.get(username.as_str()).copied() else {
609 return Err(Error::MissingUserFromDependentTable {
610 table: "devices".to_owned(),
611 user: synapse_user_id,
612 });
613 };
614
615 let Some(mas_user_id) = user_infos.mas_user_id else {
616 progress_counter.increment_skipped();
617 continue;
618 };
619
620 if user_infos.flags.is_deactivated()
621 || user_infos.flags.is_guest()
622 || user_infos.flags.is_appservice()
623 {
624 continue;
625 }
626
627 let session_id = *state
628 .devices_to_compat_sessions
629 .entry((mas_user_id, CompactString::new(&device_id)))
630 .or_insert_with(|| {
631 Ulid::from_datetime_with_rng(
634 #[expect(clippy::disallowed_methods)]
635 Utc::now(),
636 &mut rng,
637 )
638 .into()
639 });
640 let created_at = Ulid::from(session_id).datetime_utc();
641
642 let last_active_ip = ip.filter(|ip| ip != "-").and_then(|ip| {
648 ip.parse()
649 .map_err(|e| {
650 tracing::warn!(
651 error = &e as &dyn std::error::Error,
652 mxid = %synapse_user_id,
653 %device_id,
654 %ip,
655 "Failed to parse device IP, ignoring"
656 );
657 })
658 .ok()
659 });
660
661 write_buffer
662 .write(
663 &mut mas,
664 MasNewCompatSession {
665 session_id,
666 user_id: mas_user_id,
667 device_id: Some(device_id),
668 human_name: display_name,
669 created_at,
670 is_synapse_admin: user_infos.flags.is_synapse_admin(),
671 last_active_at: last_seen.map(DateTime::from),
672 last_active_ip,
673 user_agent,
674 },
675 )
676 .await
677 .into_mas("writing compat sessions")?;
678
679 progress_counter.increment_migrated();
680 }
681
682 write_buffer
683 .finish(&mut mas)
684 .await
685 .into_mas("writing compat sessions")?;
686
687 Ok((mas, state))
688 }
689 .instrument(tracing::info_span!("ingest_task")),
690 );
691
692 let res = synapse
695 .read_devices()
696 .map_err(|e| e.into_synapse("reading devices"))
697 .forward(PollSender::new(tx).sink_map_err(|_| Error::ChannelClosed))
698 .inspect_err(|e| tracing::error!(error = e as &dyn std::error::Error))
699 .await;
700
701 let (mas, state) = task.await.into_join("device write task")??;
702
703 res?;
704
705 info!(
706 "{} devices migrated ({} skipped) in {:.1}s",
707 progress_counter_.migrated(),
708 progress_counter_.skipped(),
709 Instant::now().duration_since(start).as_secs_f64()
710 );
711
712 Ok((mas, state))
713}
714
715#[tracing::instrument(skip_all, level = Level::INFO)]
718async fn migrate_unrefreshable_access_tokens(
719 synapse: &mut SynapseReader<'_>,
720 mut mas: MasWriter,
721 clock: &dyn Clock,
722 rng: &mut impl RngCore,
723 mut state: MigrationState,
724 progress_counter: ProgressCounter,
725) -> Result<(MasWriter, MigrationState), Error> {
726 let start = Instant::now();
727 let progress_counter_ = progress_counter.clone();
728
729 let (tx, mut rx) = tokio::sync::mpsc::channel(100 * 1024);
730
731 let now = clock.now();
732 let mut rng = rand_chacha::ChaChaRng::from_rng(rng).expect("failed to seed rng");
735 let task = tokio::spawn(
736 async move {
737 let mut write_buffer = MasWriteBuffer::new(&mas);
738 let mut deviceless_session_write_buffer = MasWriteBuffer::new(&mas);
739
740 while let Some(token) = rx.recv().await {
741 let SynapseAccessToken {
742 user_id: synapse_user_id,
743 device_id,
744 token,
745 valid_until_ms,
746 last_validated,
747 } = token;
748 let username = synapse_user_id
749 .extract_localpart(&state.server_name)
750 .into_extract_localpart(synapse_user_id.clone())?
751 .to_owned();
752 let Some(user_infos) = state.users.get(username.as_str()).copied() else {
753 return Err(Error::MissingUserFromDependentTable {
754 table: "access_tokens".to_owned(),
755 user: synapse_user_id,
756 });
757 };
758
759 let Some(mas_user_id) = user_infos.mas_user_id else {
760 progress_counter.increment_skipped();
761 continue;
762 };
763
764 if user_infos.flags.is_deactivated()
765 || user_infos.flags.is_guest()
766 || user_infos.flags.is_appservice()
767 {
768 progress_counter.increment_skipped();
769 continue;
770 }
771
772 let created_at = last_validated.map_or_else(|| now, DateTime::from);
776
777 let session_id = if let Some(device_id) = device_id {
778 *state
780 .devices_to_compat_sessions
781 .entry((mas_user_id, CompactString::new(&device_id)))
782 .or_insert_with(|| {
783 Uuid::from(Ulid::from_datetime_with_rng(created_at, &mut rng))
784 })
785 } else {
786 let deviceless_session_id =
789 Uuid::from(Ulid::from_datetime_with_rng(created_at, &mut rng));
790
791 deviceless_session_write_buffer
792 .write(
793 &mut mas,
794 MasNewCompatSession {
795 session_id: deviceless_session_id,
796 user_id: mas_user_id,
797 device_id: None,
798 human_name: None,
799 created_at,
800 is_synapse_admin: false,
801 last_active_at: None,
802 last_active_ip: None,
803 user_agent: None,
804 },
805 )
806 .await
807 .into_mas("failed to write deviceless compat sessions")?;
808
809 deviceless_session_id
810 };
811
812 let token_id = Uuid::from(Ulid::from_datetime_with_rng(created_at, &mut rng));
813
814 write_buffer
815 .write(
816 &mut mas,
817 MasNewCompatAccessToken {
818 token_id,
819 session_id,
820 access_token: token,
821 created_at,
822 expires_at: valid_until_ms.map(DateTime::from),
823 },
824 )
825 .await
826 .into_mas("writing compat access tokens")?;
827
828 progress_counter.increment_migrated();
829 }
830 write_buffer
831 .finish(&mut mas)
832 .await
833 .into_mas("writing compat access tokens")?;
834 deviceless_session_write_buffer
835 .finish(&mut mas)
836 .await
837 .into_mas("writing deviceless compat sessions")?;
838
839 Ok((mas, state))
840 }
841 .instrument(tracing::info_span!("ingest_task")),
842 );
843
844 let res = synapse
847 .read_unrefreshable_access_tokens()
848 .map_err(|e| e.into_synapse("reading tokens"))
849 .forward(PollSender::new(tx).sink_map_err(|_| Error::ChannelClosed))
850 .inspect_err(|e| tracing::error!(error = e as &dyn std::error::Error))
851 .await;
852
853 let (mas, state) = task.await.into_join("token write task")??;
854
855 res?;
856
857 info!(
858 "{} non-refreshable access tokens migrated ({} skipped) in {:.1}s",
859 progress_counter_.migrated(),
860 progress_counter_.skipped(),
861 Instant::now().duration_since(start).as_secs_f64()
862 );
863
864 Ok((mas, state))
865}
866
867#[tracing::instrument(skip_all, level = Level::INFO)]
870async fn migrate_refreshable_token_pairs(
871 synapse: &mut SynapseReader<'_>,
872 mut mas: MasWriter,
873 clock: &dyn Clock,
874 rng: &mut impl RngCore,
875 mut state: MigrationState,
876 progress_counter: ProgressCounter,
877) -> Result<(MasWriter, MigrationState), Error> {
878 let start = Instant::now();
879 let progress_counter_ = progress_counter.clone();
880
881 let (tx, mut rx) = tokio::sync::mpsc::channel::<SynapseRefreshableTokenPair>(100 * 1024);
882
883 let mut rng = rand_chacha::ChaChaRng::from_rng(rng).expect("failed to seed rng");
886 let now = clock.now();
887 let task = tokio::spawn(
888 async move {
889 let mut access_token_write_buffer = MasWriteBuffer::new(&mas);
890 let mut refresh_token_write_buffer = MasWriteBuffer::new(&mas);
891
892 while let Some(token) = rx.recv().await {
893 let SynapseRefreshableTokenPair {
894 user_id: synapse_user_id,
895 device_id,
896 access_token,
897 refresh_token,
898 valid_until_ms,
899 last_validated,
900 } = token;
901
902 let username = synapse_user_id
903 .extract_localpart(&state.server_name)
904 .into_extract_localpart(synapse_user_id.clone())?
905 .to_owned();
906 let Some(user_infos) = state.users.get(username.as_str()).copied() else {
907 return Err(Error::MissingUserFromDependentTable {
908 table: "refresh_tokens".to_owned(),
909 user: synapse_user_id,
910 });
911 };
912
913 let Some(mas_user_id) = user_infos.mas_user_id else {
914 progress_counter.increment_skipped();
915 continue;
916 };
917
918 if user_infos.flags.is_deactivated()
919 || user_infos.flags.is_guest()
920 || user_infos.flags.is_appservice()
921 {
922 progress_counter.increment_skipped();
923 continue;
924 }
925
926 let created_at = last_validated.map_or_else(|| now, DateTime::from);
930
931 let session_id = *state
933 .devices_to_compat_sessions
934 .entry((mas_user_id, CompactString::new(&device_id)))
935 .or_insert_with(|| {
936 Uuid::from(Ulid::from_datetime_with_rng(created_at, &mut rng))
937 });
938
939 let access_token_id =
940 Uuid::from(Ulid::from_datetime_with_rng(created_at, &mut rng));
941 let refresh_token_id =
942 Uuid::from(Ulid::from_datetime_with_rng(created_at, &mut rng));
943
944 access_token_write_buffer
945 .write(
946 &mut mas,
947 MasNewCompatAccessToken {
948 token_id: access_token_id,
949 session_id,
950 access_token,
951 created_at,
952 expires_at: valid_until_ms.map(DateTime::from),
953 },
954 )
955 .await
956 .into_mas("writing compat access tokens")?;
957 refresh_token_write_buffer
958 .write(
959 &mut mas,
960 MasNewCompatRefreshToken {
961 refresh_token_id,
962 session_id,
963 access_token_id,
964 refresh_token,
965 created_at,
966 },
967 )
968 .await
969 .into_mas("writing compat refresh tokens")?;
970
971 progress_counter.increment_migrated();
972 }
973
974 access_token_write_buffer
975 .finish(&mut mas)
976 .await
977 .into_mas("writing compat access tokens")?;
978
979 refresh_token_write_buffer
980 .finish(&mut mas)
981 .await
982 .into_mas("writing compat refresh tokens")?;
983 Ok((mas, state))
984 }
985 .instrument(tracing::info_span!("ingest_task")),
986 );
987
988 let res = synapse
991 .read_refreshable_token_pairs()
992 .map_err(|e| e.into_synapse("reading refresh token pairs"))
993 .forward(PollSender::new(tx).sink_map_err(|_| Error::ChannelClosed))
994 .inspect_err(|e| tracing::error!(error = e as &dyn std::error::Error))
995 .await;
996
997 let (mas, state) = task.await.into_join("refresh token write task")??;
998
999 res?;
1000
1001 info!(
1002 "{} refreshable token pairs migrated ({} skipped) in {:.1}s",
1003 progress_counter_.migrated(),
1004 progress_counter_.skipped(),
1005 Instant::now().duration_since(start).as_secs_f64()
1006 );
1007
1008 Ok((mas, state))
1009}
1010
1011fn transform_user(
1012 user: &SynapseUser,
1013 server_name: &str,
1014 rng: &mut impl RngCore,
1015) -> Result<(MasNewUser, Option<MasNewUserPassword>), Error> {
1016 let username = user
1017 .name
1018 .extract_localpart(server_name)
1019 .into_extract_localpart(user.name.clone())?
1020 .to_owned();
1021
1022 let user_id = Uuid::from(Ulid::from_datetime_with_rng(
1023 DateTime::<Utc>::from(user.creation_ts),
1024 rng,
1025 ))
1026 .try_into()
1027 .expect("ULID generation lead to a nil UUID, this is a bug!");
1028
1029 let new_user = MasNewUser {
1030 user_id,
1031 username,
1032 created_at: user.creation_ts.into(),
1033 locked_at: user.locked.then_some(user.creation_ts.into()),
1034 deactivated_at: bool::from(user.deactivated).then_some(user.creation_ts.into()),
1035 can_request_admin: bool::from(user.admin),
1036 is_guest: bool::from(user.is_guest),
1037 };
1038
1039 let mas_password = user
1040 .password_hash
1041 .clone()
1042 .map(|password_hash| MasNewUserPassword {
1043 user_password_id: Uuid::from(Ulid::from_datetime_with_rng(
1044 DateTime::<Utc>::from(user.creation_ts),
1045 rng,
1046 )),
1047 user_id: new_user.user_id,
1048 hashed_password: password_hash,
1049 created_at: new_user.created_at,
1050 });
1051
1052 Ok((new_user, mas_password))
1053}