mas_handlers/activity_tracker/
mod.rs1mod bound;
9mod worker;
10
11use std::net::IpAddr;
12
13use chrono::{DateTime, Utc};
14use mas_context::LogContext;
15use mas_data_model::{
16 BrowserSession, Clock, CompatSession, Session, personal::session::PersonalSession,
17};
18use mas_storage::BoxRepositoryFactory;
19use tokio_util::{sync::CancellationToken, task::TaskTracker};
20use ulid::Ulid;
21
22pub use self::bound::Bound;
23use self::worker::Worker;
24
25static MESSAGE_QUEUE_SIZE: usize = 1000;
26
27#[derive(Clone, Copy, Debug, PartialOrd, PartialEq, Eq, Hash)]
28enum SessionKind {
29 OAuth2,
30 Compat,
31 Personal,
33 Browser,
34}
35
36impl SessionKind {
37 const fn as_str(self) -> &'static str {
38 match self {
39 SessionKind::OAuth2 => "oauth2",
40 SessionKind::Compat => "compat",
41 SessionKind::Personal => "personal",
42 SessionKind::Browser => "browser",
43 }
44 }
45}
46
47enum Message {
48 Record {
49 kind: SessionKind,
50 id: Ulid,
51 date_time: DateTime<Utc>,
52 ip: Option<IpAddr>,
53 },
54 Flush(tokio::sync::oneshot::Sender<()>),
55}
56
57#[derive(Clone)]
58pub struct ActivityTracker {
59 channel: tokio::sync::mpsc::Sender<Message>,
60}
61
62impl ActivityTracker {
63 #[must_use]
69 pub fn new(
70 repository_factory: BoxRepositoryFactory,
71 flush_interval: std::time::Duration,
72 task_tracker: &TaskTracker,
73 cancellation_token: CancellationToken,
74 ) -> Self {
75 let worker = Worker::new(repository_factory);
76 let (sender, receiver) = tokio::sync::mpsc::channel(MESSAGE_QUEUE_SIZE);
77 let tracker = ActivityTracker { channel: sender };
78
79 task_tracker.spawn(LogContext::new("activity-tracker-flush").run(|| {
81 tracker
82 .clone()
83 .flush_loop(flush_interval, cancellation_token.clone())
84 }));
85 task_tracker.spawn(
86 LogContext::new("activity-tracker").run(|| worker.run(receiver, cancellation_token)),
87 );
88
89 tracker
90 }
91
92 #[must_use]
94 pub fn bind(self, ip: Option<IpAddr>) -> Bound {
95 Bound::new(self, ip)
96 }
97
98 pub async fn record_oauth2_session(
100 &self,
101 clock: &dyn Clock,
102 session: &Session,
103 ip: Option<IpAddr>,
104 ) {
105 let res = self
106 .channel
107 .send(Message::Record {
108 kind: SessionKind::OAuth2,
109 id: session.id,
110 date_time: clock.now(),
111 ip,
112 })
113 .await;
114
115 if let Err(e) = res {
116 tracing::error!("Failed to record OAuth2 session: {}", e);
117 }
118 }
119
120 pub async fn record_personal_session(
122 &self,
123 clock: &dyn Clock,
124 session: &PersonalSession,
125 ip: Option<IpAddr>,
126 ) {
127 let res = self
128 .channel
129 .send(Message::Record {
130 kind: SessionKind::Personal,
131 id: session.id,
132 date_time: clock.now(),
133 ip,
134 })
135 .await;
136
137 if let Err(e) = res {
138 tracing::error!("Failed to record Personal session: {}", e);
139 }
140 }
141
142 pub async fn record_compat_session(
144 &self,
145 clock: &dyn Clock,
146 compat_session: &CompatSession,
147 ip: Option<IpAddr>,
148 ) {
149 let res = self
150 .channel
151 .send(Message::Record {
152 kind: SessionKind::Compat,
153 id: compat_session.id,
154 date_time: clock.now(),
155 ip,
156 })
157 .await;
158
159 if let Err(e) = res {
160 tracing::error!("Failed to record compat session: {}", e);
161 }
162 }
163
164 pub async fn record_browser_session(
166 &self,
167 clock: &dyn Clock,
168 browser_session: &BrowserSession,
169 ip: Option<IpAddr>,
170 ) {
171 let res = self
172 .channel
173 .send(Message::Record {
174 kind: SessionKind::Browser,
175 id: browser_session.id,
176 date_time: clock.now(),
177 ip,
178 })
179 .await;
180
181 if let Err(e) = res {
182 tracing::error!("Failed to record browser session: {}", e);
183 }
184 }
185
186 pub async fn flush(&self) {
188 let (tx, rx) = tokio::sync::oneshot::channel();
189 let res = self.channel.send(Message::Flush(tx)).await;
190
191 match res {
192 Ok(()) => {
193 if let Err(e) = rx.await {
194 tracing::error!(
195 error = &e as &dyn std::error::Error,
196 "Failed to flush activity tracker"
197 );
198 }
199 }
200 Err(e) => {
201 tracing::error!(
202 error = &e as &dyn std::error::Error,
203 "Failed to flush activity tracker"
204 );
205 }
206 }
207 }
208
209 async fn flush_loop(
211 self,
212 interval: std::time::Duration,
213 cancellation_token: CancellationToken,
214 ) {
215 let _guard = cancellation_token.clone().drop_guard();
218 let mut interval = tokio::time::interval(interval);
219 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
220
221 loop {
222 tokio::select! {
223 biased;
224
225 () = cancellation_token.cancelled() => {
226 return;
228 }
229
230 () = self.channel.closed() => {
232 return;
234 }
235
236
237 _ = interval.tick() => {
238 self.flush().await;
239 }
240 }
241 }
242 }
243}