refactor: extract reindex + enrichment logic from handlers into use cases
Some checks failed
CI / Check / Test (push) Failing after 6m45s
Some checks failed
CI / Check / Test (push) Failing after 6m45s
This commit is contained in:
@@ -14,7 +14,7 @@ graph TB
|
|||||||
subgraph UseCases["Use Cases"]
|
subgraph UseCases["Use Cases"]
|
||||||
UC_AUTH["auth<br/>login, register"]
|
UC_AUTH["auth<br/>login, register"]
|
||||||
UC_DIARY["diary<br/>log_review, get_diary,<br/>get_activity_feed, export"]
|
UC_DIARY["diary<br/>log_review, get_diary,<br/>get_activity_feed, export"]
|
||||||
UC_MOVIES["movies<br/>get_movies, get_movie_profile,<br/>enrich_movie, sync_poster,<br/>reindex_search"]
|
UC_MOVIES["movies<br/>get_movies, get_movie_profile,<br/>enrich_movie, request_enrichment,<br/>sync_poster, reindex_search"]
|
||||||
UC_IMPORT["import<br/>create_session, apply_mapping,<br/>execute, profiles"]
|
UC_IMPORT["import<br/>create_session, apply_mapping,<br/>execute, profiles"]
|
||||||
UC_USERS["users<br/>get_users, get_profile,<br/>update_profile"]
|
UC_USERS["users<br/>get_users, get_profile,<br/>update_profile"]
|
||||||
UC_WATCHLIST["watchlist<br/>add, remove, get"]
|
UC_WATCHLIST["watchlist<br/>add, remove, get"]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use application::movies::{commands::EnrichMovieCommand, enrich_movie};
|
use application::movies::{commands::EnrichMovieCommand, enrich_movie, request_enrichment};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use domain::{
|
use domain::{
|
||||||
@@ -288,42 +288,25 @@ impl EventHandler for EnrichmentHandler {
|
|||||||
_ => return Ok(()),
|
_ => return Ok(()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Skip if profile is fresh (< 30 days old)
|
let Some(profile) = request_enrichment::fetch_if_stale(
|
||||||
if let Ok(Some(existing)) = self.profile_repo.get_by_movie_id(&movie_id).await {
|
self.enrichment_client.as_ref(),
|
||||||
let age = Utc::now() - existing.enriched_at;
|
&self.profile_repo,
|
||||||
if age.num_days() < 30 {
|
movie_id.clone(),
|
||||||
tracing::debug!(
|
&external_metadata_id,
|
||||||
movie_id = %movie_id.value(),
|
)
|
||||||
"skipping enrichment — profile is {} days old",
|
.await?
|
||||||
age.num_days()
|
else {
|
||||||
);
|
return Ok(());
|
||||||
return Ok(());
|
};
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tracing::info!(movie_id = %movie_id.value(), external_id = %external_metadata_id, "enriching movie");
|
self.download_cast_photos(&profile).await;
|
||||||
|
enrich_movie::execute(
|
||||||
match self
|
&self.movie_repository,
|
||||||
.enrichment_client
|
&self.profile_repo,
|
||||||
.fetch_profile(movie_id.clone(), &external_metadata_id)
|
&self.person_command,
|
||||||
.await
|
&self.search_command,
|
||||||
{
|
EnrichMovieCommand { movie_id, profile },
|
||||||
Ok(profile) => {
|
)
|
||||||
self.download_cast_photos(&profile).await;
|
.await
|
||||||
enrich_movie::execute(
|
|
||||||
&self.movie_repository,
|
|
||||||
&self.profile_repo,
|
|
||||||
&self.person_command,
|
|
||||||
&self.search_command,
|
|
||||||
EnrichMovieCommand { movie_id, profile },
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
Err(DomainError::NotFound(msg)) => {
|
|
||||||
tracing::warn!(movie_id = %movie_id.value(), "TMDb lookup found nothing: {msg}");
|
|
||||||
}
|
|
||||||
Err(e) => return Err(e),
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ pub mod get_movie_profile;
|
|||||||
pub mod get_movies;
|
pub mod get_movies;
|
||||||
pub mod queries;
|
pub mod queries;
|
||||||
pub mod reindex_search;
|
pub mod reindex_search;
|
||||||
|
pub mod request_enrichment;
|
||||||
pub mod search_cleanup;
|
pub mod search_cleanup;
|
||||||
pub mod sync_poster;
|
pub mod sync_poster;
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,115 @@ use crate::context::AppContext;
|
|||||||
|
|
||||||
const BATCH_SIZE: u32 = 500;
|
const BATCH_SIZE: u32 = 500;
|
||||||
|
|
||||||
|
pub struct ReindexResult {
|
||||||
|
pub movies_indexed: u64,
|
||||||
|
pub persons_indexed: u64,
|
||||||
|
pub persons_backfilled: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn execute(ctx: &AppContext) -> Result<ReindexResult, DomainError> {
|
||||||
|
let movies_indexed = reindex_movies(ctx).await?;
|
||||||
|
let persons_backfilled = backfill_persons(ctx).await?;
|
||||||
|
let persons_indexed = reindex_persons(ctx).await?;
|
||||||
|
|
||||||
|
Ok(ReindexResult {
|
||||||
|
movies_indexed,
|
||||||
|
persons_indexed,
|
||||||
|
persons_backfilled,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn reindex_movies(ctx: &AppContext) -> Result<u64, DomainError> {
|
||||||
|
let mut count: u64 = 0;
|
||||||
|
let mut offset: u32 = 0;
|
||||||
|
loop {
|
||||||
|
let page = ctx
|
||||||
|
.repos
|
||||||
|
.movie
|
||||||
|
.list_movies(
|
||||||
|
&PageParams {
|
||||||
|
limit: BATCH_SIZE,
|
||||||
|
offset,
|
||||||
|
},
|
||||||
|
&MovieFilter::default(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
for summary in &page.items {
|
||||||
|
let movie_id = summary.movie.id().clone();
|
||||||
|
let profile = ctx.repos.movie_profile.get_by_movie_id(&movie_id).await?;
|
||||||
|
|
||||||
|
if let Err(e) = ctx
|
||||||
|
.repos
|
||||||
|
.search_command
|
||||||
|
.index(IndexableDocument::Movie {
|
||||||
|
id: movie_id.clone(),
|
||||||
|
movie: Box::new(summary.movie.clone()),
|
||||||
|
profile: profile.map(Box::new),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
tracing::warn!(movie_id = %movie_id.value(), "reindex movie failed: {e}");
|
||||||
|
}
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (page.items.len() as u32) < BATCH_SIZE {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
offset += BATCH_SIZE;
|
||||||
|
tokio::task::yield_now().await;
|
||||||
|
}
|
||||||
|
Ok(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn backfill_persons(ctx: &AppContext) -> Result<u64, DomainError> {
|
||||||
|
let mut total = 0u64;
|
||||||
|
loop {
|
||||||
|
let (count, has_more) = ctx
|
||||||
|
.repos
|
||||||
|
.person_command
|
||||||
|
.backfill_from_credits_batch(BATCH_SIZE)
|
||||||
|
.await?;
|
||||||
|
total += count;
|
||||||
|
if !has_more {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
tokio::task::yield_now().await;
|
||||||
|
}
|
||||||
|
Ok(total)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn reindex_persons(ctx: &AppContext) -> Result<u64, DomainError> {
|
||||||
|
let mut count: u64 = 0;
|
||||||
|
let mut offset: u32 = 0;
|
||||||
|
loop {
|
||||||
|
let persons = ctx.repos.person_query.list_page(BATCH_SIZE, offset).await?;
|
||||||
|
|
||||||
|
for person in &persons {
|
||||||
|
if let Err(e) = ctx
|
||||||
|
.repos
|
||||||
|
.search_command
|
||||||
|
.index(IndexableDocument::Person {
|
||||||
|
id: person.id().clone(),
|
||||||
|
person: Box::new(person.clone()),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
tracing::warn!(person = %person.name(), "reindex person failed: {e}");
|
||||||
|
}
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (persons.len() as u32) < BATCH_SIZE {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
offset += BATCH_SIZE;
|
||||||
|
tokio::task::yield_now().await;
|
||||||
|
}
|
||||||
|
Ok(count)
|
||||||
|
}
|
||||||
|
|
||||||
pub struct SearchReindexHandler {
|
pub struct SearchReindexHandler {
|
||||||
ctx: AppContext,
|
ctx: AppContext,
|
||||||
running: AtomicBool,
|
running: AtomicBool,
|
||||||
@@ -37,129 +146,22 @@ impl EventHandler for SearchReindexHandler {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = self.run_reindex().await;
|
|
||||||
self.running.store(false, Ordering::SeqCst);
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SearchReindexHandler {
|
|
||||||
async fn run_reindex(&self) -> Result<(), DomainError> {
|
|
||||||
tracing::info!("search reindex started");
|
tracing::info!("search reindex started");
|
||||||
|
let result = execute(&self.ctx).await;
|
||||||
|
self.running.store(false, Ordering::SeqCst);
|
||||||
|
|
||||||
let movies_indexed = self.reindex_movies().await?;
|
let r = result?;
|
||||||
let backfilled = self.backfill_persons().await?;
|
if r.persons_backfilled > 0 {
|
||||||
if backfilled > 0 {
|
tracing::info!(
|
||||||
tracing::info!(backfilled, "backfilled missing persons from credits");
|
backfilled = r.persons_backfilled,
|
||||||
|
"backfilled missing persons from credits"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
let persons_indexed = self.reindex_persons().await?;
|
tracing::info!(
|
||||||
|
movies_indexed = r.movies_indexed,
|
||||||
tracing::info!(movies_indexed, persons_indexed, "search reindex completed");
|
persons_indexed = r.persons_indexed,
|
||||||
|
"search reindex completed"
|
||||||
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn reindex_movies(&self) -> Result<u64, DomainError> {
|
|
||||||
let mut count: u64 = 0;
|
|
||||||
let mut offset: u32 = 0;
|
|
||||||
loop {
|
|
||||||
let page = self
|
|
||||||
.ctx
|
|
||||||
.repos
|
|
||||||
.movie
|
|
||||||
.list_movies(
|
|
||||||
&PageParams {
|
|
||||||
limit: BATCH_SIZE,
|
|
||||||
offset,
|
|
||||||
},
|
|
||||||
&MovieFilter::default(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
for summary in &page.items {
|
|
||||||
let movie_id = summary.movie.id().clone();
|
|
||||||
let profile = self
|
|
||||||
.ctx
|
|
||||||
.repos
|
|
||||||
.movie_profile
|
|
||||||
.get_by_movie_id(&movie_id)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if let Err(e) = self
|
|
||||||
.ctx
|
|
||||||
.repos
|
|
||||||
.search_command
|
|
||||||
.index(IndexableDocument::Movie {
|
|
||||||
id: movie_id.clone(),
|
|
||||||
movie: Box::new(summary.movie.clone()),
|
|
||||||
profile: profile.map(Box::new),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
tracing::warn!(movie_id = %movie_id.value(), "reindex movie failed: {e}");
|
|
||||||
}
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (page.items.len() as u32) < BATCH_SIZE {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
offset += BATCH_SIZE;
|
|
||||||
tokio::task::yield_now().await;
|
|
||||||
}
|
|
||||||
Ok(count)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn backfill_persons(&self) -> Result<u64, DomainError> {
|
|
||||||
let mut total = 0u64;
|
|
||||||
loop {
|
|
||||||
let (count, has_more) = self
|
|
||||||
.ctx
|
|
||||||
.repos
|
|
||||||
.person_command
|
|
||||||
.backfill_from_credits_batch(BATCH_SIZE)
|
|
||||||
.await?;
|
|
||||||
total += count;
|
|
||||||
if !has_more {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
tokio::task::yield_now().await;
|
|
||||||
}
|
|
||||||
Ok(total)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn reindex_persons(&self) -> Result<u64, DomainError> {
|
|
||||||
let mut count: u64 = 0;
|
|
||||||
let mut offset: u32 = 0;
|
|
||||||
loop {
|
|
||||||
let persons = self
|
|
||||||
.ctx
|
|
||||||
.repos
|
|
||||||
.person_query
|
|
||||||
.list_page(BATCH_SIZE, offset)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
for person in &persons {
|
|
||||||
if let Err(e) = self
|
|
||||||
.ctx
|
|
||||||
.repos
|
|
||||||
.search_command
|
|
||||||
.index(IndexableDocument::Person {
|
|
||||||
id: person.id().clone(),
|
|
||||||
person: Box::new(person.clone()),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
tracing::warn!(person = %person.name(), "reindex person failed: {e}");
|
|
||||||
}
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (persons.len() as u32) < BATCH_SIZE {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
offset += BATCH_SIZE;
|
|
||||||
tokio::task::yield_now().await;
|
|
||||||
}
|
|
||||||
Ok(count)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
44
crates/application/src/movies/request_enrichment.rs
Normal file
44
crates/application/src/movies/request_enrichment.rs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use chrono::Utc;
|
||||||
|
use domain::{
|
||||||
|
errors::DomainError,
|
||||||
|
models::MovieProfile,
|
||||||
|
ports::{MovieEnrichmentClient, MovieProfileRepository},
|
||||||
|
value_objects::MovieId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const STALENESS_DAYS: i64 = 30;
|
||||||
|
|
||||||
|
pub async fn fetch_if_stale(
|
||||||
|
enrichment_client: &dyn MovieEnrichmentClient,
|
||||||
|
profile_repo: &Arc<dyn MovieProfileRepository>,
|
||||||
|
movie_id: MovieId,
|
||||||
|
external_metadata_id: &str,
|
||||||
|
) -> Result<Option<MovieProfile>, DomainError> {
|
||||||
|
if let Ok(Some(existing)) = profile_repo.get_by_movie_id(&movie_id).await {
|
||||||
|
let age = Utc::now() - existing.enriched_at;
|
||||||
|
if age.num_days() < STALENESS_DAYS {
|
||||||
|
tracing::debug!(
|
||||||
|
movie_id = %movie_id.value(),
|
||||||
|
"skipping enrichment — profile is {} days old",
|
||||||
|
age.num_days()
|
||||||
|
);
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::info!(movie_id = %movie_id.value(), external_id = %external_metadata_id, "enriching movie");
|
||||||
|
|
||||||
|
match enrichment_client
|
||||||
|
.fetch_profile(movie_id, external_metadata_id)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(profile) => Ok(Some(profile)),
|
||||||
|
Err(DomainError::NotFound(msg)) => {
|
||||||
|
tracing::warn!("TMDb lookup found nothing: {msg}");
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user