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