Skip to content

Commit

Permalink
Updated CacheableCursor to only store the first 100 documents in cach…
Browse files Browse the repository at this point in the history
…e return a real database connection if more than 100 documents are being accessed.
  • Loading branch information
Zizaco committed Jul 25, 2016
1 parent d6c2f76 commit 84d52ac
Show file tree
Hide file tree
Showing 2 changed files with 148 additions and 5 deletions.
103 changes: 99 additions & 4 deletions src/Mongolid/Cursor/CacheableCursor.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,27 @@ class CacheableCursor extends Cursor
*/
protected $documents;

/**
* Limit of the query. It is stored because when caching the documents
* the DOCUMENT_LIMIT const will be used.
*
* @var ìnteger
*/
protected $originalLimit;

/**
* Means that the CacheableCursor is wapping the original cursor and not
* reading from Cache anymore.
*
* @var boolean
*/
protected $ignoreCache = false;

/**
* Limits the amount of documents that will be cached for performance reasons.
*/
const DOCUMENT_LIMIT = 100;

/**
* Actually returns a Traversable object with the DriverCursor within.
* If it does not exists yet, create it using the $collection, $command and
Expand All @@ -37,29 +58,39 @@ class CacheableCursor extends Cursor
*/
protected function getCursor(): Traversable
{
// Returns original (non-cached) cursor
if ($this->ignoreCache || $this->position >= self::DOCUMENT_LIMIT) {
return $this->getOriginalCursor();
}

// Returns cached set of documents
if ($this->documents) {
return $this->documents;
}

// Check if there is a cached set of documents
$cacheComponent = Ioc::make(CacheComponentInterface::class);
$cacheKey = $this->generateCacheKey();

if ($this->documents = $cacheComponent->get($cacheKey, null)) {
return $this->documents = new ArrayIterator($this->documents);
}

// Stores the documents within the object.
// Stores the original "limit" clause of the query
$this->storeOriginalLimit();

// Stores the documents within the object and cache then for later use
$this->documents = [];
foreach (parent::getCursor() as $document) {
$this->documents[] = $document;
}

$cacheComponent->put($cacheKey, $this->documents, 0.6);

// Drops the unserializable DriverCursor. In order to make the
// CacheableCursor object serializable.
unset($this->cursor);
// Drops the unserializable DriverCursor.
$this->cursor = null;

// Return the documents iterator
return $this->documents = new ArrayIterator($this->documents);
}

Expand All @@ -79,4 +110,68 @@ protected function generateCacheKey(): string
md5($serializer->serialize($this->params))
);
}

/**
* Stores the original "limit" clause of the query.
*
* @return void
*/
protected function storeOriginalLimit()
{
if (isset($this->params[1]['limit'])) {
$this->originalLimit = $this->params[1]['limit'];
}

if ($this->originalLimit > self::DOCUMENT_LIMIT) {
$this->limit(self::DOCUMENT_LIMIT);
}
}

/**
* Gets the limit clause of the query if any.
*
* @return mixed Int or null
*/
protected function getLimit()
{
return $this->originalLimit ?: ($this->params[1]['limit'] ?? null);
}

/**
* Returns the DriverCursor considering the documents that have already
* been retrieved from cache
*
* @return Traversable
*/
protected function getOriginalCursor(): Traversable
{
if ($this->ignoreCache) {
return parent::getCursor();
}

if ($this->getLimit()) {
$this->params[1]['limit'] = $this->getLimit() - $this->position;
}

$skipped = $this->params[1]['skip'] ?? 0;

$this->skip($skipped + $this->position - 1);

$this->ignoreCache = true;

return $this->getOriginalCursor();
}

/**
* Serializes this object. Drops the unserializable DriverCursor. In order
* to make the CacheableCursor object serializable.
*
* @return string Serialized object.
*/
public function serialize()
{
$this->documents = $this->cursor = null;

return parent::serialize();
}
}
50 changes: 49 additions & 1 deletion tests/Mongolid/Cursor/CacheableCursorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Mongolid\Cursor;

use ArrayIterator;
use IteratorIterator;
use Mockery as m;
use MongoDB\Collection;
use Mongolid\Container\Ioc;
Expand Down Expand Up @@ -65,7 +66,7 @@ public function testShouldGetCursorFromDatabaseAndCacheForLater()
{
// Arrange
$documentsFromDb = [['name' => 'joe'], ['name' => 'doe']];
$cursor = $this->getCachableCursor();
$cursor = $this->getCachableCursor()->limit(150);
$cacheComponent = m::mock(CacheComponentInterface::class);
$rawCollection = m::mock();

Expand All @@ -86,6 +87,7 @@ public function testShouldGetCursorFromDatabaseAndCacheForLater()
->andReturn(null);

$rawCollection->shouldReceive('find')
->with([], ['limit' => 100])
->andReturn(new ArrayIterator($documentsFromDb));

$cacheComponent->shouldReceive('put')
Expand All @@ -99,6 +101,50 @@ public function testShouldGetCursorFromDatabaseAndCacheForLater()
);
}

public function testShouldGetOriginalCursorFromDatabaseAfterTheDocumentLimit()
{
// Arrange
$documentsFromDb = [['name' => 'joe'], ['name' => 'doe']];
$cursor = $this->getCachableCursor()->limit(150);
$cacheComponent = m::mock(CacheComponentInterface::class);
$rawCollection = m::mock();

$this->setProtected(
$cursor,
'position',
CacheableCursor::DOCUMENT_LIMIT + 1
);

$this->setProtected(
$cursor,
'collection',
$rawCollection
);

// Act
$cursor->shouldReceive('generateCacheKey')
->never();

Ioc::instance(CacheComponentInterface::class, $cacheComponent);

$cacheComponent->shouldReceive('get')
->with('find:collection:123', null)
->never();

$rawCollection->shouldReceive('find')
->with([], ['skip' => CacheableCursor::DOCUMENT_LIMIT, 'limit' => 49])
->andReturn(new ArrayIterator($documentsFromDb));

$cacheComponent->shouldReceive('put')
->never();

// Assert
$this->assertEquals(
new IteratorIterator(new ArrayIterator($documentsFromDb)),
$this->callProtected($cursor, 'getCursor')
);
}

public function testShouldGenerateUniqueCacheKey()
{
// Arrange
Expand Down Expand Up @@ -138,6 +184,8 @@ protected function getCachableCursor(
$collection = m::mock(Collection::class);
$collection->shouldReceive('getNamespace')
->andReturn('my_db.my_collection');
$collection->shouldReceive('getCollectionName')
->andReturn('my_collection');
}

$mock = m::mock(
Expand Down

0 comments on commit 84d52ac

Please sign in to comment.