This commit is contained in:
@@ -18,33 +18,42 @@ use payload::DbEventPayload;
|
||||
|
||||
pub struct DbEventQueueConfig {
|
||||
pub poll_interval_ms: u64,
|
||||
pub batch_size: i64,
|
||||
pub max_attempts: i32,
|
||||
pub batch_size: i64,
|
||||
pub max_attempts: i32,
|
||||
}
|
||||
|
||||
impl DbEventQueueConfig {
|
||||
pub fn from_env() -> Self {
|
||||
Self {
|
||||
poll_interval_ms: std::env::var("EVENT_QUEUE_POLL_INTERVAL_MS")
|
||||
.ok().and_then(|v| v.parse().ok()).unwrap_or(500),
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(500),
|
||||
batch_size: std::env::var("EVENT_QUEUE_BATCH_SIZE")
|
||||
.ok().and_then(|v| v.parse().ok()).unwrap_or(10),
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(10),
|
||||
max_attempts: std::env::var("EVENT_QUEUE_MAX_ATTEMPTS")
|
||||
.ok().and_then(|v| v.parse().ok()).unwrap_or(5),
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(5),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PostgresEventQueue {
|
||||
pool: PgPool,
|
||||
pool: PgPool,
|
||||
config: Arc<DbEventQueueConfig>,
|
||||
}
|
||||
|
||||
impl PostgresEventQueue {
|
||||
pub async fn create(pool: PgPool, config: DbEventQueueConfig) -> anyhow::Result<Self> {
|
||||
migrations::run(&pool).await?;
|
||||
Ok(Self { pool, config: Arc::new(config) })
|
||||
Ok(Self {
|
||||
pool,
|
||||
config: Arc::new(config),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn create_publisher(pool: PgPool) -> anyhow::Result<Arc<dyn EventPublisher>> {
|
||||
@@ -68,14 +77,12 @@ impl EventPublisher for PostgresEventQueue {
|
||||
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 ($1, $2)"
|
||||
)
|
||||
.bind(event_type)
|
||||
.bind(payload_json)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::InfrastructureError(format!("insert event: {e}")))?;
|
||||
sqlx::query("INSERT INTO event_queue (event_type, payload) VALUES ($1, $2)")
|
||||
.bind(event_type)
|
||||
.bind(payload_json)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::InfrastructureError(format!("insert event: {e}")))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -83,10 +90,10 @@ impl EventPublisher for PostgresEventQueue {
|
||||
|
||||
impl EventConsumer for PostgresEventQueue {
|
||||
fn consume(&self) -> BoxStream<'_, Result<EventEnvelope, DomainError>> {
|
||||
let pool = self.pool.clone();
|
||||
let config = Arc::clone(&self.config);
|
||||
let (tx, rx) = mpsc::channel(128);
|
||||
let rx = Arc::new(Mutex::new(rx));
|
||||
let pool = self.pool.clone();
|
||||
let config = Arc::clone(&self.config);
|
||||
let (tx, rx) = mpsc::channel(128);
|
||||
let rx = Arc::new(Mutex::new(rx));
|
||||
|
||||
tokio::spawn(async move {
|
||||
let poll_interval = Duration::from_millis(config.poll_interval_ms);
|
||||
@@ -124,13 +131,13 @@ impl EventConsumer for PostgresEventQueue {
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
struct QueueRow {
|
||||
id: i64,
|
||||
payload: String,
|
||||
id: i64,
|
||||
payload: String,
|
||||
attempts: i32,
|
||||
}
|
||||
|
||||
async fn claim_batch(
|
||||
pool: &PgPool,
|
||||
pool: &PgPool,
|
||||
config: &DbEventQueueConfig,
|
||||
) -> Result<Vec<QueueRow>, DomainError> {
|
||||
// CTE with FOR UPDATE SKIP LOCKED — atomic and safe for multiple workers
|
||||
@@ -148,7 +155,7 @@ async fn claim_batch(
|
||||
FROM claimed
|
||||
WHERE q.id = claimed.id
|
||||
RETURNING q.id, q.payload, q.attempts
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(config.batch_size)
|
||||
.fetch_all(pool)
|
||||
@@ -159,25 +166,28 @@ async fn claim_batch(
|
||||
}
|
||||
|
||||
fn decode_row(
|
||||
pool: &PgPool,
|
||||
row: QueueRow,
|
||||
pool: &PgPool,
|
||||
row: QueueRow,
|
||||
max_attempts: i32,
|
||||
) -> Result<EventEnvelope, DomainError> {
|
||||
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)?;
|
||||
Ok(EventEnvelope::new(event, Box::new(DbAckHandle {
|
||||
pool: pool.clone(),
|
||||
row_id: row.id,
|
||||
attempts: row.attempts,
|
||||
max_attempts,
|
||||
})))
|
||||
Ok(EventEnvelope::new(
|
||||
event,
|
||||
Box::new(DbAckHandle {
|
||||
pool: pool.clone(),
|
||||
row_id: row.id,
|
||||
attempts: row.attempts,
|
||||
max_attempts,
|
||||
}),
|
||||
))
|
||||
}
|
||||
|
||||
struct DbAckHandle {
|
||||
pool: PgPool,
|
||||
row_id: i64,
|
||||
attempts: i32,
|
||||
pool: PgPool,
|
||||
row_id: i64,
|
||||
attempts: i32,
|
||||
max_attempts: i32,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user