The clean-up of DeadAir's old copies probed for its table on every open, and on Postgres each failed probe is an ERROR in the server's log: 120 an hour from the healthcheck alone. Production ran it already and is the only database. A scan sent a not-due skip for each of ~1,535 feeds into a broadcast of 1,024, so a page slow to read lagged and dropped events: 443 counted in five hours. Nothing listening uses them; the terminal and the log still get them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- The database server no longer logs an error each time the healthcheck runs, about 2,900 a day, from a clear-out of DeadAir's old copies that had already run (#196).
|
||||||
|
- A page left open no longer misses updates after a check for new episodes: the note that each feed not due was skipped, one per feed, filled the queue they share (#197).
|
||||||
- Checking for new episodes no longer waits up to 12 seconds each time on a clear-out of dead feeds from the Directory, which now runs once a day (#193).
|
- Checking for new episodes no longer waits up to 12 seconds each time on a clear-out of dead feeds from the Directory, which now runs once a day (#193).
|
||||||
- All Subscriptions, and every item list, comes up faster, most of all the first time after a quiet spell: it took half a second to two (#188).
|
- All Subscriptions, and every item list, comes up faster, most of all the first time after a quiet spell: it took half a second to two (#188).
|
||||||
|
|
||||||
|
|||||||
39
src/db.rs
39
src/db.rs
@@ -99,22 +99,6 @@ async fn create_missing(orm: &sea_orm::DatabaseConnection) -> Result<()> {
|
|||||||
let sql = "ALTER TABLE deadair_prefs ADD COLUMN pause_ms BIGINT";
|
let sql = "ALTER TABLE deadair_prefs ADD COLUMN pause_ms BIGINT";
|
||||||
orm.execute_unprepared(sql).await.with_context(|| sql.to_owned())?;
|
orm.execute_unprepared(sql).await.with_context(|| sql.to_owned())?;
|
||||||
}
|
}
|
||||||
// DeadAir made a shortened copy of each file until it played the original through a
|
|
||||||
// playlist (#181). The copies go with their table, and the files are looked at again, which
|
|
||||||
// takes seconds each. Found only while the table is there, so nothing is written after.
|
|
||||||
let copies = Statement::from_string(orm.get_database_backend(), "SELECT path FROM deadair WHERE path IS NOT NULL");
|
|
||||||
if let Ok(rows) = orm.query_all_raw(copies).await {
|
|
||||||
for r in rows {
|
|
||||||
// Only DeadAir's own files, `name.deadair.mp3`: anything else would be someone's original.
|
|
||||||
if let Ok(path) = r.try_get::<String>("", "path")
|
|
||||||
&& path.contains(".deadair.")
|
|
||||||
{
|
|
||||||
let _ = std::fs::remove_file(&path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// IF EXISTS: the healthcheck's `ipx status` may open the database at the same moment.
|
|
||||||
orm.execute_unprepared("DROP TABLE IF EXISTS deadair").await.context("dropping DeadAir's copies")?;
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2489,29 +2473,6 @@ pub fn now() -> i64 {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn deadair_drops_its_old_copies_once() {
|
|
||||||
let db = Db::memory().await.unwrap();
|
|
||||||
let dir = std::env::temp_dir().join(format!("ipx-deadair-old-{}", std::process::id()));
|
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
|
||||||
let (orig, copy) = (dir.join("a.mp3"), dir.join("a.deadair.mp3"));
|
|
||||||
std::fs::write(&orig, "original").unwrap();
|
|
||||||
std::fs::write(©, "copy").unwrap();
|
|
||||||
db.orm.execute_unprepared("CREATE TABLE deadair (enclosure_id BIGINT PRIMARY KEY, path TEXT)").await.unwrap();
|
|
||||||
for (id, path) in [(1, ©), (2, &orig)] {
|
|
||||||
db.orm
|
|
||||||
.execute_unprepared(&format!("INSERT INTO deadair VALUES ({id}, '{}')", path.display()))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
create_missing(&db.orm).await.unwrap();
|
|
||||||
assert!(!copy.exists(), "the copy goes");
|
|
||||||
assert!(orig.exists(), "a path that is not DeadAir's own is never removed");
|
|
||||||
assert!(db.orm.execute_unprepared("SELECT 1 FROM deadair").await.is_err(), "and the table");
|
|
||||||
create_missing(&db.orm).await.unwrap();
|
|
||||||
std::fs::remove_dir_all(&dir).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn deadair_looks_at_a_file_once_per_download_and_counts_an_episode_once() {
|
async fn deadair_looks_at_a_file_once_per_download_and_counts_an_episode_once() {
|
||||||
let db = Db::memory().await.unwrap();
|
let db = Db::memory().await.unwrap();
|
||||||
|
|||||||
18
src/ipc.rs
18
src/ipc.rs
@@ -144,7 +144,12 @@ impl Emitter {
|
|||||||
// line per feed per tick for "not due yet" would push everything worth reading
|
// line per feed per tick for "not due yet" would push everything worth reading
|
||||||
// out of the buffer within a few minutes.
|
// out of the buffer within a few minutes.
|
||||||
log_event(&e, self.tx.is_some());
|
log_event(&e, self.tx.is_some());
|
||||||
if let Some(tx) = &self.tx {
|
// A feed not due changes nothing a listener shows, and a scan has one for nearly every
|
||||||
|
// feed: 1,500 at once overflowed the channel's 1,024 for a page slow to read (#197).
|
||||||
|
let routine = matches!(&e, Event::FeedSkip { reason, .. } if reason.starts_with("not due"));
|
||||||
|
if let Some(tx) = &self.tx
|
||||||
|
&& !routine
|
||||||
|
{
|
||||||
// An error here only means nobody is listening yet.
|
// An error here only means nobody is listening yet.
|
||||||
let _ = tx.send(e.clone());
|
let _ = tx.send(e.clone());
|
||||||
}
|
}
|
||||||
@@ -362,6 +367,17 @@ pub async fn proxy(path: &Path, cmd: &Command) -> Result<()> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_feed_not_due_stays_off_the_live_updates() {
|
||||||
|
let (tx, mut rx) = broadcast::channel(4);
|
||||||
|
let out = Emitter::socket(tx, false);
|
||||||
|
for i in 0..10 {
|
||||||
|
out.emit(Event::FeedSkip { feed: format!("f{i}"), reason: "not due for 5m".into() });
|
||||||
|
}
|
||||||
|
out.emit(Event::FeedSkip { feed: "g".into(), reason: "not modified".into() });
|
||||||
|
assert!(matches!(rx.try_recv(), Ok(Event::FeedSkip { feed, .. }) if feed == "g"), "nothing missed");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_feed_that_moved_says_so_on_the_wire_and_in_words() {
|
fn a_feed_that_moved_says_so_on_the_wire_and_in_words() {
|
||||||
let e = Event::FeedMoved { feed: "x".into(), from: "http://a/f".into(), to: "https://a/f".into() };
|
let e = Event::FeedMoved { feed: "x".into(), from: "http://a/f".into(), to: "https://a/f".into() };
|
||||||
|
|||||||
@@ -1970,15 +1970,15 @@ async fn events(
|
|||||||
Sse::new(futures_util::StreamExt::flatten(stream)).keep_alive(axum::response::sse::KeepAlive::default())
|
Sse::new(futures_util::StreamExt::flatten(stream)).keep_alive(axum::response::sse::KeepAlive::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The feed an event changed what the list shows of: its counts, error or last check. Not the
|
/// The feed an event changed what the list shows of: its counts, error or last check. The
|
||||||
/// routine skip of a feed not due, dozens a minute that change nothing.
|
/// routine skip of a feed not due never reaches here (`Emitter::emit`).
|
||||||
fn changed_feed(ev: &Event) -> Option<&str> {
|
fn changed_feed(ev: &Event) -> Option<&str> {
|
||||||
match ev {
|
match ev {
|
||||||
Event::FeedDone { feed, .. }
|
Event::FeedDone { feed, .. }
|
||||||
| Event::FeedError { feed, .. }
|
| Event::FeedError { feed, .. }
|
||||||
| Event::FeedMoved { feed, .. }
|
| Event::FeedMoved { feed, .. }
|
||||||
| Event::DownloadDone { feed, .. } => Some(feed),
|
| Event::DownloadDone { feed, .. } => Some(feed),
|
||||||
Event::FeedSkip { feed, reason } if !reason.starts_with("not due") => Some(feed),
|
Event::FeedSkip { feed, .. } => Some(feed),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user