Skip to main content

headless_lms_server/controllers/tmc_server/
mod.rs

1/*!
2Handlers for HTTP requests to `/api/v0/tmc-server`.
3
4These endpoints are used by the TMC server so that it can integrate with this system.
5*/
6
7pub mod users;
8pub mod users_by_upstream_id;
9
10use crate::prelude::*;
11use headless_lms_utils::tmc::TmcClient;
12
13/// Add controllers from all the submodules.
14pub fn _add_routes(cfg: &mut ServiceConfig) {
15    cfg.service(web::scope("/users-by-upstream-id").configure(users_by_upstream_id::_add_routes))
16        .service(web::scope("/users").configure(users::_add_routes));
17}
18
19/// Notify TMC that a user's password is now managed by courses.mooc.fi.
20///
21/// This is **best-effort and never fails the caller**: by the time it runs the password has
22/// already been stored locally, so a transient TMC outage must not turn a successful
23/// signup/login/migration into an error. It retries a few times inline (so common blips are
24/// resolved before the HTTP response) and, if those are exhausted, hands off to a background
25/// task that keeps retrying with backoff.
26pub async fn notify_password_managed_with_retry(
27    tmc_client: &TmcClient,
28    upstream_id: String,
29    user_id: Uuid,
30) {
31    const MAX_ATTEMPTS_INLINE: u32 = 3;
32    const MAX_DELAY_MS_INLINE: u64 = 2_000;
33    for attempt in 1..=MAX_ATTEMPTS_INLINE {
34        match tmc_client
35            .set_user_password_managed_by_courses_mooc_fi(upstream_id.clone(), user_id)
36            .await
37        {
38            Ok(_) => return,
39            Err(e) if attempt < MAX_ATTEMPTS_INLINE => {
40                let delay = std::time::Duration::from_millis(
41                    200u64
42                        .saturating_mul(2u64.pow(attempt - 1))
43                        .min(MAX_DELAY_MS_INLINE),
44                );
45                warn!(
46                    "Failed to notify TMC that user's password is saved in courses.mooc.fi (inline attempt {}/{}), retrying in {:?}: upstream_id={}, user_id={}, error={}",
47                    attempt, MAX_ATTEMPTS_INLINE, delay, upstream_id, user_id, e
48                );
49                tokio::time::sleep(delay).await;
50            }
51            Err(e) => {
52                warn!(
53                    "Inline TMC notification attempts exhausted, handing off to background task: upstream_id={}, user_id={}, error={}",
54                    upstream_id, user_id, e
55                );
56            }
57        }
58    }
59
60    let tmc_client = tmc_client.clone();
61    tokio::spawn(async move {
62        const MAX_ATTEMPTS_BG: u32 = 10;
63        const MAX_DELAY_MS_BG: u64 = 30_000;
64        for attempt in 1..=MAX_ATTEMPTS_BG {
65            match tmc_client
66                .set_user_password_managed_by_courses_mooc_fi(upstream_id.clone(), user_id)
67                .await
68            {
69                Ok(_) => {
70                    info!(
71                        "Background TMC notification succeeded on attempt {}: upstream_id={}, user_id={}",
72                        attempt, upstream_id, user_id
73                    );
74                    return;
75                }
76                Err(e) if attempt < MAX_ATTEMPTS_BG => {
77                    let delay = std::time::Duration::from_millis(
78                        200u64
79                            .saturating_mul(2u64.pow(attempt - 1))
80                            .min(MAX_DELAY_MS_BG),
81                    );
82                    warn!(
83                        "Background TMC notification failed (attempt {}/{}), retrying in {:?}: upstream_id={}, user_id={}, error={}",
84                        attempt, MAX_ATTEMPTS_BG, delay, upstream_id, user_id, e
85                    );
86                    tokio::time::sleep(delay).await;
87                }
88                Err(e) => {
89                    error!(
90                        "Background TMC notification exhausted all {} retries: upstream_id={}, user_id={}, error={}",
91                        MAX_ATTEMPTS_BG, upstream_id, user_id, e
92                    );
93                }
94            }
95        }
96    });
97}