1// Copyright 2023 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
1415//! A wrapper around a hash map that tracks pending requests and makes sure
16//! that expired requests are removed.
1718use indexmap::{map::Entry, IndexMap};
19use ruma::time::{Duration, Instant};
20use tracing::warn;
21use uuid::Uuid;
2223/// Configuration of limits for the outgoing request handling.
24#[derive(Clone, Debug)]
25pub(crate) struct RequestLimits {
26/// Maximum amount of unanswered (pending) requests that the client widget
27 /// API is going to process before starting to drop them. This ensures
28 /// that a buggy widget cannot force the client machine to consume memory
29 /// indefinitely.
30pub(crate) max_pending_requests: usize,
31/// For how long can the unanswered (pending) request stored in a map before
32 /// it is dropped. This ensures that requests that are not answered within
33 /// a ceratin amount of time, are dropped/cleaned up (considered as failed).
34pub(crate) response_timeout: Duration,
35}
3637/// A wrapper around a hash map that ensures that the request limits
38/// are taken into account.
39///
40/// Expired requests get cleaned up so that the hashmap remains
41/// limited to a certain amount of pending requests.
42pub(super) struct PendingRequests<T> {
43 requests: IndexMap<Uuid, Expirable<T>>,
44 limits: RequestLimits,
45}
4647impl<T> PendingRequests<T> {
48pub(super) fn new(limits: RequestLimits) -> Self {
49Self { requests: IndexMap::with_capacity(limits.max_pending_requests), limits }
50 }
5152/// Inserts a new request into the map.
53 ///
54 /// Returns `None` if the maximum allowed capacity is reached.
55pub(super) fn insert(&mut self, key: Uuid, value: T) -> Option<&mut T> {
56if self.requests.len() >= self.limits.max_pending_requests {
57return None;
58 }
5960let Entry::Vacant(entry) = self.requests.entry(key) else {
61panic!("uuid collision");
62 };
6364let expirable = Expirable::new(value, Instant::now() + self.limits.response_timeout);
65let inserted = entry.insert(expirable);
66Some(&mut inserted.value)
67 }
6869/// Extracts a request from the map based on its identifier.
70 ///
71 /// Returns `None` if the value is not present or expired.
72pub(super) fn extract(&mut self, key: &Uuid) -> Result<T, &'static str> {
73let value =
74self.requests.swap_remove(key).ok_or("Received response for an unknown request")?;
75 value.value().ok_or("Dropping response for an expired request")
76 }
7778/// Removes all expired requests from the map.
79pub(super) fn remove_expired(&mut self) {
80self.requests.retain(|id, req| {
81let expired = req.expired();
82if expired {
83warn!(?id, "Dropping response for an expired request");
84 }
85 !expired
86 });
87 }
88}
8990struct Expirable<T> {
91 value: T,
92 expires_at: Instant,
93}
9495impl<T> Expirable<T> {
96fn new(value: T, expires_at: Instant) -> Self {
97Self { value, expires_at }
98 }
99100fn value(self) -> Option<T> {
101 (!self.expired()).then_some(self.value)
102 }
103104fn expired(&self) -> bool {
105 Instant::now() >= self.expires_at
106 }
107}
108109#[cfg(test)]
110mod tests {
111use std::time::Duration;
112113use uuid::Uuid;
114115use super::{PendingRequests, RequestLimits};
116117struct Dummy;
118119#[test]
120fn insertion_limits_for_pending_requests_work() {
121let mut pending: PendingRequests<Dummy> = PendingRequests::new(RequestLimits {
122 max_pending_requests: 1,
123 response_timeout: Duration::from_secs(10),
124 });
125126// First insert is ok.
127let first = Uuid::new_v4();
128assert!(pending.insert(first, Dummy).is_some());
129assert!(!pending.requests.is_empty());
130131// Second insert fails - limits is 1 pending request.
132let second = Uuid::new_v4();
133assert!(pending.insert(second, Dummy).is_none());
134135// First extract is ok.
136 // Second extract fails - it's not in a map.
137assert!(pending.extract(&first).is_ok());
138assert!(pending.extract(&second).is_err());
139140// After first is extracted, we have capacity for the second one.
141assert!(pending.insert(second, Dummy).is_some());
142// So extracting it should also work.
143assert!(pending.extract(&second).is_ok());
144// After extraction, we expect that the map is empty.
145assert!(pending.requests.is_empty());
146 }
147148#[test]
149fn time_limits_for_pending_requests_work() {
150let mut pending: PendingRequests<Dummy> = PendingRequests::new(RequestLimits {
151 max_pending_requests: 10,
152 response_timeout: Duration::from_secs(1),
153 });
154155// Insert a request, it's fine, limits are high.
156let key = Uuid::new_v4();
157assert!(pending.insert(key, Dummy).is_some());
158159// Wait for 2 seconds, the inserted request should lapse.
160std::thread::sleep(Duration::from_secs(2));
161assert!(pending.extract(&key).is_err());
162163// Insert 2 requests. Should be fine, limits are high.
164assert!(pending.insert(Uuid::new_v4(), Dummy).is_some());
165assert!(pending.insert(Uuid::new_v4(), Dummy).is_some());
166167// Wait for half a second, remove expired ones (none must be removed).
168 // Then, add another one (should also be fine, limits are high). So
169 // we should have 3 requests in a hash map.
170std::thread::sleep(Duration::from_millis(500));
171 pending.remove_expired();
172let key = Uuid::new_v4();
173assert!(pending.insert(key, Dummy).is_some());
174assert!(pending.requests.len() == 3);
175176// Wait for another half a second. First two requests should lapse.
177 // But the last one should still be in the map.
178std::thread::sleep(Duration::from_millis(500));
179 pending.remove_expired();
180assert!(pending.requests.len() == 1);
181assert!(pending.extract(&key).is_ok());
182assert!(pending.requests.is_empty());
183 }
184}