feat: event ids, outbox identity migration
This commit is contained in:
@@ -6,14 +6,15 @@ use std::time::Duration;
|
||||
use async_trait::async_trait;
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
events::{AckHandle, DomainEvent, EventEnvelope},
|
||||
events::{AckHandle, DomainEvent, EventEnvelope, EventId},
|
||||
ports::{EventConsumer, EventPublisher},
|
||||
};
|
||||
use futures::stream::{self, BoxStream};
|
||||
use sqlx::SqlitePool;
|
||||
use tokio::sync::{Mutex, mpsc};
|
||||
|
||||
use payload::DbEventPayload;
|
||||
use crate::shared::enqueue_events;
|
||||
pub(crate) use payload::DbEventPayload;
|
||||
|
||||
pub struct DbEventQueueConfig {
|
||||
pub poll_interval_ms: u64,
|
||||
@@ -65,17 +66,15 @@ impl SqliteEventQueue {
|
||||
#[async_trait]
|
||||
impl EventPublisher for SqliteEventQueue {
|
||||
async fn publish(&self, event: &DomainEvent) -> Result<(), DomainError> {
|
||||
let db_payload = DbEventPayload::from(event);
|
||||
let event_type = db_payload.event_type();
|
||||
let payload_json = serde_json::to_string(&db_payload)
|
||||
.map_err(|e| DomainError::InfrastructureError(format!("serialize: {e}")))?;
|
||||
|
||||
sqlx::query("INSERT INTO event_queue (event_type, payload) VALUES (?, ?)")
|
||||
.bind(event_type)
|
||||
.bind(payload_json)
|
||||
.execute(&self.pool)
|
||||
let mut tx = self
|
||||
.pool
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|e| DomainError::InfrastructureError(format!("insert event: {e}")))?;
|
||||
.map_err(|e| DomainError::InfrastructureError(format!("begin: {e}")))?;
|
||||
enqueue_events(&mut tx, std::slice::from_ref(event)).await?;
|
||||
tx.commit()
|
||||
.await
|
||||
.map_err(|e| DomainError::InfrastructureError(format!("commit: {e}")))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -126,6 +125,7 @@ impl EventConsumer for SqliteEventQueue {
|
||||
struct QueueRow {
|
||||
id: i64,
|
||||
payload: String,
|
||||
event_id: Option<String>,
|
||||
attempts: i32,
|
||||
next_attempt_at: String,
|
||||
}
|
||||
@@ -146,7 +146,7 @@ async fn claim_batch(
|
||||
ORDER BY next_attempt_at ASC
|
||||
LIMIT ?
|
||||
)
|
||||
RETURNING id, payload, attempts, next_attempt_at",
|
||||
RETURNING id, payload, event_id, attempts, next_attempt_at",
|
||||
)
|
||||
.bind(config.batch_size)
|
||||
.fetch_all(pool)
|
||||
@@ -166,7 +166,14 @@ fn decode_row(
|
||||
let db_payload: DbEventPayload = serde_json::from_str(&row.payload)
|
||||
.map_err(|e| DomainError::InfrastructureError(format!("deserialize: {e}")))?;
|
||||
let event = DomainEvent::try_from(db_payload)?;
|
||||
let id = row
|
||||
.event_id
|
||||
.as_deref()
|
||||
.map(crate::shared::parse_uuid)
|
||||
.transpose()?
|
||||
.map(EventId::from_uuid);
|
||||
Ok(EventEnvelope::new(
|
||||
id,
|
||||
event,
|
||||
Box::new(DbAckHandle {
|
||||
pool: pool.clone(),
|
||||
|
||||
@@ -109,3 +109,84 @@ async fn claimed_rows_are_marked_processing_and_not_claimed_twice() {
|
||||
let payloads: Vec<&str> = first.iter().map(|r| r.payload.as_str()).collect();
|
||||
assert_eq!(payloads, ["p0", "p1", "p2"], "oldest first");
|
||||
}
|
||||
|
||||
mod identity {
|
||||
use domain::{
|
||||
events::DomainEvent,
|
||||
ports::{EventConsumer, EventPublisher},
|
||||
value_objects::UserId,
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use super::{DbEventQueueConfig, config};
|
||||
use crate::event_queue::SqliteEventQueue;
|
||||
|
||||
async fn queue() -> (SqliteEventQueue, SqlitePool) {
|
||||
let pool = sqlx::sqlite::SqlitePoolOptions::new()
|
||||
.max_connections(1)
|
||||
.connect("sqlite::memory:")
|
||||
.await
|
||||
.unwrap();
|
||||
crate::migrate(&pool).await.unwrap();
|
||||
let cfg: DbEventQueueConfig = config(10);
|
||||
(SqliteEventQueue::create(pool.clone(), cfg), pool)
|
||||
}
|
||||
|
||||
fn event() -> DomainEvent {
|
||||
DomainEvent::UserUpdated {
|
||||
user_id: UserId::generate(),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn published_events_get_a_unique_event_id() {
|
||||
let (q, pool) = queue().await;
|
||||
|
||||
q.publish(&event()).await.unwrap();
|
||||
q.publish(&event()).await.unwrap();
|
||||
|
||||
let ids: Vec<Option<String>> =
|
||||
sqlx::query_scalar("SELECT event_id FROM event_queue ORDER BY id")
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(ids.len(), 2);
|
||||
let a = ids[0].as_deref().expect("first event has an id");
|
||||
let b = ids[1].as_deref().expect("second event has an id");
|
||||
assert_ne!(a, b);
|
||||
uuid::Uuid::parse_str(a).expect("event id is a uuid");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn the_consumer_surfaces_the_published_event_id() {
|
||||
let (q, pool) = queue().await;
|
||||
q.publish(&event()).await.unwrap();
|
||||
let stored: String = sqlx::query_scalar("SELECT event_id FROM event_queue")
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let envelope = q.consume().next().await.unwrap().unwrap();
|
||||
|
||||
assert_eq!(envelope.id().unwrap().value().to_string(), stored);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn legacy_queue_rows_without_event_id_still_decode() {
|
||||
let (q, pool) = queue().await;
|
||||
let payload =
|
||||
serde_json::to_string(&crate::event_queue::DbEventPayload::from(&event())).unwrap();
|
||||
sqlx::query(
|
||||
"INSERT INTO event_queue (event_type, payload, event_id) VALUES ('UserUpdated', ?, NULL)",
|
||||
)
|
||||
.bind(payload)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let envelope = q.consume().next().await.unwrap().unwrap();
|
||||
|
||||
assert!(envelope.id().is_none());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user