Skip to main content

headless_lms_models/library/credit_registration/
sisu_day_gap.rs

1//! The nightly hours when Finland is already on the next day but Sisu, which dates attainments by
2//! the UTC day, is not: 00:00–03:00 Finnish time in summer, 00:00–02:00 in winter. Suotar dates an
3//! attainment by its Finnish day, so one dated today is in the future for Sisu then, and Sisu
4//! refuses it. Importing waits the gap out.
5
6use chrono::TimeDelta;
7
8use crate::prelude::*;
9
10/// The gap starts this long before Finnish midnight, so a batch claimed just before it reaches
11/// Sisu in time.
12const SISU_DAY_GAP_LEAD: TimeDelta = TimeDelta::minutes(5);
13/// The gap ends this long after UTC midnight, for clock differences between us, Suotar and Sisu.
14const SISU_DAY_GAP_TAIL: TimeDelta = TimeDelta::minutes(5);
15/// How long after the gap the imports it held back are spread over, so they do not all reach
16/// Suotar the moment it ends.
17const SISU_DAY_GAP_SPREAD: TimeDelta = TimeDelta::hours(2);
18
19/// When the gap we are in ends, or `None` outside it. Read off the database's clock and time zone
20/// data, which knows when Finland changes to and from summer time.
21pub async fn current_gap_end(conn: &mut PgConnection) -> ModelResult<Option<DateTime<Utc>>> {
22    let gap_end = sqlx::query_scalar!(
23        r#"
24SELECT CASE
25    WHEN ((now() + make_interval(secs => $1)) AT TIME ZONE 'Europe/Helsinki')::date
26      > ((now() - make_interval(secs => $2)) AT TIME ZONE 'UTC')::date
27    THEN (
28      ((now() + make_interval(secs => $1)) AT TIME ZONE 'Europe/Helsinki')::date::timestamp
29        AT TIME ZONE 'UTC'
30    ) + make_interval(secs => $2)
31  END AS "gap_end?: DateTime<Utc>"
32        "#,
33        SISU_DAY_GAP_LEAD.num_seconds() as f64,
34        SISU_DAY_GAP_TAIL.num_seconds() as f64,
35    )
36    .fetch_one(conn)
37    .await?;
38    Ok(gap_end)
39}
40
41/// Moves every import due before `gap_end` to a random moment in the [`SISU_DAY_GAP_SPREAD`]
42/// after it. A moved row is no longer due before `gap_end`, so running this on every tick of the
43/// gap moves each row once. Returns how many rows moved.
44pub async fn spread_imports_past_gap(
45    conn: &mut PgConnection,
46    gap_end: DateTime<Utc>,
47) -> ModelResult<u64> {
48    let moved = sqlx::query!(
49        r#"
50UPDATE credit_registrations
51SET next_attempt_at = $1 + random() * make_interval(secs => $2)
52WHERE state = 'checking_enrolment'
53  AND deleted_at IS NULL
54  AND next_attempt_at < $1
55        "#,
56        gap_end,
57        SISU_DAY_GAP_SPREAD.num_seconds() as f64,
58    )
59    .execute(conn)
60    .await?;
61    Ok(moved.rows_affected())
62}