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();
}
}
+