refactor: split jobs.rs into per-context modules

This commit is contained in:
2026-06-11 14:44:23 +02:00
parent 4f0f44dec3
commit 8ac87a3735
18 changed files with 170 additions and 134 deletions

View File

@@ -40,8 +40,8 @@ pub use import_profile::PostgresImportProfileRepository;
pub use import_session::PostgresImportSessionRepository;
pub use persons::{PostgresPersonAdapter, create_person_adapter};
pub use profile::PostgresMovieProfileRepository;
pub use refresh_sessions::PostgresRefreshSessionAdapter;
pub use profile_fields::PostgresProfileFieldsRepository;
pub use refresh_sessions::PostgresRefreshSessionAdapter;
pub use users::PostgresUserRepository;
pub use watch_event::{PostgresWatchEventRepository, PostgresWebhookTokenRepository};
pub use watchlist::PostgresWatchlistRepository;

View File

@@ -1,9 +1,7 @@
use async_trait::async_trait;
use chrono::DateTime;
use domain::{
errors::DomainError,
models::RefreshSession,
ports::RefreshSessionRepository,
errors::DomainError, models::RefreshSession, ports::RefreshSessionRepository,
value_objects::UserId,
};
use sqlx::PgPool;

View File

@@ -41,8 +41,8 @@ pub use import_profile::SqliteImportProfileRepository;
pub use import_session::SqliteImportSessionRepository;
pub use persons::{SqlitePersonAdapter, create_person_adapter};
pub use profile::SqliteMovieProfileRepository;
pub use refresh_sessions::SqliteRefreshSessionAdapter;
pub use profile_fields::SqliteProfileFieldsRepository;
pub use refresh_sessions::SqliteRefreshSessionAdapter;
pub use users::SqliteUserRepository;
pub use watch_event::{SqliteWatchEventRepository, SqliteWebhookTokenRepository};
pub use watchlist::SqliteWatchlistRepository;

View File

@@ -1,9 +1,7 @@
use async_trait::async_trait;
use chrono::DateTime;
use domain::{
errors::DomainError,
models::RefreshSession,
ports::RefreshSessionRepository,
errors::DomainError, models::RefreshSession, ports::RefreshSessionRepository,
value_objects::UserId,
};
use sqlx::SqlitePool;

View File

@@ -0,0 +1,39 @@
use std::time::Duration;
use async_trait::async_trait;
use domain::{errors::DomainError, events::DomainEvent, ports::PeriodicJob};
use crate::context::AppContext;
pub struct EnrichmentStalenessJob {
ctx: AppContext,
}
impl EnrichmentStalenessJob {
pub fn new(ctx: AppContext) -> Self {
Self { ctx }
}
}
#[async_trait]
impl PeriodicJob for EnrichmentStalenessJob {
fn interval(&self) -> Duration {
Duration::from_secs(3600)
}
async fn run(&self) -> Result<(), DomainError> {
let stale = self.ctx.repos.movie_profile.list_stale().await?;
if stale.is_empty() {
return Ok(());
}
tracing::info!("enrichment scan: {} stale movies", stale.len());
for (movie_id, external_metadata_id) in stale {
let event = DomainEvent::MovieEnrichmentRequested {
movie_id,
external_metadata_id,
};
self.ctx.services.event_publisher.publish(&event).await?;
}
Ok(())
}
}

View File

@@ -0,0 +1,29 @@
use std::time::Duration;
use async_trait::async_trait;
use domain::{errors::DomainError, ports::PeriodicJob};
use crate::context::AppContext;
pub struct ImportSessionCleanupJob {
ctx: AppContext,
}
impl ImportSessionCleanupJob {
pub fn new(ctx: AppContext) -> Self {
Self { ctx }
}
}
#[async_trait]
impl PeriodicJob for ImportSessionCleanupJob {
fn interval(&self) -> Duration {
Duration::from_secs(3600)
}
async fn run(&self) -> Result<(), DomainError> {
let n = crate::import::cleanup::execute(&self.ctx).await?;
tracing::info!("import session cleanup: removed {} expired sessions", n);
Ok(())
}
}

View File

@@ -0,0 +1,11 @@
mod enrichment_staleness;
mod import_cleanup;
mod refresh_session_cleanup;
mod watch_event_cleanup;
mod wrapup;
pub use enrichment_staleness::EnrichmentStalenessJob;
pub use import_cleanup::ImportSessionCleanupJob;
pub use refresh_session_cleanup::RefreshSessionCleanupJob;
pub use watch_event_cleanup::WatchEventCleanupJob;
pub use wrapup::{WrapUpAutoGenerateJob, WrapUpCleanupJob};

View File

@@ -0,0 +1,31 @@
use std::time::Duration;
use async_trait::async_trait;
use domain::{errors::DomainError, ports::PeriodicJob};
use crate::context::AppContext;
pub struct RefreshSessionCleanupJob {
ctx: AppContext,
}
impl RefreshSessionCleanupJob {
pub fn new(ctx: AppContext) -> Self {
Self { ctx }
}
}
#[async_trait]
impl PeriodicJob for RefreshSessionCleanupJob {
fn interval(&self) -> Duration {
Duration::from_secs(86400)
}
async fn run(&self) -> Result<(), DomainError> {
let n = self.ctx.repos.refresh_session.delete_expired().await?;
if n > 0 {
tracing::info!("refresh session cleanup: removed {n} expired sessions");
}
Ok(())
}
}

View File

@@ -0,0 +1,31 @@
use std::time::Duration;
use async_trait::async_trait;
use domain::{errors::DomainError, ports::PeriodicJob};
use crate::context::AppContext;
pub struct WatchEventCleanupJob {
ctx: AppContext,
}
impl WatchEventCleanupJob {
pub fn new(ctx: AppContext) -> Self {
Self { ctx }
}
}
#[async_trait]
impl PeriodicJob for WatchEventCleanupJob {
fn interval(&self) -> Duration {
Duration::from_secs(86400)
}
async fn run(&self) -> Result<(), DomainError> {
let n = crate::integrations::cleanup::execute(&self.ctx).await?;
if n > 0 {
tracing::info!("watch event cleanup: removed {n} old entries");
}
Ok(())
}
}

View File

@@ -2,91 +2,10 @@ use std::time::Duration;
use async_trait::async_trait;
use chrono::Datelike;
use domain::{errors::DomainError, events::DomainEvent, ports::PeriodicJob};
use domain::{errors::DomainError, ports::PeriodicJob};
use crate::context::AppContext;
pub struct ImportSessionCleanupJob {
ctx: AppContext,
}
impl ImportSessionCleanupJob {
pub fn new(ctx: AppContext) -> Self {
Self { ctx }
}
}
#[async_trait]
impl PeriodicJob for ImportSessionCleanupJob {
fn interval(&self) -> Duration {
Duration::from_secs(3600)
}
async fn run(&self) -> Result<(), DomainError> {
let n = crate::import::cleanup::execute(&self.ctx).await?;
tracing::info!("import session cleanup: removed {} expired sessions", n);
Ok(())
}
}
pub struct WatchEventCleanupJob {
ctx: AppContext,
}
impl WatchEventCleanupJob {
pub fn new(ctx: AppContext) -> Self {
Self { ctx }
}
}
#[async_trait]
impl PeriodicJob for WatchEventCleanupJob {
fn interval(&self) -> Duration {
Duration::from_secs(86400)
}
async fn run(&self) -> Result<(), DomainError> {
let n = crate::integrations::cleanup::execute(&self.ctx).await?;
if n > 0 {
tracing::info!("watch event cleanup: removed {n} old entries");
}
Ok(())
}
}
pub struct EnrichmentStalenessJob {
ctx: AppContext,
}
impl EnrichmentStalenessJob {
pub fn new(ctx: AppContext) -> Self {
Self { ctx }
}
}
#[async_trait]
impl PeriodicJob for EnrichmentStalenessJob {
fn interval(&self) -> Duration {
Duration::from_secs(3600)
}
async fn run(&self) -> Result<(), DomainError> {
let stale = self.ctx.repos.movie_profile.list_stale().await?;
if stale.is_empty() {
return Ok(());
}
tracing::info!("enrichment scan: {} stale movies", stale.len());
for (movie_id, external_metadata_id) in stale {
let event = DomainEvent::MovieEnrichmentRequested {
movie_id,
external_metadata_id,
};
self.ctx.services.event_publisher.publish(&event).await?;
}
Ok(())
}
}
pub struct WrapUpAutoGenerateJob {
ctx: AppContext,
}
@@ -105,7 +24,6 @@ impl PeriodicJob for WrapUpAutoGenerateJob {
async fn run(&self) -> Result<(), DomainError> {
let now = chrono::Utc::now().naive_utc();
// Only run in January
if now.month() != 1 {
return Ok(());
}
@@ -140,7 +58,6 @@ impl PeriodicJob for WrapUpAutoGenerateJob {
}
}
// Global wrap-up
let existing = self
.ctx
.repos
@@ -162,31 +79,6 @@ impl PeriodicJob for WrapUpAutoGenerateJob {
}
}
pub struct RefreshSessionCleanupJob {
ctx: AppContext,
}
impl RefreshSessionCleanupJob {
pub fn new(ctx: AppContext) -> Self {
Self { ctx }
}
}
#[async_trait]
impl PeriodicJob for RefreshSessionCleanupJob {
fn interval(&self) -> Duration {
Duration::from_secs(86400)
}
async fn run(&self) -> Result<(), DomainError> {
let n = self.ctx.repos.refresh_session.delete_expired().await?;
if n > 0 {
tracing::info!("refresh session cleanup: removed {n} expired sessions");
}
Ok(())
}
}
pub struct WrapUpCleanupJob {
ctx: AppContext,
}

View File

@@ -11,8 +11,8 @@ use domain::{
MovieProfileRepository, MovieRepository, ObjectStorage, PasswordHasher, PersonCommand,
PersonQuery, PosterFetcherClient, RefreshSessionRepository, ReviewRepository,
SearchCommand, SearchPort, StatsRepository, UserProfileFieldsRepository, UserRepository,
UserSettingsRepository, WatchEventRepository, WatchlistRepository,
WebhookTokenRepository, WrapUpRepository, WrapUpStatsQuery,
UserSettingsRepository, WatchEventRepository, WatchlistRepository, WebhookTokenRepository,
WrapUpRepository, WrapUpStatsQuery,
},
testing::{
FakeAuthService, FakeDiaryRepository, FakeDocumentParser, FakeMetadataClient,
@@ -21,8 +21,8 @@ use domain::{
InMemoryMovieProfileRepository, InMemoryMovieRepository, InMemoryProfileFieldsRepo,
InMemoryRefreshSessionRepository, InMemoryReviewRepository, InMemoryUserRepository,
InMemoryUserSettingsRepository, InMemoryWatchEventRepository, InMemoryWatchlistRepository,
InMemoryWebhookTokenRepository, NoopEventPublisher, NoopObjectStorage,
PanicDiaryExporter, PanicPersonCommand,
InMemoryWebhookTokenRepository, NoopEventPublisher, NoopObjectStorage, PanicDiaryExporter,
PanicPersonCommand,
},
};

View File

@@ -44,10 +44,10 @@ pub use import::{
};
pub use import_profile::ImportProfile;
pub use import_session::ImportSession;
pub use refresh_session::RefreshSession;
pub use person::{
CastCredit, CrewCredit, ExternalPersonId, Person, PersonCredits, PersonEnrichmentData, PersonId,
};
pub use refresh_session::RefreshSession;
pub use search::{
EntityType, IndexableDocument, MovieSearchHit, PersonSearchHit, SearchFilters, SearchQuery,
SearchResults,

View File

@@ -8,9 +8,9 @@ use crate::{
models::wrapup::WrapUpReport,
models::{
AnnotatedRow, DiaryEntry, DiaryFilter, EntityType, ExportFormat, ExternalPersonId,
FeedEntry, FieldMapping, FileFormat, Goal, ImportError, ImportProfile, ImportSession, RefreshSession,
FeedEntry, FieldMapping, FileFormat, Goal, ImportError, ImportProfile, ImportSession,
IndexableDocument, Movie, MovieFilter, MovieProfile, MovieStats, MovieSummary, ParsedFile,
ParsedPlaybackEvent, Person, PersonCredits, PersonEnrichmentData, PersonId,
ParsedPlaybackEvent, Person, PersonCredits, PersonEnrichmentData, PersonId, RefreshSession,
RemoteGoalEntry, RemoteWatchlistEntry, Review, ReviewHistory, SearchQuery, SearchResults,
User, UserSettings, UserStats, UserSummary, UserTrends, WatchEvent, WatchEventStatus,
WatchlistEntry, WatchlistWithMovie, WebhookToken,

View File

@@ -818,10 +818,7 @@ impl RefreshSessionRepository for InMemoryRefreshSessionRepository {
}
async fn revoke_all_for_user(&self, user_id: &UserId) -> Result<(), DomainError> {
self.store
.lock()
.unwrap()
.retain(|s| s.user_id != *user_id);
self.store.lock().unwrap().retain(|s| s.user_id != *user_id);
Ok(())
}

View File

@@ -79,7 +79,9 @@ pub async fn build_database_adapters(backend: &str, url: &str) -> anyhow::Result
goal: w.goal,
user_settings: w.user_settings,
remote_goal: w.remote_goal,
refresh_session: Arc::new(postgres::PostgresRefreshSessionAdapter::new(w.pool.clone())) as _,
refresh_session: Arc::new(postgres::PostgresRefreshSessionAdapter::new(
w.pool.clone(),
)) as _,
db_pool: DbPool::Postgres(w.pool),
})
}
@@ -118,7 +120,8 @@ pub async fn build_database_adapters(backend: &str, url: &str) -> anyhow::Result
goal: w.goal,
user_settings: w.user_settings,
remote_goal: w.remote_goal,
refresh_session: Arc::new(sqlite::SqliteRefreshSessionAdapter::new(w.pool.clone())) as _,
refresh_session: Arc::new(sqlite::SqliteRefreshSessionAdapter::new(w.pool.clone()))
as _,
db_pool: DbPool::Sqlite(w.pool),
})
}

View File

@@ -17,7 +17,9 @@ use crate::{
render::render_page,
state::AppState,
};
use api_types::{LoginRequest, LoginResponse, LogoutRequest, RefreshRequest, RefreshResponse, RegisterRequest};
use api_types::{
LoginRequest, LoginResponse, LogoutRequest, RefreshRequest, RefreshResponse, RegisterRequest,
};
use application::ports::HtmlPageContext;
use template_askama::{LoginTemplate, RegisterTemplate};

View File

@@ -87,7 +87,9 @@ pub async fn connect(database_url: &str, backend: &str) -> anyhow::Result<Worker
goal: w.goal,
user_settings: w.user_settings,
remote_goal: w.remote_goal,
refresh_session: Arc::new(postgres::PostgresRefreshSessionAdapter::new(w.pool.clone())) as _,
refresh_session: Arc::new(postgres::PostgresRefreshSessionAdapter::new(
w.pool.clone(),
)) as _,
db_pool: DbPool::Postgres(w.pool),
})
}
@@ -130,7 +132,8 @@ pub async fn connect(database_url: &str, backend: &str) -> anyhow::Result<Worker
goal: w.goal,
user_settings: w.user_settings,
remote_goal: w.remote_goal,
refresh_session: Arc::new(sqlite::SqliteRefreshSessionAdapter::new(w.pool.clone())) as _,
refresh_session: Arc::new(sqlite::SqliteRefreshSessionAdapter::new(w.pool.clone()))
as _,
db_pool: DbPool::Sqlite(w.pool),
})
}

View File

@@ -178,7 +178,9 @@ async fn main() -> anyhow::Result<()> {
Arc::new(application::jobs::WatchEventCleanupJob::new(ctx.clone())),
Arc::new(application::jobs::WrapUpAutoGenerateJob::new(ctx.clone())),
Arc::new(application::jobs::WrapUpCleanupJob::new(ctx.clone())),
Arc::new(application::jobs::RefreshSessionCleanupJob::new(ctx.clone())),
Arc::new(application::jobs::RefreshSessionCleanupJob::new(
ctx.clone(),
)),
];
if let Some(job) = enrichment_job {
periodic_jobs.push(job);