1use std::future::Future;
8
9use uuid::Uuid;
10
11pub const UNIQUE_UUID_MAX_ATTEMPTS: usize = 5;
13
14pub enum UniqueUuidAttempt<T> {
16 Accepted(T),
18 Collision,
20}
21
22pub fn new_uuid() -> String {
24 Uuid::new_v4().to_string()
25}
26
27fn unique_uuid_exhausted_message(value_name: &str) -> String {
28 format!("failed to create unique {value_name} UUID after {UNIQUE_UUID_MAX_ATTEMPTS} attempts")
29}
30
31pub async fn with_unique_uuid<F, Fut, T, E>(
38 value_name: &str,
39 mut try_candidate: F,
40) -> std::result::Result<T, E>
41where
42 F: FnMut(Uuid) -> Fut,
43 Fut: Future<Output = std::result::Result<UniqueUuidAttempt<T>, E>>,
44 E: From<crate::UtilsError>,
45{
46 for attempt in 1..=UNIQUE_UUID_MAX_ATTEMPTS {
47 let candidate = Uuid::new_v4();
48 match try_candidate(candidate).await? {
49 UniqueUuidAttempt::Accepted(value) => return Ok(value),
50 UniqueUuidAttempt::Collision => {
51 tracing::warn!(
52 value_name,
53 attempt,
54 candidate = %candidate,
55 "uuid collision, retrying"
56 );
57 }
58 }
59 }
60
61 Err(crate::UtilsError::invalid_value(unique_uuid_exhausted_message(value_name)).into())
62}
63
64pub async fn new_best_effort_uuid<F, Fut, E>(
70 value_name: &str,
71 mut is_taken: F,
72) -> std::result::Result<Uuid, E>
73where
74 F: FnMut(Uuid) -> Fut,
75 Fut: Future<Output = std::result::Result<bool, E>>,
76 E: From<crate::UtilsError>,
77{
78 with_unique_uuid(value_name, |candidate| {
79 let taken = is_taken(candidate);
80 async move {
81 if taken.await? {
82 Ok(UniqueUuidAttempt::Collision)
83 } else {
84 Ok(UniqueUuidAttempt::Accepted(candidate))
85 }
86 }
87 })
88 .await
89}
90
91pub fn new_short_token() -> String {
93 Uuid::new_v4().simple().to_string()
94}
95
96pub fn new_runtime_id() -> String {
102 format!("runtime-{}", new_short_token())
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use crate::{Result, UtilsError};
109 use std::sync::{
110 Arc,
111 atomic::{AtomicUsize, Ordering},
112 };
113
114 #[tokio::test]
115 async fn new_best_effort_uuid_returns_first_free_candidate() {
116 let attempts = Arc::new(AtomicUsize::new(0));
117 let result: Result<Uuid> = new_best_effort_uuid("test value", {
118 let attempts = Arc::clone(&attempts);
119 move |_| {
120 let attempts = Arc::clone(&attempts);
121 async move {
122 attempts.fetch_add(1, Ordering::SeqCst);
123 Ok(false)
124 }
125 }
126 })
127 .await;
128
129 assert!(result.is_ok());
130 assert_eq!(attempts.load(Ordering::SeqCst), 1);
131 }
132
133 #[tokio::test]
134 async fn with_unique_uuid_retries_collisions_and_returns_callback_value() {
135 let attempts = Arc::new(AtomicUsize::new(0));
136 let result: Result<String> = with_unique_uuid("test value", {
137 let attempts = Arc::clone(&attempts);
138 move |candidate| {
139 let attempts = Arc::clone(&attempts);
140 async move {
141 let attempt = attempts.fetch_add(1, Ordering::SeqCst);
142 if attempt < 2 {
143 Ok(UniqueUuidAttempt::Collision)
144 } else {
145 Ok(UniqueUuidAttempt::Accepted(candidate.to_string()))
146 }
147 }
148 }
149 })
150 .await;
151 let result = result.expect("third candidate should be accepted");
152
153 assert!(Uuid::parse_str(&result).is_ok());
154 assert_eq!(attempts.load(Ordering::SeqCst), 3);
155 }
156
157 #[tokio::test]
158 async fn with_unique_uuid_propagates_callback_errors_without_retrying() {
159 let attempts = Arc::new(AtomicUsize::new(0));
160 let result: Result<String> = with_unique_uuid("test value", {
161 let attempts = Arc::clone(&attempts);
162 move |_| {
163 let attempts = Arc::clone(&attempts);
164 async move {
165 attempts.fetch_add(1, Ordering::SeqCst);
166 Err(UtilsError::invalid_value("candidate check failed"))
167 }
168 }
169 })
170 .await;
171
172 let error = result.expect_err("callback error should be returned");
173 assert!(matches!(error, UtilsError::InvalidValue(_)));
174 assert_eq!(attempts.load(Ordering::SeqCst), 1);
175 }
176
177 #[derive(Debug, PartialEq, Eq)]
178 enum CustomUuidError {
179 Callback,
180 Exhausted(String),
181 }
182
183 impl From<UtilsError> for CustomUuidError {
184 fn from(error: UtilsError) -> Self {
185 Self::Exhausted(error.to_string())
186 }
187 }
188
189 #[tokio::test]
190 async fn with_unique_uuid_supports_caller_error_type() {
191 let attempts = Arc::new(AtomicUsize::new(0));
192 let result: std::result::Result<String, CustomUuidError> =
193 with_unique_uuid("custom value", {
194 let attempts = Arc::clone(&attempts);
195 move |_| {
196 let attempts = Arc::clone(&attempts);
197 async move {
198 attempts.fetch_add(1, Ordering::SeqCst);
199 Err(CustomUuidError::Callback)
200 }
201 }
202 })
203 .await;
204
205 assert_eq!(result, Err(CustomUuidError::Callback));
206 assert_eq!(attempts.load(Ordering::SeqCst), 1);
207 }
208
209 #[tokio::test]
210 async fn with_unique_uuid_uses_caller_error_for_exhaustion() {
211 let attempts = Arc::new(AtomicUsize::new(0));
212 let result: std::result::Result<String, CustomUuidError> =
213 with_unique_uuid("custom value", {
214 let attempts = Arc::clone(&attempts);
215 move |_| {
216 let attempts = Arc::clone(&attempts);
217 async move {
218 attempts.fetch_add(1, Ordering::SeqCst);
219 Ok(UniqueUuidAttempt::Collision)
220 }
221 }
222 })
223 .await;
224
225 assert_eq!(
226 result,
227 Err(CustomUuidError::Exhausted(
228 "failed to create unique custom value UUID after 5 attempts".to_string()
229 ))
230 );
231 assert_eq!(attempts.load(Ordering::SeqCst), UNIQUE_UUID_MAX_ATTEMPTS);
232 }
233
234 #[tokio::test]
235 async fn new_best_effort_uuid_retries_taken_candidates() {
236 let attempts = Arc::new(AtomicUsize::new(0));
237 let result: Result<Uuid> = new_best_effort_uuid("test value", {
238 let attempts = Arc::clone(&attempts);
239 move |_| {
240 let attempts = Arc::clone(&attempts);
241 async move {
242 let attempt = attempts.fetch_add(1, Ordering::SeqCst);
243 Ok(attempt < 2)
244 }
245 }
246 })
247 .await;
248
249 assert!(result.is_ok());
250 assert_eq!(attempts.load(Ordering::SeqCst), 3);
251 }
252
253 #[tokio::test]
254 async fn new_best_effort_uuid_stops_after_retry_budget_is_exhausted() {
255 let attempts = Arc::new(AtomicUsize::new(0));
256 let result: Result<Uuid> = new_best_effort_uuid("test value", {
257 let attempts = Arc::clone(&attempts);
258 move |_| {
259 let attempts = Arc::clone(&attempts);
260 async move {
261 attempts.fetch_add(1, Ordering::SeqCst);
262 Ok(true)
263 }
264 }
265 })
266 .await;
267
268 let error = result.expect_err("all candidates were reported as taken");
269 assert!(matches!(error, UtilsError::InvalidValue(_)));
270 assert_eq!(attempts.load(Ordering::SeqCst), UNIQUE_UUID_MAX_ATTEMPTS);
271 }
272
273 #[test]
274 fn new_short_token_uses_uuid_v4_hex_entropy() {
275 let token = new_short_token();
276
277 assert_eq!(token.len(), 32);
278 assert!(!token.contains('-'));
279 assert!(Uuid::parse_str(&token).is_ok());
280 }
281
282 #[test]
283 fn new_runtime_id_uses_runtime_prefix_and_uuid_entropy() {
284 let runtime_id = new_runtime_id();
285 let token = runtime_id
286 .strip_prefix("runtime-")
287 .expect("runtime id should use runtime prefix");
288
289 assert_eq!(token.len(), 32);
290 assert!(Uuid::parse_str(token).is_ok());
291 }
292}