fix(daemon): consume the MovieCommand channel
Nothing drained AppState's movie_commands mpsc, so manual search and one-click grab were accepted with 202 and then did nothing until the 64-slot buffer filled and the endpoint started 503ing. A new daemon task drains it: Search resets the movie's backoff and runs the targeted-search + grab lane immediately; Grab sends the already-chosen release straight to Transmission. Closes #107
This commit is contained in:
@@ -7,6 +7,7 @@ mod grab;
|
||||
mod import;
|
||||
mod indexers;
|
||||
mod jellyfin;
|
||||
mod manual;
|
||||
mod notify;
|
||||
mod reaper;
|
||||
pub mod reconcile;
|
||||
@@ -100,8 +101,8 @@ enum Error {
|
||||
},
|
||||
#[error("serve: {0}")]
|
||||
Serve(std::io::Error),
|
||||
#[error("reconcile task: {0}")]
|
||||
ReconcileTask(#[from] tokio::task::JoinError),
|
||||
#[error("background task: {0}")]
|
||||
BackgroundTask(#[from] tokio::task::JoinError),
|
||||
}
|
||||
|
||||
async fn run() -> Result<(), Error> {
|
||||
@@ -120,7 +121,8 @@ async fn run() -> Result<(), Error> {
|
||||
None
|
||||
};
|
||||
let notifier = Notifier::new(config.ntfy_url.clone())?;
|
||||
let reconcile = reconcile_loop(&database, &config, &transmission, tmdb.as_ref(), ¬ifier)?;
|
||||
let (reconcile, manual_grab) =
|
||||
reconcile_loop(&database, &config, &transmission, tmdb.as_ref(), ¬ifier)?;
|
||||
|
||||
// Jellyseerr's Radarr shim (DESIGN.md §9.4) reads the same database and
|
||||
// needs its own TMDB client for `movie/lookup`.
|
||||
@@ -135,9 +137,9 @@ async fn run() -> Result<(), Error> {
|
||||
if let Some(tmdb_url) = config.tmdb_url {
|
||||
upstreams = upstreams.with_tmdb_url(tmdb_url);
|
||||
}
|
||||
let state = AppState::new(upstreams)?.with_database(database);
|
||||
let state = AppState::new(upstreams)?.with_database(database.clone());
|
||||
|
||||
let app = arr_api::router(state)
|
||||
let app = arr_api::router(state.clone())
|
||||
.merge(arr_compat::router(compat))
|
||||
.fallback(web::serve)
|
||||
.layer(TraceLayer::new_for_http());
|
||||
@@ -152,7 +154,11 @@ async fn run() -> Result<(), Error> {
|
||||
tracing::info!(addr = %config.bind_addr, docs = arr_api::DOCS_PATH, "listening");
|
||||
|
||||
let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
|
||||
let mut reconcile_task = tokio::spawn(reconcile.run(shutdown_rx));
|
||||
let mut reconcile_task = tokio::spawn(reconcile.run(shutdown_rx.clone()));
|
||||
// Issue #107: nothing else drains `AppState`'s `MovieCommand` channel, so
|
||||
// the manual-search and manual-grab endpoints were a no-op — the command
|
||||
// sat in the 64-slot buffer forever.
|
||||
let mut manual_task = tokio::spawn(manual::run(state, database, manual_grab, shutdown_rx));
|
||||
let signal_tx = shutdown_tx.clone();
|
||||
let server = async move {
|
||||
axum::serve(listener, app)
|
||||
@@ -168,10 +174,19 @@ async fn run() -> Result<(), Error> {
|
||||
result = &mut server => {
|
||||
let _ = shutdown_tx.send(true);
|
||||
reconcile_task.await?;
|
||||
manual_task.await?;
|
||||
result.map_err(Error::Serve)
|
||||
}
|
||||
result = &mut reconcile_task => {
|
||||
result?;
|
||||
let _ = shutdown_tx.send(true);
|
||||
manual_task.await?;
|
||||
server.await.map_err(Error::Serve)
|
||||
}
|
||||
result = &mut manual_task => {
|
||||
result?;
|
||||
let _ = shutdown_tx.send(true);
|
||||
reconcile_task.await?;
|
||||
server.await.map_err(Error::Serve)
|
||||
}
|
||||
}
|
||||
@@ -180,14 +195,19 @@ async fn run() -> Result<(), Error> {
|
||||
/// Wire the reconcile lanes (DESIGN.md §8). Grab and RSS both need a
|
||||
/// Prowlarr key and grab needs TMDB as well; a lane whose upstream is not
|
||||
/// configured stays unregistered rather than failing every tick.
|
||||
///
|
||||
/// Also returns a second, independent `GrabAction` for `manual::run` (issue
|
||||
/// #107) — the manual trigger needs the same search-and-grab path on demand
|
||||
/// rather than on the reconcile tick's schedule, and `ReconcileLoop::register`
|
||||
/// takes ownership of the one it ticks.
|
||||
fn reconcile_loop(
|
||||
database: &Db,
|
||||
config: &Config,
|
||||
transmission: &arr_dl::TransmissionClient,
|
||||
tmdb: Option<&Arc<TmdbClient>>,
|
||||
notifier: &Notifier,
|
||||
) -> Result<ReconcileLoop, Error> {
|
||||
let mut reconcile = ReconcileLoop::new(database.clone());
|
||||
) -> Result<(ReconcileLoop, Option<GrabAction>), Error> {
|
||||
let reconcile = ReconcileLoop::new(database.clone());
|
||||
let seeding = SeedingRules::new(
|
||||
SeedingLimits {
|
||||
ratio: config.seed_ratio_limit,
|
||||
@@ -213,20 +233,14 @@ fn reconcile_loop(
|
||||
.map(|key| arr_indexer::ProwlarrClient::new(config.prowlarr_url.clone(), key))
|
||||
.transpose()?;
|
||||
|
||||
if let (Some(prowlarr), Some(tmdb)) = (prowlarr.as_ref(), tmdb) {
|
||||
reconcile = reconcile.register(
|
||||
Tick::Reconcile,
|
||||
GrabAction::new(
|
||||
prowlarr.clone(),
|
||||
transmission.clone(),
|
||||
config.download_dir.clone(),
|
||||
seeding.clone(),
|
||||
)
|
||||
.with_tmdb(Arc::clone(tmdb)),
|
||||
);
|
||||
} else {
|
||||
tracing::warn!("Prowlarr or TMDB is not configured: nothing will be grabbed");
|
||||
}
|
||||
let (mut reconcile, manual_grab) = register_movie_grab(
|
||||
reconcile,
|
||||
prowlarr.as_ref(),
|
||||
transmission,
|
||||
config,
|
||||
&seeding,
|
||||
tmdb,
|
||||
);
|
||||
// TV grabbing needs no TMDB at grab time: air dates are already on the
|
||||
// episode rows, which is the same gate the digital release date is for
|
||||
// movies (§6.2).
|
||||
@@ -298,7 +312,57 @@ fn reconcile_loop(
|
||||
);
|
||||
}
|
||||
|
||||
Ok(reconcile.register(Tick::Reaper, ReaperAction::new(transmission.clone())))
|
||||
let reconcile = reconcile.register(Tick::Reaper, ReaperAction::new(transmission.clone()));
|
||||
Ok((reconcile, manual_grab))
|
||||
}
|
||||
|
||||
/// Register the movie grab lane on `reconcile` and hand back a second,
|
||||
/// independent instance for `manual::run` (issue #107) — the manual trigger
|
||||
/// needs the same search-and-grab path on demand rather than on the tick's
|
||||
/// schedule, and `ReconcileLoop::register` takes ownership of the one it
|
||||
/// ticks. `None` when Prowlarr or TMDB is not configured; nothing can be
|
||||
/// grabbed either way.
|
||||
fn register_movie_grab(
|
||||
mut reconcile: ReconcileLoop,
|
||||
prowlarr: Option<&arr_indexer::ProwlarrClient>,
|
||||
transmission: &arr_dl::TransmissionClient,
|
||||
config: &Config,
|
||||
seeding: &SeedingRules,
|
||||
tmdb: Option<&Arc<TmdbClient>>,
|
||||
) -> (ReconcileLoop, Option<GrabAction>) {
|
||||
let Some((prowlarr, tmdb)) = prowlarr.zip(tmdb) else {
|
||||
tracing::warn!("Prowlarr or TMDB is not configured: nothing will be grabbed");
|
||||
return (reconcile, None);
|
||||
};
|
||||
reconcile = reconcile.register(
|
||||
Tick::Reconcile,
|
||||
movie_grab_action(prowlarr, transmission, config, seeding, tmdb),
|
||||
);
|
||||
let manual_grab = Some(movie_grab_action(
|
||||
prowlarr,
|
||||
transmission,
|
||||
config,
|
||||
seeding,
|
||||
tmdb,
|
||||
));
|
||||
(reconcile, manual_grab)
|
||||
}
|
||||
|
||||
/// The movie grab lane, built fresh for each caller.
|
||||
fn movie_grab_action(
|
||||
prowlarr: &arr_indexer::ProwlarrClient,
|
||||
transmission: &arr_dl::TransmissionClient,
|
||||
config: &Config,
|
||||
seeding: &SeedingRules,
|
||||
tmdb: &Arc<TmdbClient>,
|
||||
) -> GrabAction {
|
||||
GrabAction::new(
|
||||
prowlarr.clone(),
|
||||
transmission.clone(),
|
||||
config.download_dir.clone(),
|
||||
seeding.clone(),
|
||||
)
|
||||
.with_tmdb(Arc::clone(tmdb))
|
||||
}
|
||||
|
||||
/// Stop accepting on Ctrl-C, or on the SIGTERM a service manager sends.
|
||||
|
||||
Reference in New Issue
Block a user