Skip to main content

autoconnect_ws_sm/identified/
on_server_notif.rs

1#[cfg(feature = "reliable_report")]
2use std::mem;
3
4use cadence::{Counted, Timed};
5
6use autoconnect_common::protocol::{ServerMessage, ServerNotification};
7use autopush_common::{
8    db::CheckStorageResponse, metric_name::MetricName, metrics::StatsdClientExt,
9    notification::Notification, util::sec_since_epoch,
10};
11
12use super::WebPushClient;
13use crate::error::{SMError, SMErrorKind};
14
15/// Where a Notification being sent to the client originated, used to tag send
16/// metrics and to gate storage-only measurements.
17#[derive(Clone, Copy, PartialEq, Eq)]
18enum SendSource {
19    /// Pushed live to a connected client (never entered storage).
20    Direct,
21    /// Retrieved from storage on (re)connect.
22    Stored,
23}
24
25impl SendSource {
26    /// The tag value emitted to metrics.
27    fn as_tag(&self) -> &'static str {
28        match self {
29            SendSource::Direct => "Direct",
30            SendSource::Stored => "Stored",
31        }
32    }
33}
34
35impl WebPushClient {
36    /// Handle a `ServerNotification` for this user
37    ///
38    /// `ServerNotification::Disconnect` is emitted by the same autoconnect
39    /// node receiving it when a User has logged into that same node twice to
40    /// "Ghost" (disconnect) the first user's session for its second session.
41    ///
42    /// Other variants are emitted by autoendpoint
43    pub async fn on_server_notif(
44        &mut self,
45        snotif: ServerNotification,
46    ) -> Result<Vec<ServerMessage>, SMError> {
47        match snotif {
48            ServerNotification::Notification(notif) => Ok(vec![self.notif(notif)?]),
49            ServerNotification::CheckStorage => self.check_storage().await,
50            ServerNotification::Disconnect => Err(SMErrorKind::Ghost.into()),
51        }
52    }
53
54    /// After disconnecting from the `ClientRegistry`, moves any queued Direct
55    /// Push Notifications to unacked_direct_notifs (to be stored in the db on
56    /// `shutdown`)
57    pub fn on_server_notif_shutdown(&mut self, snotif: ServerNotification) {
58        trace!("WebPushClient::on_server_notif_shutdown");
59        if let ServerNotification::Notification(notif) = snotif {
60            let key = notif.version.clone();
61            self.ack_state.unacked_direct_notifs.insert(key, notif);
62        }
63    }
64
65    /// Send a Direct Push Notification to this user
66    fn notif(&mut self, notif: Notification) -> Result<ServerMessage, SMError> {
67        trace!("WebPushClient::notif Sending a direct notif");
68        // The notification we return here is sent directly to the client.
69        // No reliability state is recorded.
70        let response = notif.clone();
71        if notif.ttl != 0 {
72            // Consume the original notification by adding it to the
73            // unacked map. This will eventually record the state.
74            let key = notif.version.clone();
75            self.ack_state.unacked_direct_notifs.insert(key, notif);
76        }
77        self.emit_send_metrics(&response, SendSource::Direct);
78        Ok(ServerMessage::Notification(response))
79    }
80
81    /// Top level read of Push Notifications from storage
82    ///
83    /// Initializes the top level `check_storage` and `include_topic` flags and
84    /// runs `check_storage_loop`
85    pub(super) async fn check_storage(&mut self) -> Result<Vec<ServerMessage>, SMError> {
86        trace!("🗄️ WebPushClient::check_storage");
87        self.flags.check_storage = true;
88        self.flags.include_topic = true;
89        self.check_storage_loop().await
90    }
91
92    /// Loop the read of Push Notifications from storage
93    ///
94    /// Loops until any unexpired Push Notifications are read or there's no
95    /// more Notifications in storage
96    pub(super) async fn check_storage_loop(&mut self) -> Result<Vec<ServerMessage>, SMError> {
97        trace!("🗄️ WebPushClient::check_storage_loop");
98        while self.flags.check_storage {
99            let smsgs = self.check_storage_advance().await?;
100            if !smsgs.is_empty() {
101                self.check_msg_limit().await?;
102                return Ok(smsgs);
103            }
104        }
105        // No more notifications (check_storage = false). Despite returning no
106        // notifs we may have advanced through expired timestamp messages and
107        // need to increment_storage to mark them as deleted
108        if self.flags.increment_storage {
109            debug!("🗄️ WebPushClient::check_storage_loop increment_storage");
110            self.increment_storage().await?;
111        }
112        Ok(vec![])
113    }
114
115    /// Read a chunk (max count 10 returned) of Notifications from storage
116    ///
117    /// This filters out expired Notifications and may return an empty result
118    /// set when there's still pending Notifications to be read: so it should
119    /// be called in a loop to advance through all Notification records
120    async fn check_storage_advance(&mut self) -> Result<Vec<ServerMessage>, SMError> {
121        trace!("🗄️ WebPushClient::check_storage_advance");
122        let CheckStorageResponse {
123            include_topic,
124            mut messages,
125            timestamp,
126        } = self.do_check_storage().await?;
127
128        debug!(
129            "🗄️ WebPushClient::check_storage_advance \
130                 include_topic: {} -> {} \
131                 unacked_stored_highest: {:?} -> {:?}",
132            self.flags.include_topic,
133            include_topic,
134            self.ack_state.unacked_stored_highest,
135            timestamp
136        );
137        self.flags.include_topic = include_topic;
138        self.ack_state.unacked_stored_highest = timestamp;
139
140        if messages.is_empty() {
141            trace!("🗄️ WebPushClient::check_storage_advance finished");
142            self.flags.check_storage = false;
143            self.sent_from_storage = 0;
144            return Ok(vec![]);
145        }
146
147        // Filter out TTL expired messages
148        let now_sec = sec_since_epoch();
149        // Topic messages require immediate deletion from the db
150        let mut expired_messages = vec![];
151        // NOTE: Vec::extract_if (stabilizing soon) can negate the need for the
152        // inner msg.clone()
153        messages.retain(|msg| {
154            if !msg.expired(now_sec) {
155                return true;
156            }
157            if msg.sortkey_timestamp.is_none() {
158                expired_messages.push(msg.clone());
159            }
160            false
161        });
162        // TODO: A batch remove_messages would be nicer
163        #[allow(unused_mut)]
164        for mut msg in expired_messages {
165            let chidmessageid = msg.chidmessageid();
166            trace!("🉑 removing expired topic chidmessageid: {chidmessageid}");
167            self.app_state
168                .db
169                .remove_message(&self.uaid, &chidmessageid)
170                .await?;
171            #[cfg(feature = "reliable_report")]
172            msg.record_reliability(
173                &self.app_state.reliability,
174                autopush_common::reliability::ReliabilityState::Expired,
175            )
176            .await;
177        }
178
179        self.flags.increment_storage = !include_topic && timestamp.is_some();
180
181        if messages.is_empty() {
182            trace!("🗄️ WebPushClient::check_storage_advance empty response (filtered expired)");
183            return Ok(vec![]);
184        }
185
186        for msg in messages.iter() {
187            let key = msg.version.clone();
188            self.ack_state
189                .unacked_stored_notifs
190                .insert(key, msg.clone());
191        }
192        let smsgs: Vec<_> = messages
193            .into_iter()
194            .inspect(|msg| {
195                trace!("🗄️ WebPushClient::check_storage_advance Sending stored");
196                self.emit_send_metrics(msg, SendSource::Stored)
197            })
198            .map(ServerMessage::Notification)
199            .collect();
200
201        let count = smsgs.len() as u32;
202        debug!(
203            "🗄️ WebPushClient::check_storage_advance: sent_from_storage: {}, +{}",
204            self.sent_from_storage, count
205        );
206        self.sent_from_storage += count;
207        Ok(smsgs)
208    }
209
210    #[cfg(feature = "reliable_report")]
211    /// Record and transition the state for trackable messages.
212    async fn record_state(
213        &self,
214        messages: &mut Vec<Notification>,
215        state: autopush_common::reliability::ReliabilityState,
216    ) {
217        // *Note* because `.map()` is sync
218        // we can't call the async func without additional hoops.
219        for message in messages {
220            message
221                .record_reliability(&self.app_state.reliability, state)
222                .await;
223        }
224    }
225
226    /// Read a chunk (max count 10 returned) of Notifications from storage
227    ///
228    /// This alternates between reading Topic Notifications and Timestamp
229    /// Notifications which are stored separately in storage.
230    ///
231    /// Topic Messages differ in that they replace pending Notifications with
232    /// new ones if they have matching Topic names. They are used when a sender
233    /// desires a scenario where multiple Messages sent to an offline device
234    /// result in the user only seeing the latest Message when the device comes
235    /// online.
236    async fn do_check_storage(&self) -> Result<CheckStorageResponse, SMError> {
237        // start at the latest unacked timestamp or the previous, latest timestamp.
238        let timestamp = self
239            .ack_state
240            .unacked_stored_highest
241            .or(self.current_timestamp);
242        trace!("🗄️ WebPushClient::do_check_storage {:?}", &timestamp);
243        // if we're to include topic messages, do those first.
244        // NOTE: Bigtable can't fetch `current_timestamp`, so we can't rely on
245        // `fetch_topic_messages()` returning a reasonable timestamp.
246        let topic_resp = if self.flags.include_topic {
247            trace!("🗄️ WebPushClient::do_check_storage: fetch_topic_messages");
248            // Get the most recent max 11 messages.
249            #[allow(unused_mut)]
250            let mut messages = self
251                .app_state
252                .db
253                .fetch_topic_messages(&self.uaid, 11)
254                .await?;
255            #[cfg(feature = "reliable_report")]
256            // Since we pulled these from storage, mark them as "retrieved"
257            self.record_state(
258                &mut messages.messages,
259                autopush_common::reliability::ReliabilityState::Retrieved,
260            )
261            .await;
262            messages
263        } else {
264            Default::default()
265        };
266        // if we have topic messages...
267        if !topic_resp.messages.is_empty() {
268            trace!(
269                "🗄️ WebPushClient::do_check_storage: Topic message returns: {:#?}",
270                topic_resp.messages
271            );
272            self.app_state
273                .metrics
274                .count_with_tags(
275                    "notification.message.retrieved",
276                    topic_resp.messages.len() as i64,
277                )
278                .with_tag("topic", "true")
279                .send();
280            return Ok(CheckStorageResponse {
281                include_topic: true,
282                messages: topic_resp.messages,
283                timestamp: topic_resp.timestamp,
284            });
285        }
286        // No topic messages, so carry on with normal ones, starting from the latest timestamp.
287        let timestamp = if self.flags.include_topic {
288            // See above, but Bigtable doesn't return the last message read timestamp when polling
289            // for topic messages. Instead, we'll use the explicitly set one we store in the User
290            // record and copy into the WebPushClient struct.
291            topic_resp.timestamp.or(self.current_timestamp)
292        } else {
293            timestamp
294        };
295        trace!(
296            "🗄️ WebPushClient::do_check_storage: fetch_timestamp_messages timestamp: {:?}",
297            timestamp
298        );
299        #[allow(unused_mut)]
300        let mut timestamp_resp = self
301            .app_state
302            .db
303            .fetch_timestamp_messages(&self.uaid, timestamp, 10)
304            .await?;
305        if !timestamp_resp.messages.is_empty() {
306            trace!(
307                "🗄️ WebPushClient::do_check_storage: Timestamp message returns: {:#?}",
308                timestamp_resp.messages
309            );
310            self.app_state
311                .metrics
312                .count_with_tags(
313                    "notification.message.retrieved",
314                    timestamp_resp.messages.len() as i64,
315                )
316                .with_tag("topic", "false")
317                .send();
318            #[cfg(feature = "reliable_report")]
319            // Since we pulled these from storage, mark them as "retrieved"
320            self.record_state(
321                &mut timestamp_resp.messages,
322                autopush_common::reliability::ReliabilityState::Retrieved,
323            )
324            .await;
325        }
326
327        Ok(CheckStorageResponse {
328            include_topic: false,
329            messages: timestamp_resp.messages,
330            // If we didn't get a timestamp off the last query, use the
331            // original value if passed one
332            timestamp: timestamp_resp.timestamp.or(timestamp),
333        })
334    }
335
336    /// Update the user's last Message read timestamp (for timestamp Messages)
337    ///
338    /// Called when a Client's Ack'd all timestamp messages sent to it to move
339    /// the timestamp Messages' "pointer". See
340    /// `AckState::unacked_stored_highest` for further information.
341    pub(super) async fn increment_storage(&mut self) -> Result<(), SMError> {
342        trace!(
343            "🗄️ WebPushClient::increment_storage: unacked_stored_highest: {:?}",
344            self.ack_state.unacked_stored_highest
345        );
346        let Some(timestamp) = self.ack_state.unacked_stored_highest else {
347            return Err(SMErrorKind::Internal(
348                "increment_storage w/ no unacked_stored_highest".to_owned(),
349            )
350            .into());
351        };
352        self.current_timestamp = Some(timestamp);
353        self.app_state
354            .db
355            .increment_storage(&self.uaid, timestamp)
356            .await?;
357        #[cfg(feature = "reliable_report")]
358        {
359            let mut notifs = mem::take(&mut self.ack_state.acked_stored_timestamp_notifs);
360            self.record_state(
361                &mut notifs,
362                autopush_common::reliability::ReliabilityState::Delivered,
363            )
364            .await;
365        }
366        self.flags.increment_storage = false;
367        Ok(())
368    }
369
370    /// Ensure this user hasn't exceeded the maximum allowed number of messages
371    /// read from storage (`Settings::msg_limit`)
372    ///
373    /// Drops the user record and returns the `SMErrorKind::UaidReset` error if
374    /// they have
375    async fn check_msg_limit(&mut self) -> Result<(), SMError> {
376        trace!(
377            "WebPushClient::check_msg_limit: sent_from_storage: {} msg_limit: {}",
378            self.sent_from_storage, self.app_state.settings.msg_limit
379        );
380        if self.sent_from_storage > self.app_state.settings.msg_limit {
381            // Exceeded the max limit of stored messages: drop the user to
382            // trigger a re-register
383            self.app_state
384                .metrics
385                .incr_with_tags(MetricName::UaExpiration)
386                .with_tag("reason", "too_many_messages")
387                .send();
388            self.app_state.db.remove_user(&self.uaid).await?;
389            return Err(SMErrorKind::UaidReset.into());
390        }
391        Ok(())
392    }
393
394    /// Emit metrics for a Notification to be sent to the user
395    fn emit_send_metrics(&self, notif: &Notification, source: SendSource) {
396        let metrics = &self.app_state.metrics;
397        let ua_info = &self.ua_info;
398        metrics
399            .incr_with_tags(MetricName::UaNotificationSent)
400            .with_tag("source", source.as_tag())
401            .with_tag("topic", &notif.topic.is_some().to_string())
402            .with_tag("os", &ua_info.metrics_os)
403            // TODO: include `internal` if meta is set
404            .send();
405        metrics
406            .count_with_tags(
407                "ua.message_data",
408                notif.data.as_ref().map_or(0, |data| data.len() as i64),
409            )
410            .with_tag("source", source.as_tag())
411            .with_tag("os", &ua_info.metrics_os)
412            .send();
413        // For messages pulled from storage, record how long they were stored before delivery.
414        // Don't record for direct sends (never entered storage)
415        if source == SendSource::Stored {
416            let stored_ms = sec_since_epoch().saturating_sub(notif.timestamp) * 1000;
417            metrics
418                .time_with_tags(MetricName::NotificationStorageTime.as_ref(), stored_ms)
419                .with_tag("topic", &notif.topic.is_some().to_string())
420                .with_tag("os", &ua_info.metrics_os)
421                .send();
422        }
423    }
424}