fix(arr): wire the command timeout to the settings row
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
//! What the API needs to answer a request: one HTTP client and the addresses
|
||||
//! of the three upstreams the service cannot work without (DESIGN.md §3).
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::{atomic::AtomicU64, Arc};
|
||||
use std::time::Duration;
|
||||
|
||||
use arr_db::Db;
|
||||
@@ -81,6 +81,10 @@ pub struct AppState {
|
||||
pending_metadata_commands: Arc<tokio::sync::Mutex<mpsc::Receiver<MetadataCommand>>>,
|
||||
subtitle_providers: Arc<Vec<Arc<dyn Provider>>>,
|
||||
translation_backends: Arc<Vec<Arc<dyn Backend>>>,
|
||||
/// The remote-command backend's live timeout, in milliseconds (#219).
|
||||
/// `None` unless the daemon compiled and configured that backend; the
|
||||
/// settings API writes it on every edit of the row.
|
||||
command_timeout: Option<Arc<AtomicU64>>,
|
||||
jellyfin: Option<JellyfinClient>,
|
||||
syncer: Syncer,
|
||||
}
|
||||
@@ -155,6 +159,7 @@ impl AppState {
|
||||
pending_metadata_commands: Arc::new(tokio::sync::Mutex::new(pending_metadata_commands)),
|
||||
subtitle_providers: Arc::new(Vec::new()),
|
||||
translation_backends: Arc::new(Vec::new()),
|
||||
command_timeout: None,
|
||||
jellyfin: None,
|
||||
syncer: Syncer::default(),
|
||||
})
|
||||
@@ -191,6 +196,15 @@ impl AppState {
|
||||
self
|
||||
}
|
||||
|
||||
/// Attach the cell the remote-command backend re-reads per batch (#219),
|
||||
/// so an edit of `remote_command_timeout_seconds` reaches it without a
|
||||
/// restart. Absent when that backend is not configured.
|
||||
#[must_use]
|
||||
pub fn with_command_timeout(mut self, timeout: Arc<AtomicU64>) -> Self {
|
||||
self.command_timeout = Some(timeout);
|
||||
self
|
||||
}
|
||||
|
||||
/// Attach the Jellyfin client, so a manual subtitle grab or translation
|
||||
/// can trigger the same library refresh import does (§7.5, §15).
|
||||
#[must_use]
|
||||
@@ -219,6 +233,10 @@ impl AppState {
|
||||
.find(|backend| backend.id().as_str() == id)
|
||||
}
|
||||
|
||||
pub(crate) fn command_timeout(&self) -> Option<&Arc<AtomicU64>> {
|
||||
self.command_timeout.as_ref()
|
||||
}
|
||||
|
||||
/// Attach the `alass` binary this deployment runs (§15). Defaults to
|
||||
/// resolving `alass` from `PATH`.
|
||||
#[must_use]
|
||||
|
||||
@@ -197,6 +197,7 @@ pub async fn update(
|
||||
let input = parsed(body)?;
|
||||
input.validate().map_err(ApiError::Invalid)?;
|
||||
let columns = input.into_columns()?;
|
||||
let timeout_seconds = columns.remote_command_timeout_seconds;
|
||||
sqlx::query!(
|
||||
r#"UPDATE subtitle_settings SET
|
||||
wanted_languages = ?, providers_enabled = ?, translation_engine = ?,
|
||||
@@ -213,6 +214,17 @@ pub async fn update(
|
||||
)
|
||||
.execute(pool(&state)?)
|
||||
.await?;
|
||||
// Issue #219: the row alone never reaches the running backend. Push it
|
||||
// into the cell the remote-command translator re-reads per batch; the
|
||||
// cell counts milliseconds, the row counts seconds.
|
||||
if let Some(timeout) = state.command_timeout() {
|
||||
timeout.store(
|
||||
u64::try_from(timeout_seconds)
|
||||
.unwrap_or(u64::MAX)
|
||||
.saturating_mul(1_000),
|
||||
std::sync::atomic::Ordering::Relaxed,
|
||||
);
|
||||
}
|
||||
Ok(Json(load(&state).await?))
|
||||
}
|
||||
|
||||
@@ -223,24 +235,39 @@ mod tests {
|
||||
use crate::{router, AppState, Upstreams};
|
||||
|
||||
async fn application() -> (tempfile::TempDir, String) {
|
||||
let (dir, base, _timeout) = application_with_timeout().await;
|
||||
(dir, base)
|
||||
}
|
||||
|
||||
/// The same app, with the remote-command backend's live timeout cell
|
||||
/// attached — what the daemon wires up when that backend is configured.
|
||||
async fn application_with_timeout() -> (
|
||||
tempfile::TempDir,
|
||||
String,
|
||||
std::sync::Arc<std::sync::atomic::AtomicU64>,
|
||||
) {
|
||||
use std::sync::{atomic::AtomicU64, Arc};
|
||||
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let database = arr_db::Db::connect(dir.path().join("arr.db"))
|
||||
.await
|
||||
.expect("connect database");
|
||||
database.migrate().await.expect("migrate database");
|
||||
let timeout = Arc::new(AtomicU64::new(30_000));
|
||||
let state = AppState::new(Upstreams::new(
|
||||
"http://127.0.0.1:1".into(),
|
||||
"http://127.0.0.1:1".into(),
|
||||
))
|
||||
.expect("state")
|
||||
.with_database(database);
|
||||
.with_database(database)
|
||||
.with_command_timeout(timeout.clone());
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
|
||||
.await
|
||||
.expect("bind");
|
||||
let address = listener.local_addr().expect("address");
|
||||
let app = router(state);
|
||||
tokio::spawn(async move { axum::serve(listener, app).await.expect("serve") });
|
||||
(dir, format!("http://{address}"))
|
||||
(dir, format!("http://{address}"), timeout)
|
||||
}
|
||||
|
||||
fn valid_input() -> serde_json::Value {
|
||||
@@ -307,6 +334,33 @@ mod tests {
|
||||
assert_eq!(refetched, updated);
|
||||
}
|
||||
|
||||
/// Issue #219: the row alone never reaches the running backend, so a PUT
|
||||
/// must push its value into the cell the command translator re-reads.
|
||||
#[tokio::test]
|
||||
async fn a_put_updates_the_live_command_timeout() {
|
||||
let (_dir, base, timeout) = application_with_timeout().await;
|
||||
assert_eq!(
|
||||
timeout.load(std::sync::atomic::Ordering::Relaxed),
|
||||
30_000,
|
||||
"seeded from the row at startup"
|
||||
);
|
||||
|
||||
reqwest::Client::new()
|
||||
.put(format!("{base}/api/settings/subtitles"))
|
||||
.json(&valid_input())
|
||||
.send()
|
||||
.await
|
||||
.expect("put settings")
|
||||
.error_for_status()
|
||||
.expect("valid input accepted");
|
||||
|
||||
assert_eq!(
|
||||
timeout.load(std::sync::atomic::Ordering::Relaxed),
|
||||
45_000,
|
||||
"the edited value reaches the running backend"
|
||||
);
|
||||
}
|
||||
|
||||
/// Whether a known engine is selectable depends on which `translate-*`
|
||||
/// features this binary was built with, so the test asks the build rather
|
||||
/// than assuming. With no feature on, every engine is uncompiled and must
|
||||
|
||||
Reference in New Issue
Block a user