feat: processed-event ledger, idempotent dispatch

This commit is contained in:
2026-10-26 00:17:28 +01:00
parent fa8e94817a
commit 5681d0c034
29 changed files with 610 additions and 24 deletions

View File

@@ -38,6 +38,7 @@ pub struct DatabaseOutput {
pub federation_settings: std::sync::Arc<dyn domain::ports::UserFederationSettingsQuery>,
pub remote_goal: Arc<dyn domain::ports::RemoteGoalRepository>,
pub refresh_session: Arc<dyn RefreshSessionRepository>,
pub processed_events: Arc<dyn domain::ports::ProcessedEventLedger>,
pub deduplicator: Arc<dyn MovieDeduplicator>,
pub image_ref_command: Arc<dyn ImageRefCommand>,
pub image_ref_query: Arc<dyn ImageRefQuery>,
@@ -114,6 +115,7 @@ fn assemble(
federation_settings: w.federation_settings,
remote_goal: w.remote_goal,
refresh_session: Arc::new(sqlite::SqliteRefreshSessionAdapter::new(w.pool.clone())) as _,
processed_events: Arc::new(sqlite::SqliteProcessedEventLedger::new(w.pool.clone())) as _,
deduplicator: w.deduplicator,
image_ref_command,
image_ref_query,

View File

@@ -14,6 +14,10 @@ pub struct FollowBackfillHandler {
#[async_trait]
impl EventHandler for FollowBackfillHandler {
fn name(&self) -> &'static str {
"follow_backfill"
}
async fn handle(&self, event: &DomainEvent) -> Result<(), DomainError> {
match event {
DomainEvent::FollowAccepted {

View File

@@ -18,6 +18,11 @@ pub struct WorkerApp {
}
impl WorkerApp {
/// Names of the registered event handlers, in registration order.
pub fn handler_names(&self) -> Vec<&'static str> {
self.service.handler_names()
}
/// Runs the handlers and, when there is one, the outbox relay, until
/// `shutdown` fires. Both stop together and finish their in-flight work.
///
@@ -142,6 +147,7 @@ async fn assemble(
let search_command = db.search_command;
let wrapup_repo = db.wrapup_repo;
let refresh_session = db.refresh_session;
let processed_events = db.processed_events;
let event_publisher = event_publisher_arc;
let metadata = metadata_client;
@@ -217,6 +223,9 @@ async fn assemble(
Arc::new(application::jobs::RefreshSessionCleanupJob::new(
Arc::clone(&refresh_session),
)),
Arc::new(application::jobs::ProcessedEventsCleanupJob::new(
Arc::clone(&processed_events),
)),
];
if let Some(job) = enrichment_job {
periodic_jobs.push(job);
@@ -301,7 +310,7 @@ async fn assemble(
};
Ok(WorkerApp {
service: WorkerService::new(consumer_arc, handlers),
service: WorkerService::new(consumer_arc, handlers, processed_events)?,
relay,
})
}

View File

@@ -107,3 +107,34 @@ async fn server_publishes_to_the_outbox_even_when_bus_is_nats() {
.unwrap();
assert_eq!(queued, 1);
}
/// Handler names key the processed-event ledger, so each must be unique. The
/// check runs over the real wiring, in both federation builds.
#[tokio::test]
async fn handler_names_are_unique() {
let pool = support::memory_pool().await;
let worker = bootstrap::build_worker_on(&support::test_config(), pool)
.await
.expect("worker builds");
let names = worker.handler_names();
let unique: std::collections::HashSet<_> = names.iter().collect();
assert_eq!(
unique.len(),
names.len(),
"duplicate handler name in {names:?}"
);
for core in [
"poster_sync",
"image_cleanup",
"search_cleanup",
"search_reindex",
] {
assert!(names.contains(&core), "{core} missing from {names:?}");
}
assert_eq!(
names.contains(&"activitypub"),
cfg!(feature = "federation"),
"{names:?}"
);
}