Skip to main content

rpfm_server/
background_thread.rs

1//---------------------------------------------------------------------------//
2// Copyright (c) 2017-2026 Ismael Gutiérrez González. All rights reserved.
3//
4// This file is part of the Rusted PackFile Manager (RPFM) project,
5// which can be found here: https://github.com/Frodo45127/rpfm.
6//
7// This file is licensed under the MIT license, which can be found here:
8// https://github.com/Frodo45127/rpfm/blob/master/LICENSE.
9//---------------------------------------------------------------------------//
10
11//! Per-session request loop.
12//!
13//! Each [`Session`] spawns one thread running [`background_loop`]. The loop pulls [`SessionMessage`]s off the
14//! session's mpsc channel and runs each request on the session's [`SessionState`] through [`crate::api`],
15//! with the session's settings.
16//!
17//! Running requests serially per session is what keeps state consistent across many concurrent requests in the
18//! same session: a `pack.save` followed by a `pack.close` always sees the right pack, even when the WebSocket
19//! multiplexer is firing requests as fast as the client sends them. The exception are diagnostics checks: they stop
20//! when other requests arrive, and get queued again behind them, so a check never makes the client wait.
21//!
22//! Telemetry: each request is recorded via [`rpfm_telemetry::record_action`] so usage counters reflect what the
23//! session actually did.
24
25use tokio::sync::mpsc::UnboundedReceiver;
26
27use std::sync::Arc;
28
29use rpfm_telemetry::info;
30
31use crate::api::{self, JobContext};
32use crate::session::{Session, SessionMessage};
33use crate::state::SessionState;
34
35/// The per-session request loop.
36///
37/// Receives requests from the session's mpsc `receiver` and runs them serially against the session's
38/// [`SessionState`]. Requests get their response through their reply sender, and jobs through the session's jobs.
39///
40/// One instance runs per [`Session`], on a thread spawned by [`Session::new`]. The loop terminates when the session is
41/// dropped or [`SessionMessage::Exit`] arrives.
42pub fn background_loop(mut receiver: UnboundedReceiver<SessionMessage>, session: Arc<Session>) {
43    let mut state = SessionState::new(session.clone());
44
45    info!("Background Thread looping around…");
46    while let Some(message) = receiver.blocking_recv() {
47        match message {
48            SessionMessage::Api(request, sender) => {
49                session.dequeued();
50                rpfm_telemetry::record_action(&request.method);
51                let settings = session.settings();
52                let context = JobContext::new(&|_| {}, &|_, _| true);
53                let _ = sender.send(api::dispatch(&mut state, request, &settings, &context));
54            }
55            SessionMessage::Job(job, request) => {
56                session.dequeued();
57                let jobs = session.jobs().clone();
58                if !jobs.start(job) {
59                    continue;
60                }
61
62                rpfm_telemetry::record_action(&request.method);
63                let settings = session.settings();
64                let report_stage = |stage: &str| jobs.set_stage(job, stage);
65                let report_progress = |done: usize, total: usize| {
66                    jobs.set_progress(job, (done * 100 / total.max(1)).min(100) as u8);
67                    !session.has_queued_requests()
68                };
69
70                let context = JobContext::new(&report_stage, &report_progress);
71
72                // The request is kept to queue it again if the job stops for the requests behind it.
73                let response = api::dispatch(&mut state, request.clone(), &settings, &context);
74                if context.yielded() {
75                    session.requeue_diagnostics_check(job, request);
76                } else {
77                    jobs.finish(job, response.outcome);
78                }
79            }
80            SessionMessage::Autosave => {
81                session.dequeued();
82                state.autosave(&session.settings());
83            }
84            SessionMessage::Exit => break,
85        }
86    }
87
88    // Let a running autosave finish, so it doesn't leave a half-written backup behind.
89    state.wait_for_autosave();
90}