diff --git a/CHANGELOG b/CHANGELOG index 8e4988d5..4cdc00b4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -13,6 +13,9 @@ Bug fixes: - Sort Tiny Tiny RSS special feeds according to special ordering - Invalidate sessions when passwords are changed +Changes: +- Perform regular database maintenance to improve long-term performance + Version 0.7.1 (2019-03-25) ========================== diff --git a/lib/Database.php b/lib/Database.php index 10e66eb6..366d84d0 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -110,6 +110,11 @@ class Database { return $this->db->charsetAcceptable(); } + /** Performs maintenance on the database to ensure good performance */ + public function driverMaintenance(): bool { + return $this->db->maintenance(); + } + /** Computes the column and value text of an SQL "SET" clause, validating arbitrary input against a whitelist * * Returns an indexed array containing the clause text, an array of types, and another array of values @@ -1788,10 +1793,11 @@ class Database { $limitUnread = Date::sub(Arsse::$conf->purgeArticlesUnread); } $feeds = $this->db->query("SELECT id, size from arsse_feeds")->getAll(); + $deleted = 0; foreach ($feeds as $feed) { - $query->run($feed['id'], $feed['size'], $feed['id'], $limitUnread, $limitRead); + $deleted += $query->run($feed['id'], $feed['size'], $feed['id'], $limitUnread, $limitRead)->changes(); } - return true; + return (bool) $deleted; } /** Ensures the specified article exists and raises an exception otherwise diff --git a/lib/Db/Driver.php b/lib/Db/Driver.php index 7f04dc6c..a456fba3 100644 --- a/lib/Db/Driver.php +++ b/lib/Db/Driver.php @@ -82,4 +82,10 @@ interface Driver { * This functionality should be avoided in favour of using statement parameters whenever possible */ public function literalString(string $str): string; + + /** Performs implementation-specific database maintenance to ensure good performance + * + * This should be restricted to quick maintenance; in SQLite terms it might include ANALYZE, but not VACUUM + */ + public function maintenance(): bool; } diff --git a/lib/Db/MySQL/Driver.php b/lib/Db/MySQL/Driver.php index cec575b1..bb9cac82 100644 --- a/lib/Db/MySQL/Driver.php +++ b/lib/Db/MySQL/Driver.php @@ -216,4 +216,17 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { public function literalString(string $str): string { return "'".$this->db->real_escape_string($str)."'"; } + + public function maintenance(): bool { + // with MySQL each table must be analyzed separately, so we first have to get a list of tables + foreach ($this->query("SHOW TABLES like 'arsse\\_%'") as $table) { + $table = array_pop($table); + if (!preg_match("/^arsse_[a-z_]+$/", $table)) { + // table is not one of ours + continue; // @codeCoverageIgnore + } + $this->query("ANALYZE TABLE $table"); + } + return true; + } } diff --git a/lib/Db/PostgreSQL/Driver.php b/lib/Db/PostgreSQL/Driver.php index 12ad8fcd..7550393b 100644 --- a/lib/Db/PostgreSQL/Driver.php +++ b/lib/Db/PostgreSQL/Driver.php @@ -225,4 +225,10 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { public function literalString(string $str): string { return pg_escape_literal($this->db, $str); } + + public function maintenance(): bool { + // analyze the database + $this->exec("ANALYZE"); + return true; + } } diff --git a/lib/Db/SQLite3/Driver.php b/lib/Db/SQLite3/Driver.php index 96e345fa..6cf290f5 100644 --- a/lib/Db/SQLite3/Driver.php +++ b/lib/Db/SQLite3/Driver.php @@ -188,4 +188,10 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { public function literalString(string $str): string { return "'".\SQLite3::escapeString($str)."'"; } + + public function maintenance(): bool { + // analyze the database then checkpoint and truncate the write-ahead log + $this->exec("ANALYZE; PRAGMA wal_checkpoint(truncate)"); + return true; + } } diff --git a/lib/Service.php b/lib/Service.php index bc752aef..93d4e9ba 100644 --- a/lib/Service.php +++ b/lib/Service.php @@ -92,7 +92,12 @@ class Service { } public static function cleanupPost(): bool { - // delete old articles, according to configured threasholds - return Arsse::$db->articleCleanup(); + // delete old articles, according to configured thresholds + $deleted = Arsse::$db->articleCleanup(); + // if any articles were deleted, perform database maintenance + if ($deleted) { + Arsse::$db->driverMaintenance(); + } + return true; } } diff --git a/tests/cases/Database/SeriesMiscellany.php b/tests/cases/Database/SeriesMiscellany.php index 00803567..a7591bbe 100644 --- a/tests/cases/Database/SeriesMiscellany.php +++ b/tests/cases/Database/SeriesMiscellany.php @@ -44,4 +44,8 @@ trait SeriesMiscellany { public function testCheckCharacterSetAcceptability() { $this->assertInternalType("bool", Arsse::$db->driverCharsetAcceptable()); } + + public function testPerformMaintenance() { + $this->assertTrue(Arsse::$db->driverMaintenance()); + } } diff --git a/tests/cases/Db/BaseDriver.php b/tests/cases/Db/BaseDriver.php index 74ef7c97..677339b5 100644 --- a/tests/cases/Db/BaseDriver.php +++ b/tests/cases/Db/BaseDriver.php @@ -382,4 +382,9 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest { public function testProduceAStringLiteral() { $this->assertSame("'It''s a string!'", $this->drv->literalString("It's a string!")); } + + public function testPerformMaintenance() { + // this performs maintenance in the absence of tables; see BaseUpdate.php for another test with tables + $this->assertTrue($this->drv->maintenance()); + } } diff --git a/tests/cases/Db/BaseUpdate.php b/tests/cases/Db/BaseUpdate.php index 27806846..e9bc10d0 100644 --- a/tests/cases/Db/BaseUpdate.php +++ b/tests/cases/Db/BaseUpdate.php @@ -130,4 +130,9 @@ class BaseUpdate extends \JKingWeb\Arsse\Test\AbstractTest { $this->assertException("updateTooNew", "Db"); $this->drv->schemaUpdate(-1, $this->base); } + + public function testPerformMaintenance() { + $this->drv->schemaUpdate(Database::SCHEMA_VERSION); + $this->assertTrue($this->drv->maintenance()); + } }