links/store/
mod.rs

1//! This module contains all things relating to the way redirects, vanity
2//! paths, and statistics are stored in links. For details about configuring
3//! each store backend, see that backend's documentation.
4
5pub mod backend;
6mod memory;
7mod redis;
8
9#[cfg(test)]
10mod tests;
11
12use std::{collections::HashMap, sync::Arc};
13
14use anyhow::Result;
15use backend::StoreBackend;
16use links_id::Id;
17use links_normalized::{Link, Normalized};
18use parking_lot::RwLock;
19use serde::{Deserialize, Serialize};
20use strum::{Display as EnumDisplay, EnumString, IntoStaticStr};
21use tokio::spawn;
22use tracing::{debug, instrument, trace};
23
24pub use self::{memory::Store as Memory, redis::Store as Redis};
25use crate::stats::{Statistic, StatisticDescription, StatisticValue};
26
27/// The type of store backend used by the links redirector server. All variants
28/// must have a canonical human-readable string representation using only
29/// 'a'-'z', '0'-'9', and '_'.
30#[derive(
31	Copy,
32	Clone,
33	Debug,
34	Default,
35	PartialEq,
36	Eq,
37	Serialize,
38	Deserialize,
39	EnumString,
40	EnumDisplay,
41	IntoStaticStr,
42)]
43#[non_exhaustive]
44#[serde(rename_all = "snake_case")]
45#[strum(serialize_all = "snake_case")]
46pub enum BackendType {
47	/// A fully in-memory store backend, storing all data in RAM
48	/// with no other backups, but without any external dependencies. Not
49	/// recommended outside of tests.
50	#[default]
51	Memory,
52	/// A store backend which stores all data using a Redis 6.2+ server.
53	Redis,
54}
55
56impl BackendType {
57	/// Get the backend type's name as a string
58	#[must_use]
59	pub fn as_str(self) -> &'static str {
60		self.into()
61	}
62}
63
64/// A holder for a [`Store`], which allows the store to be updated on the fly.
65#[derive(Debug)]
66pub struct Current {
67	store: RwLock<Store>,
68}
69
70impl Current {
71	/// Create a new [`Current`]. The store passed into this function will be
72	/// returned by calls to `get`.
73	#[must_use]
74	pub const fn new(store: Store) -> Self {
75		Self {
76			store: RwLock::new(store),
77		}
78	}
79
80	/// Create a new static reference to a new [`Current`]. The store passed
81	/// into this function will be returned by calls to `get`.
82	///
83	/// # Memory
84	/// This function leaks memory. Make sure it is not called an unbounded
85	/// number of times.
86	#[must_use]
87	pub fn new_static(store: Store) -> &'static Self {
88		Box::leak(Box::new(Self::new(store)))
89	}
90
91	/// Get the current [`Store`]. The returned store itself will remain
92	/// unchanged even if the [`Current`] is updated.
93	pub fn get(&self) -> Store {
94		self.store.read().clone()
95	}
96
97	/// Update the store inside this [`Current`]. All future calls to `get` will
98	/// return this store instead, but all still-active stores will remain
99	/// unchanged.
100	pub fn update(&self, store: Store) {
101		*self.store.write() = store;
102	}
103
104	/// Get the current store's backend name. This is (slightly) more efficient
105	/// than `current.get().backend_name()`, because it doesn't need to update
106	/// the store's internal reference count.
107	pub fn backend_name(&self) -> &'static str {
108		self.store.read().backend_name()
109	}
110}
111
112/// A wrapper around any [`StoreBackend`], providing access to the underlying
113/// store along some with extra things like logging.
114#[derive(Debug, Clone)]
115pub struct Store {
116	store: Arc<dyn StoreBackend>,
117}
118
119impl Store {
120	/// Create a new instance of this `Store`. Configuration is
121	/// backend-specific and is provided as a `HashMap` from string keys to
122	/// string values, that are parsed by the backend as needed.
123	///
124	/// # Errors
125	/// This function returns an error if the store could not be initialized.
126	/// This may happen if the configuration is invalid or for other
127	/// backend-specific reasons (such as a file not being createable or a
128	/// network connection not being establishable, etc.).
129	#[instrument(level = "debug", ret, err)]
130	pub async fn new(store_type: BackendType, config: &HashMap<String, String>) -> Result<Self> {
131		match store_type {
132			BackendType::Memory => Ok(Self {
133				store: Arc::new(Memory::new(config).await?),
134			}),
135			BackendType::Redis => Ok(Self {
136				store: Arc::new(Redis::new(config).await?),
137			}),
138		}
139	}
140
141	/// Get the underlying implementation's name. The name (used in e.g. the
142	/// configuration) of the backend store implementing this trait must be a
143	/// human-readable name using only 'a'-'z', '0'-'9', and '_'.
144	#[must_use]
145	pub fn backend_name(&self) -> &'static str {
146		self.store.get_store_type().as_str()
147	}
148
149	/// Get a redirect. Returns the full `to` link corresponding to the `from`
150	/// links ID. A link not existing is not an error, if no matching link is
151	/// found, `Ok(None)` is returned.
152	///
153	/// # Error
154	/// An error is only returned if something actually fails; if we don't know
155	/// if a link exists or not, or what it is. A link not existing is not
156	/// considered an error.
157	#[instrument(level = "debug", skip(self), fields(name = self.backend_name()), ret, err)]
158	pub async fn get_redirect(&self, from: Id) -> Result<Option<Link>> {
159		self.store.get_redirect(from).await
160	}
161
162	/// Set a redirect. `from` is the ID of the link, while `to` is the full
163	/// destination link. If a mapping with this ID already exists, it must be
164	/// changed to the new one, returning the old one.
165	///
166	/// # Storage Guarantees
167	/// If an `Ok` is returned, the new value was definitely set / processed /
168	/// saved, and will be available on next request.
169	/// If an `Err` is returned, the value must not have been set / modified,
170	/// insofar as that is possible to determine from the backend.
171	#[instrument(level = "debug", skip(self), fields(name = self.backend_name()), ret, err)]
172	pub async fn set_redirect(&self, from: Id, to: Link) -> Result<Option<Link>> {
173		self.store.set_redirect(from, to).await
174	}
175
176	/// Remove a redirect. `from` is the ID of the links link to be removed.
177	/// Returns the old value of the mapping or `None` if there was no such
178	/// mapping.
179	///
180	/// # Storage Guarantees
181	/// If an `Ok` is returned, the new value was definitely removed /
182	/// processed / saved, and will be unavailable on next request.
183	/// If an `Err` is returned, the value must not have been removed /
184	/// modified, insofar as that is possible to determine from the backend.
185	#[instrument(level = "debug", skip(self), fields(name = self.backend_name()), ret, err)]
186	pub async fn rem_redirect(&self, from: Id) -> Result<Option<Link>> {
187		self.store.rem_redirect(from).await
188	}
189
190	/// Get a vanity path's ID. Returns the ID of the `to` link corresponding
191	/// to the `from` vanity path. An ID not existing is not an error, if no
192	/// matching ID is found, `None` is returned.
193	///
194	/// # Error
195	/// An error is only returned if something actually fails; if we don't know
196	/// if a link exists or not, or what it is. A link not existing is not
197	/// considered an error.
198	#[instrument(level = "debug", skip(self), fields(name = self.backend_name()), ret, err)]
199	pub async fn get_vanity(&self, from: Normalized) -> Result<Option<Id>> {
200		self.store.get_vanity(from).await
201	}
202
203	/// Set a vanity path for an ID. `from` is the vanity path of the links ID,
204	/// while `to` is the ID itself. If a vanity link with this path already
205	/// exists, it must be changed to the new one, returning the old one.
206	///
207	/// # Storage Guarantees
208	/// If an `Ok` is returned, the new value was definitely set / processed /
209	/// saved, and will be available on next request.
210	/// If an `Err` is returned, the value must not have been set / modified,
211	/// insofar as that is possible to determine from the backend.
212	#[instrument(level = "debug", skip(self), fields(name = self.backend_name()), ret, err)]
213	pub async fn set_vanity(&self, from: Normalized, to: Id) -> Result<Option<Id>> {
214		self.store.set_vanity(from, to).await
215	}
216
217	/// Remove a vanity path. `from` is the vanity path to be removed. Returns
218	/// the old value of the mapping or `None` if there was no such mapping.
219	///
220	/// # Storage Guarantees
221	/// If an `Ok` is returned, the new value was definitely removed /
222	/// processed / saved, and will be unavailable on next request.
223	/// If an `Err` is returned, the value must not have been removed /
224	/// modified, insofar as that is possible to determine from the backend.
225	#[instrument(level = "debug", skip(self), fields(name = self.backend_name()), ret, err)]
226	pub async fn rem_vanity(&self, from: Normalized) -> Result<Option<Id>> {
227		self.store.rem_vanity(from).await
228	}
229
230	/// Get statistics' values by their description. Returns all matching
231	/// [statistics][`Statistic`] and their values for the provided [statistic
232	/// description][`StatisticDescription`]. Statistics not having been
233	/// collected is not an error, if no matching statistics are found, an empty
234	/// iterator is returned.
235	///
236	/// # Error
237	/// An error is only returned if something fails when it should have worked.
238	/// A statistic not existing or the store not supporting statistics is not
239	/// considered an error.
240	#[instrument(level = "debug", skip(self), fields(name = self.backend_name()), ret, err)]
241	pub async fn get_statistics(
242		&self,
243		description: StatisticDescription,
244	) -> Result<impl Iterator<Item = (Statistic, StatisticValue)>> {
245		Ok(self.store.get_statistics(description).await?.into_iter())
246	}
247
248	/// Increment multiple statistics' count for the given id and/or vanity
249	/// path. Each of the provided [statistic][`Statistic`]s' values for the
250	/// provided [id][`Id`] and [vanity path][`Normalized`] are incremented by 1
251	/// in a spawned tokio task in the background.
252	///
253	/// # Error
254	/// This function failing in any way is not considered an error, because
255	/// statistics are done on a best-effort basis. However, any errors that
256	/// occur are logged.
257	pub fn incr_statistics<I>(&self, statistics: I)
258	where
259		I: IntoIterator<Item = Statistic> + Send + 'static,
260		<I as IntoIterator>::IntoIter: Send,
261	{
262		let store = self.store.clone();
263		spawn(async move {
264			for stat in statistics {
265				match store.incr_statistic(stat.clone()).await {
266					Ok(val) => trace!(?val, ?stat, "statistic incremented"),
267					Err(err) => debug!(?err, ?stat, "statistic incrementing failed"),
268				}
269			}
270		});
271	}
272
273	/// Remove statistics by their description. Deletes all
274	/// [statistics][`Statistic`] that match the provided
275	/// [description][`StatisticDescription`] and returns their values before
276	/// they were deleted, if they're available. A statistic not having been
277	/// collected is not an error, if no matching statistics are found, an empty
278	/// iterator is returned.
279	///
280	/// # Error
281	/// An error is only returned if something fails when it should have worked.
282	/// A statistic not existing or the store not supporting statistics is not
283	/// considered an error.
284	#[instrument(level = "debug", skip(self), fields(name = self.backend_name()), ret, err)]
285	pub async fn rem_statistics(
286		&self,
287		description: StatisticDescription,
288	) -> Result<impl Iterator<Item = (Statistic, StatisticValue)>> {
289		Ok(self.store.rem_statistics(description).await?.into_iter())
290	}
291}
292
293#[cfg(test)]
294mod store_tests {
295	use std::str::FromStr;
296
297	use super::*;
298
299	#[tokio::test]
300	async fn current() {
301		let id = Id::from([1, 2, 3, 4, 5]);
302		let link = Link::from_str("https://example.com").unwrap();
303		let store = Store::new("memory".parse().unwrap(), &HashMap::new())
304			.await
305			.unwrap();
306		store.set_redirect(id, link.clone()).await.unwrap();
307
308		let current = Current::new(store.clone());
309		let static_current = Current::new_static(store);
310
311		assert_eq!(
312			current.get().get_redirect(id).await.unwrap(),
313			Some(link.clone())
314		);
315		assert_eq!(
316			static_current.get().get_redirect(id).await.unwrap(),
317			Some(link)
318		);
319
320		let new_store = Store::new("memory".parse().unwrap(), &HashMap::new())
321			.await
322			.unwrap();
323		current.update(new_store.clone());
324		static_current.update(new_store.clone());
325
326		assert_eq!(current.get().get_redirect(id).await.unwrap(), None);
327		assert_eq!(static_current.get().get_redirect(id).await.unwrap(), None);
328	}
329
330	#[test]
331	fn type_to_from() {
332		assert_eq!(
333			BackendType::Memory,
334			BackendType::Memory.as_str().parse().unwrap()
335		);
336
337		assert_eq!(
338			BackendType::Redis,
339			BackendType::Redis.as_str().parse().unwrap()
340		);
341	}
342
343	#[tokio::test]
344	async fn cheap_clone() {
345		let store_a = Store::new("memory".parse().unwrap(), &HashMap::new())
346			.await
347			.unwrap();
348		let store_b = Store::new("memory".parse().unwrap(), &HashMap::new())
349			.await
350			.unwrap();
351		let store_c = store_a.clone();
352		let id = Id::from([0, 1, 2, 3, 4]);
353
354		store_a
355			.set_redirect(id, Link::new("https://example.com/test").unwrap())
356			.await
357			.unwrap();
358
359		assert_eq!(
360			store_a.get_redirect(id).await.unwrap(),
361			store_c.get_redirect(id).await.unwrap(),
362		);
363		assert_ne!(
364			store_a.get_redirect(id).await.unwrap(),
365			store_b.get_redirect(id).await.unwrap(),
366		);
367	}
368
369	#[tokio::test]
370	async fn new() {
371		Store::new("memory".parse().unwrap(), &HashMap::new())
372			.await
373			.unwrap();
374	}
375
376	#[tokio::test]
377	async fn backend_name() {
378		let store = Store::new("memory".parse().unwrap(), &HashMap::new())
379			.await
380			.unwrap();
381
382		let name = store.backend_name();
383
384		assert_eq!(name, "memory");
385	}
386
387	#[tokio::test]
388	async fn get_redirect() {
389		let store = Store::new("memory".parse().unwrap(), &HashMap::new())
390			.await
391			.unwrap();
392
393		let id = Id::from([0x10, 0x20, 0x30, 0x40, 0x50]);
394		let link = Link::new("https://example.com/test").unwrap();
395
396		store.set_redirect(id, link.clone()).await.unwrap();
397
398		assert_eq!(store.get_redirect(Id::new()).await.unwrap(), None);
399		assert_eq!(store.get_redirect(id).await.unwrap(), Some(link));
400	}
401
402	#[tokio::test]
403	async fn set_redirect() {
404		let store = Store::new("memory".parse().unwrap(), &HashMap::new())
405			.await
406			.unwrap();
407
408		let id = Id::from([0x11, 0x21, 0x31, 0x41, 0x51]);
409		let link = Link::new("https://example.com/test").unwrap();
410
411		store.set_redirect(id, link.clone()).await.unwrap();
412
413		assert_eq!(store.get_redirect(id).await.unwrap(), Some(link));
414	}
415
416	#[tokio::test]
417	async fn rem_redirect() {
418		let store = Store::new("memory".parse().unwrap(), &HashMap::new())
419			.await
420			.unwrap();
421
422		let id = Id::from([0x12, 0x22, 0x32, 0x42, 0x52]);
423		let link = Link::new("https://example.com/test").unwrap();
424
425		store.set_redirect(id, link.clone()).await.unwrap();
426
427		assert_eq!(store.get_redirect(id).await.unwrap(), Some(link.clone()));
428		store.rem_redirect(id).await.unwrap();
429		assert_eq!(store.get_redirect(id).await.unwrap(), None);
430	}
431
432	#[tokio::test]
433	async fn get_vanity() {
434		let store = Store::new("memory".parse().unwrap(), &HashMap::new())
435			.await
436			.unwrap();
437
438		let vanity = Normalized::new("Example Test");
439		let id = Id::from([0x13, 0x23, 0x33, 0x43, 0x53]);
440
441		store.set_vanity(vanity.clone(), id).await.unwrap();
442
443		assert_eq!(
444			store
445				.get_vanity(Normalized::new("Doesn't exist."))
446				.await
447				.unwrap(),
448			None
449		);
450		assert_eq!(store.get_vanity(vanity.clone()).await.unwrap(), Some(id));
451	}
452
453	#[tokio::test]
454	async fn set_vanity() {
455		let store = Store::new("memory".parse().unwrap(), &HashMap::new())
456			.await
457			.unwrap();
458
459		let vanity = Normalized::new("Example Test");
460		let id = Id::from([0x13, 0x23, 0x33, 0x43, 0x53]);
461
462		store.set_vanity(vanity.clone(), id).await.unwrap();
463
464		assert_eq!(store.get_vanity(vanity.clone()).await.unwrap(), Some(id));
465	}
466
467	#[tokio::test]
468	async fn rem_vanity() {
469		let store = Store::new("memory".parse().unwrap(), &HashMap::new())
470			.await
471			.unwrap();
472
473		let vanity = Normalized::new("Example Test");
474		let id = Id::from([0x13, 0x23, 0x33, 0x43, 0x53]);
475
476		store.set_vanity(vanity.clone(), id).await.unwrap();
477
478		assert_eq!(store.get_vanity(vanity.clone()).await.unwrap(), Some(id));
479		store.rem_vanity(vanity.clone()).await.unwrap();
480		assert_eq!(store.get_vanity(vanity.clone()).await.unwrap(), None);
481	}
482}