use djogi::context::DjogiContext;
use djogi::outbox::Publisher as _;
use djogi::outbox::publishers::NotifyPublisher;
use djogi::outbox::worker;
use djogi::pg::pool::DjogiPool;
use djogi::transaction::atomic;
use time::Duration;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL env var is required");
let outbox_tables: Vec<String> = std::env::var("OUTBOX_TABLES")
.unwrap_or_else(|_| "worker_outbox".to_string())
.split(',')
.map(|s| s.trim().to_string())
.collect();
let batch_size: u32 = std::env::var("OUTBOX_BATCH_SIZE")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(32);
let channel_prefix =
std::env::var("OUTBOX_CHANNEL_PREFIX").unwrap_or_else(|_| "djogi_outbox_".to_string());
let poll_ms: u64 = std::env::var("OUTBOX_POLL_MS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(500);
let pool = DjogiPool::connect(&database_url).await?;
let publishers: std::collections::HashMap<String, std::sync::Arc<NotifyPublisher>> =
outbox_tables
.iter()
.map(|t| {
let channel = format!("{channel_prefix}{t}");
let publisher = std::sync::Arc::new(NotifyPublisher::new(pool.clone(), channel));
(t.clone(), publisher)
})
.collect();
let lease_duration = Duration::minutes(5);
let stale_threshold = Duration::minutes(10);
loop {
for table in &outbox_tables {
let publisher_arc = std::sync::Arc::clone(
publishers
.get(table)
.expect("publisher built for every configured table"),
);
let table_owned = table.clone();
let mut ctx = DjogiContext::from_pool(pool.clone());
let result = atomic(&mut ctx, |ctx| {
let publisher_inner = std::sync::Arc::clone(&publisher_arc);
let tbl = table_owned.clone();
Box::pin(async move {
let rows = worker::claim_pending(ctx, &tbl, batch_size, lease_duration).await?;
for row in rows {
let row_id = row.id;
match publisher_inner.publish(&row).await {
Ok(()) => {
worker::mark_published(ctx, &tbl, row_id).await?;
}
Err(djogi::outbox::PublishError::Transient { message }) => {
worker::mark_failed(ctx, &tbl, row_id, &message, true).await?;
}
Err(djogi::outbox::PublishError::Permanent { message }) => {
worker::mark_failed(ctx, &tbl, row_id, &message, false).await?;
}
Err(e) => {
let msg = e.to_string();
worker::mark_failed(ctx, &tbl, row_id, &msg, true).await?;
}
}
}
Ok::<_, djogi::DjogiError>(())
})
})
.await;
if let Err(e) = result {
eprintln!("relay: error processing {table}: {e}");
}
let mut stale_ctx = DjogiContext::from_pool(pool.clone());
match worker::recover_stale(&mut stale_ctx, table, stale_threshold).await {
Ok(0) => {}
Ok(n) => eprintln!("relay: recovered {n} stale rows from {table}"),
Err(e) => eprintln!("relay: recover_stale error for {table}: {e}"),
}
}
tokio::time::sleep(std::time::Duration::from_millis(poll_ms)).await;
}
}