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
|
||||
|
||||
- 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).
|
||||
- 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";
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -2489,29 +2473,6 @@ pub fn now() -> i64 {
|
||||
mod tests {
|
||||
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]
|
||||
async fn deadair_looks_at_a_file_once_per_download_and_counts_an_episode_once() {
|
||||
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
|
||||
// out of the buffer within a few minutes.
|
||||
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.
|
||||
let _ = tx.send(e.clone());
|
||||
}
|
||||
@@ -362,6 +367,17 @@ pub async fn proxy(path: &Path, cmd: &Command) -> Result<()> {
|
||||
mod tests {
|
||||
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]
|
||||
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() };
|
||||
|
||||
@@ -1970,15 +1970,15 @@ async fn events(
|
||||
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
|
||||
/// routine skip of a feed not due, dozens a minute that change nothing.
|
||||
/// The feed an event changed what the list shows of: its counts, error or last check. The
|
||||
/// routine skip of a feed not due never reaches here (`Emitter::emit`).
|
||||
fn changed_feed(ev: &Event) -> Option<&str> {
|
||||
match ev {
|
||||
Event::FeedDone { feed, .. }
|
||||
| Event::FeedError { feed, .. }
|
||||
| Event::FeedMoved { feed, .. }
|
||||
| Event::DownloadDone { feed, .. } => Some(feed),
|
||||
Event::FeedSkip { feed, reason } if !reason.starts_with("not due") => Some(feed),
|
||||
Event::FeedSkip { feed, .. } => Some(feed),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user