Skip to content

Commit b6c7f11

Browse files
committed
PHPLIB-451: Internal TailableCursorIterator class
This can be used to ensure rewinding is a NOP (i.e. getMore will not be executed) for tailable cursors with an empty firstBatch.
1 parent 746c0f0 commit b6c7f11

File tree

2 files changed

+165
-0
lines changed

2 files changed

+165
-0
lines changed

src/Model/TailableCursorIterator.php

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
/*
3+
* Copyright 2019 MongoDB, Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
namespace MongoDB\Model;
19+
20+
use MongoDB\Driver\Cursor;
21+
use IteratorIterator;
22+
23+
/**
24+
* Iterator for tailable cursors.
25+
*
26+
* This iterator may be used to wrap a tailable cursor. By indicating whether
27+
* the cursor's first batch of results is empty, this iterator can NOP initial
28+
* calls to rewind() and prevent it from executing a getMore command.
29+
*
30+
* @internal
31+
*/
32+
class TailableCursorIterator extends IteratorIterator
33+
{
34+
private $isRewindNop;
35+
36+
/**
37+
* Constructor.
38+
*
39+
* @internal
40+
* @param Cursor $cursor
41+
* @param boolean $isFirstBatchIsEmpty
42+
*/
43+
public function __construct(Cursor $cursor, $isFirstBatchIsEmpty)
44+
{
45+
parent::__construct($cursor);
46+
$this->isRewindNop = $isFirstBatchIsEmpty;
47+
}
48+
49+
/**
50+
* @see https://php.net/iteratoriterator.rewind
51+
* @return void
52+
*/
53+
public function next()
54+
{
55+
try {
56+
parent::next();
57+
} finally {
58+
/* If the cursor ever advances to a valid position, do not prevent
59+
* future attempts to rewind the cursor. This will allow the driver
60+
* to throw a LogicException if the cursor has been advanced past
61+
* its first element. */
62+
if ($this->valid()) {
63+
$this->isRewindNop = false;
64+
}
65+
}
66+
}
67+
68+
/**
69+
* @see https://php.net/iteratoriterator.rewind
70+
* @return void
71+
*/
72+
public function rewind()
73+
{
74+
if ($this->isRewindNop) {
75+
return;
76+
}
77+
78+
parent::rewind();
79+
}
80+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
namespace MongoDB\Tests\Model;
4+
5+
use MongoDB\Collection;
6+
use MongoDB\Driver\Exception\LogicException;
7+
use MongoDB\Model\TailableCursorIterator;
8+
use MongoDB\Operation\Find;
9+
use MongoDB\Operation\CreateCollection;
10+
use MongoDB\Operation\DropCollection;
11+
use MongoDB\Tests\CommandObserver;
12+
use MongoDB\Tests\FunctionalTestCase;
13+
14+
class TailableCursorIteratorTest extends FunctionalTestCase
15+
{
16+
private $collection;
17+
18+
public function setUp()
19+
{
20+
parent::setUp();
21+
22+
$operation = new DropCollection($this->getDatabaseName(), $this->getCollectionName());
23+
$operation->execute($this->getPrimaryServer());
24+
25+
$operation = new CreateCollection($this->getDatabaseName(), $this->getCollectionName(), ['capped' => true, 'size' => 8192]);
26+
$operation->execute($this->getPrimaryServer());
27+
28+
$this->collection = new Collection($this->manager, $this->getDatabaseName(), $this->getCollectionName());
29+
}
30+
31+
public function testFirstBatchIsEmpty()
32+
{
33+
$this->collection->insertOne(['x' => 1]);
34+
35+
$cursor = $this->collection->find(['x' => ['$gt' => 1]], ['cursorType' => Find::TAILABLE]);
36+
$iterator = new TailableCursorIterator($cursor, true);
37+
38+
$this->assertNoCommandExecuted(function() use ($iterator) { $iterator->rewind(); });
39+
$this->assertFalse($iterator->valid());
40+
41+
$this->collection->insertOne(['x' => 2]);
42+
43+
$iterator->next();
44+
$this->assertTrue($iterator->valid());
45+
$this->assertMatchesDocument(['x' => 2], $iterator->current());
46+
47+
$this->expectException(LogicException::class);
48+
$iterator->rewind();
49+
}
50+
51+
public function testFirstBatchIsNotEmpty()
52+
{
53+
$this->collection->insertOne(['x' => 1]);
54+
55+
$cursor = $this->collection->find([], ['cursorType' => Find::TAILABLE]);
56+
$iterator = new TailableCursorIterator($cursor, false);
57+
58+
$this->assertNoCommandExecuted(function() use ($iterator) { $iterator->rewind(); });
59+
$this->assertTrue($iterator->valid());
60+
$this->assertMatchesDocument(['x' => 1], $iterator->current());
61+
62+
$this->collection->insertOne(['x' => 2]);
63+
64+
$iterator->next();
65+
$this->assertTrue($iterator->valid());
66+
$this->assertMatchesDocument(['x' => 2], $iterator->current());
67+
68+
$this->expectException(LogicException::class);
69+
$iterator->rewind();
70+
}
71+
72+
private function assertNoCommandExecuted(callable $callable)
73+
{
74+
$commands = [];
75+
76+
(new CommandObserver)->observe(
77+
$callable,
78+
function(array $event) use (&$commands) {
79+
$this->fail(sprintf('"%s" command was executed', $event['started']->getCommandName()));
80+
}
81+
);
82+
83+
$this->assertEmpty($commands);
84+
}
85+
}

0 commit comments

Comments
 (0)