diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 14bd394..7b7b4d4 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -8,10 +8,6 @@ . - diff --git a/src/DatabaseL2.php b/src/DatabaseL2.php index 95c4f83..e74176d 100644 --- a/src/DatabaseL2.php +++ b/src/DatabaseL2.php @@ -33,7 +33,7 @@ protected function prefixTable($base_name) return $this->table_prefix . $base_name; } - protected function cleanUp() + protected function pruneReplacedEvents() { // No deletions, nothing to do. if (empty($this->address_deletion_patterns)) { @@ -64,7 +64,47 @@ protected function cleanUp() public function __destruct() { - $this->cleanUp(); + $this->pruneReplacedEvents(); + } + + public function countGarbage() + { + try { + $sth = $this->dbh->prepare('SELECT COUNT(*) garbage FROM ' . $this->prefixTable('lcache_events') . ' WHERE "expiration" < :now'); + $sth->bindValue(':now', REQUEST_TIME, \PDO::PARAM_INT); + $sth->execute(); + } catch (\PDOException $e) { + $this->logSchemaIssueOrRethrow('Failed to count garbage', $e); + return null; + } + + $count = $sth->fetchObject(); + return intval($count->garbage); + } + + public function collectGarbage($item_limit = null) + { + $sql = 'DELETE FROM ' . $this->prefixTable('lcache_events') . ' WHERE "expiration" < :now'; + // This is not supported by standard SQLite. + // @codeCoverageIgnoreStart + if (!is_null($item_limit)) { + $sql .= ' ORDER BY "event_id" LIMIT :item_limit'; + } + // @codeCoverageIgnoreEnd + try { + $sth = $this->dbh->prepare($sql); + $sth->bindValue(':now', REQUEST_TIME, \PDO::PARAM_INT); + // This is not supported by standard SQLite. + // @codeCoverageIgnoreStart + if (!is_null($item_limit)) { + $sth->bindValue(':item_limit', $item_limit, \PDO::PARAM_INT); + } + // @codeCoverageIgnoreEnd + $sth->execute(); + } catch (\PDOException $e) { + $this->logSchemaIssueOrRethrow('Failed to collect garbage', $e); + return false; + } } protected function queueDeletion(Address $address) diff --git a/src/Integrated.php b/src/Integrated.php index 4fe9035..d479422 100644 --- a/src/Integrated.php +++ b/src/Integrated.php @@ -110,4 +110,9 @@ public function getPool() { return $this->l1->getPool(); } + + public function collectGarbage($item_limit = null) + { + return $this->l2->collectGarbage($item_limit); + } } diff --git a/src/L2.php b/src/L2.php index 8ac53d1..336f946 100644 --- a/src/L2.php +++ b/src/L2.php @@ -9,4 +9,6 @@ abstract public function set($pool, Address $address, $value = null, $ttl = null abstract public function delete($pool, Address $address); abstract public function deleteTag(L1 $l1, $tag); abstract public function getAddressesForTag($tag); + abstract public function collectGarbage($item_limit = null); + abstract public function countGarbage(); } diff --git a/src/StaticL2.php b/src/StaticL2.php index 9902c82..dbf26a3 100644 --- a/src/StaticL2.php +++ b/src/StaticL2.php @@ -19,6 +19,31 @@ public function __construct() $this->tags = []; } + public function countGarbage() + { + $garbage = 0; + foreach ($this->events as $event_id => $entry) { + if ($entry->expiration < REQUEST_TIME) { + $garbage++; + } + } + return $garbage; + } + + public function collectGarbage($item_limit = null) + { + $deleted = 0; + foreach ($this->events as $event_id => $entry) { + if ($entry->expiration < REQUEST_TIME) { + unset($this->events[$event_id]); + $deleted++; + } + if ($deleted === $item_limit) { + break; + } + } + } + // Returns an LCache\Entry public function getEntry(Address $address) { diff --git a/tests/LCacheTest.php b/tests/LCacheTest.php index 79259bd..16b1de9 100644 --- a/tests/LCacheTest.php +++ b/tests/LCacheTest.php @@ -424,6 +424,11 @@ public function testBrokenDatabaseFallback() // Try applying events to an uninitialized L1. $this->assertNull($l2->applyEvents(new StaticL1())); + + // Try garbage collection routines. + $pool->collectGarbage(); + $count = $l2->countGarbage(); + $this->assertNull($count); } public function testDatabaseL2SyncWithNoWrites() @@ -638,17 +643,54 @@ public function performFailedUnserializationOnSyncTest($l2) $this->assertEquals(1, $l1->getMisses()); } - public function testDatabaseL2FailedUnserializationOnSyncTest() { + public function testDatabaseL2FailedUnserializationOnSync() + { $this->createSchema(); $l2 = new DatabaseL2($this->dbh); $this->performFailedUnserializationOnSyncTest($l2); } - public function testStaticL2FailedUnserializationOnSyncTest() { + public function testStaticL2FailedUnserializationOnSync() + { $l2 = new StaticL2(); $this->performFailedUnserializationOnSyncTest($l2); } + public function performGarbageCollectionTest($l2) + { + $pool = new Integrated(new StaticL1(), $l2); + $myaddr = new Address('mybin', 'mykey'); + $this->assertEquals(0, $l2->countGarbage()); + $pool->set($myaddr, 'myvalue', -1); + $this->assertEquals(1, $l2->countGarbage()); + $pool->collectGarbage(); + $this->assertEquals(0, $l2->countGarbage()); + } + + public function testDatabaseL2GarbageCollection() + { + $this->createSchema(); + $l2 = new DatabaseL2($this->dbh); + $this->performGarbageCollectionTest($l2); + } + + public function testStaticL2GarbageCollection() + { + $l2 = new StaticL2(); + $this->performGarbageCollectionTest($l2); + + // Test item limits. + $pool = new Integrated(new StaticL1(), $l2); + $myaddr2 = new Address('mybin', 'mykey2'); + $myaddr3 = new Address('mybin', 'mykey3'); + $pool->collectGarbage(); + $pool->set($myaddr2, 'myvalue', -1); + $pool->set($myaddr3, 'myvalue', -1); + $this->assertEquals(2, $l2->countGarbage()); + $pool->collectGarbage(1); + $this->assertEquals(1, $l2->countGarbage()); + } + /** * @return PHPUnit_Extensions_Database_DataSet_IDataSet */ @@ -657,3 +699,4 @@ protected function getDataSet() return new \PHPUnit_Extensions_Database_DataSet_DefaultDataSet(); } } +