Skip to main content

reinhardt_tasks/backends/
metadata_store.rs

1//! Pluggable metadata store for task backends
2//!
3//! This module provides a trait for external task metadata storage,
4//! enabling backends like RabbitMQ to track task status and data
5//! independently from the message queue.
6//!
7//! # Design Rationale
8//!
9//! Message queues like RabbitMQ and SQS are designed for message delivery,
10//! not data storage. To support features like status tracking and task
11//! data retrieval, we need a separate storage layer.
12//!
13//! # Available Implementations
14//!
15//! - [`InMemoryMetadataStore`]: Default, non-persistent implementation
16//!
17//! # Examples
18//!
19//! ```
20//! use reinhardt_tasks::backends::metadata_store::{InMemoryMetadataStore, MetadataStore, TaskMetadata};
21//! use reinhardt_tasks::{TaskId, TaskStatus};
22//!
23//! # async fn example() {
24//! let store = InMemoryMetadataStore::new();
25//!
26//! let task_id = TaskId::new();
27//! let metadata = TaskMetadata::new(task_id, "my_task".to_string());
28//!
29//! // Store metadata
30//! store.store(metadata.clone()).await.unwrap();
31//!
32//! // Retrieve metadata
33//! let retrieved = store.get(task_id).await.unwrap();
34//! assert!(retrieved.is_some());
35//!
36//! // Update status
37//! store.update_status(task_id, TaskStatus::Running).await.unwrap();
38//! # }
39//! ```
40
41use crate::{TaskId, TaskStatus, registry::SerializedTask};
42use async_trait::async_trait;
43use serde::{Deserialize, Serialize};
44use std::collections::HashMap;
45use std::sync::Arc;
46use tokio::sync::RwLock;
47
48/// Error type for metadata store operations
49#[derive(Debug, Clone)]
50pub enum MetadataStoreError {
51	/// Task not found in store
52	NotFound(TaskId),
53	/// Storage operation failed
54	StorageError(String),
55	/// Serialization/deserialization error
56	SerializationError(String),
57}
58
59impl std::fmt::Display for MetadataStoreError {
60	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61		match self {
62			MetadataStoreError::NotFound(id) => {
63				write!(f, "Task {} not found in metadata store", id)
64			}
65			MetadataStoreError::StorageError(msg) => write!(f, "Metadata storage error: {}", msg),
66			MetadataStoreError::SerializationError(msg) => {
67				write!(f, "Metadata serialization error: {}", msg)
68			}
69		}
70	}
71}
72
73impl std::error::Error for MetadataStoreError {}
74
75/// Task metadata for external storage
76///
77/// Contains all information needed to track task status and retrieve task data.
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct TaskMetadata {
80	/// Unique task identifier
81	pub id: TaskId,
82	/// Task name (e.g., "send_email", "process_image")
83	pub name: String,
84	/// Current task status
85	pub status: TaskStatus,
86	/// Unix timestamp when task was created
87	pub created_at: i64,
88	/// Unix timestamp when task was last updated
89	pub updated_at: i64,
90	/// Optional serialized task data
91	pub task_data: Option<SerializedTask>,
92}
93
94impl TaskMetadata {
95	/// Create new task metadata with Pending status
96	pub fn new(id: TaskId, name: String) -> Self {
97		let now = chrono::Utc::now().timestamp();
98		Self {
99			id,
100			name,
101			status: TaskStatus::Pending,
102			created_at: now,
103			updated_at: now,
104			task_data: None,
105		}
106	}
107
108	/// Create new task metadata with serialized task data
109	pub fn with_task_data(id: TaskId, name: String, task_data: SerializedTask) -> Self {
110		let now = chrono::Utc::now().timestamp();
111		Self {
112			id,
113			name,
114			status: TaskStatus::Pending,
115			created_at: now,
116			updated_at: now,
117			task_data: Some(task_data),
118		}
119	}
120
121	/// Update the status and timestamp
122	pub fn set_status(&mut self, status: TaskStatus) {
123		self.status = status;
124		self.updated_at = chrono::Utc::now().timestamp();
125	}
126}
127
128/// Trait for pluggable metadata storage backends
129///
130/// Implement this trait to provide custom storage for task metadata.
131/// Common implementations include Redis, PostgreSQL, and DynamoDB.
132///
133/// # Thread Safety
134///
135/// Implementations must be `Send + Sync` to support concurrent access
136/// from multiple async tasks.
137///
138/// # Examples
139///
140/// ```
141/// use reinhardt_tasks::backends::metadata_store::{MetadataStore, MetadataStoreError, TaskMetadata};
142/// use reinhardt_tasks::{TaskId, TaskStatus};
143/// use async_trait::async_trait;
144///
145/// struct MyCustomStore { /* ... */ }
146///
147/// #[async_trait]
148/// impl MetadataStore for MyCustomStore {
149///     async fn store(&self, metadata: TaskMetadata) -> Result<(), MetadataStoreError> {
150///         // Store in database
151///         Ok(())
152///     }
153///
154///     async fn get(&self, task_id: TaskId) -> Result<Option<TaskMetadata>, MetadataStoreError> {
155///         // Retrieve from database
156///         Ok(None)
157///     }
158///
159///     async fn update_status(&self, task_id: TaskId, status: TaskStatus) -> Result<(), MetadataStoreError> {
160///         // Update status in database
161///         Ok(())
162///     }
163///
164///     async fn delete(&self, task_id: TaskId) -> Result<(), MetadataStoreError> {
165///         // Delete from database
166///         Ok(())
167///     }
168/// }
169/// ```
170#[async_trait]
171pub trait MetadataStore: Send + Sync {
172	/// Store task metadata
173	async fn store(&self, metadata: TaskMetadata) -> Result<(), MetadataStoreError>;
174
175	/// Retrieve task metadata by ID
176	async fn get(&self, task_id: TaskId) -> Result<Option<TaskMetadata>, MetadataStoreError>;
177
178	/// Update task status
179	async fn update_status(
180		&self,
181		task_id: TaskId,
182		status: TaskStatus,
183	) -> Result<(), MetadataStoreError>;
184
185	/// Delete task metadata
186	async fn delete(&self, task_id: TaskId) -> Result<(), MetadataStoreError>;
187}
188
189/// In-memory implementation of MetadataStore
190///
191/// This is the default implementation for development and testing.
192/// Data is stored in memory and will be lost when the process exits.
193///
194/// # Thread Safety
195///
196/// Uses `RwLock` for concurrent read/write access.
197///
198/// # Examples
199///
200/// ```
201/// use reinhardt_tasks::backends::metadata_store::{InMemoryMetadataStore, MetadataStore, TaskMetadata};
202/// use reinhardt_tasks::TaskId;
203///
204/// # async fn example() {
205/// let store = InMemoryMetadataStore::new();
206///
207/// let task_id = TaskId::new();
208/// let metadata = TaskMetadata::new(task_id, "test_task".to_string());
209///
210/// store.store(metadata).await.unwrap();
211/// # }
212/// ```
213pub struct InMemoryMetadataStore {
214	store: Arc<RwLock<HashMap<TaskId, TaskMetadata>>>,
215}
216
217impl InMemoryMetadataStore {
218	/// Create a new in-memory metadata store
219	pub fn new() -> Self {
220		Self {
221			store: Arc::new(RwLock::new(HashMap::new())),
222		}
223	}
224
225	/// Get the number of entries in the store
226	pub async fn len(&self) -> usize {
227		self.store.read().await.len()
228	}
229
230	/// Check if the store is empty
231	pub async fn is_empty(&self) -> bool {
232		self.store.read().await.is_empty()
233	}
234
235	/// Clear all entries from the store
236	pub async fn clear(&self) {
237		self.store.write().await.clear();
238	}
239}
240
241impl Default for InMemoryMetadataStore {
242	fn default() -> Self {
243		Self::new()
244	}
245}
246
247impl Clone for InMemoryMetadataStore {
248	fn clone(&self) -> Self {
249		Self {
250			store: Arc::clone(&self.store),
251		}
252	}
253}
254
255#[async_trait]
256impl MetadataStore for InMemoryMetadataStore {
257	async fn store(&self, metadata: TaskMetadata) -> Result<(), MetadataStoreError> {
258		let mut store = self.store.write().await;
259		store.insert(metadata.id, metadata);
260		Ok(())
261	}
262
263	async fn get(&self, task_id: TaskId) -> Result<Option<TaskMetadata>, MetadataStoreError> {
264		let store = self.store.read().await;
265		Ok(store.get(&task_id).cloned())
266	}
267
268	async fn update_status(
269		&self,
270		task_id: TaskId,
271		status: TaskStatus,
272	) -> Result<(), MetadataStoreError> {
273		let mut store = self.store.write().await;
274
275		if let Some(metadata) = store.get_mut(&task_id) {
276			metadata.set_status(status);
277			Ok(())
278		} else {
279			Err(MetadataStoreError::NotFound(task_id))
280		}
281	}
282
283	async fn delete(&self, task_id: TaskId) -> Result<(), MetadataStoreError> {
284		let mut store = self.store.write().await;
285		store.remove(&task_id);
286		Ok(())
287	}
288}
289
290#[cfg(test)]
291mod tests {
292	use super::*;
293
294	#[tokio::test]
295	async fn test_in_memory_store_basic_operations() {
296		let store = InMemoryMetadataStore::new();
297
298		let task_id = TaskId::new();
299		let metadata = TaskMetadata::new(task_id, "test_task".to_string());
300
301		// Store
302		store.store(metadata.clone()).await.unwrap();
303		assert_eq!(store.len().await, 1);
304
305		// Get
306		let retrieved = store.get(task_id).await.unwrap().unwrap();
307		assert_eq!(retrieved.id, task_id);
308		assert_eq!(retrieved.name, "test_task");
309		assert_eq!(retrieved.status, TaskStatus::Pending);
310
311		// Update status
312		store
313			.update_status(task_id, TaskStatus::Running)
314			.await
315			.unwrap();
316		let updated = store.get(task_id).await.unwrap().unwrap();
317		assert_eq!(updated.status, TaskStatus::Running);
318
319		// Delete
320		store.delete(task_id).await.unwrap();
321		assert!(store.get(task_id).await.unwrap().is_none());
322	}
323
324	#[tokio::test]
325	async fn test_in_memory_store_update_nonexistent() {
326		let store = InMemoryMetadataStore::new();
327		let task_id = TaskId::new();
328
329		let result = store.update_status(task_id, TaskStatus::Running).await;
330		assert!(matches!(result, Err(MetadataStoreError::NotFound(_))));
331	}
332
333	#[tokio::test]
334	async fn test_in_memory_store_clear() {
335		let store = InMemoryMetadataStore::new();
336
337		for _ in 0..10 {
338			let metadata = TaskMetadata::new(TaskId::new(), "test".to_string());
339			store.store(metadata).await.unwrap();
340		}
341
342		assert_eq!(store.len().await, 10);
343		store.clear().await;
344		assert!(store.is_empty().await);
345	}
346
347	#[tokio::test]
348	async fn test_task_metadata_with_task_data() {
349		let task_id = TaskId::new();
350		let task_data =
351			SerializedTask::new("test_task".to_string(), r#"{"key": "value"}"#.to_string());
352		let metadata = TaskMetadata::with_task_data(task_id, "test_task".to_string(), task_data);
353
354		assert!(metadata.task_data.is_some());
355		assert_eq!(metadata.task_data.as_ref().unwrap().name(), "test_task");
356	}
357
358	#[test]
359	fn test_metadata_store_error_display() {
360		let task_id = TaskId::new();
361		let not_found = MetadataStoreError::NotFound(task_id);
362		assert!(not_found.to_string().contains("not found"));
363
364		let storage_error = MetadataStoreError::StorageError("connection failed".to_string());
365		assert!(storage_error.to_string().contains("connection failed"));
366	}
367}