Skip to content

Commit fbfec95

Browse files
committed
Merge pull request #443
2 parents af2130a + 343c74a commit fbfec95

File tree

2 files changed

+193
-0
lines changed

2 files changed

+193
-0
lines changed

source/tutorial.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ Tutorials
1212
/tutorial/gridfs
1313
/tutorial/indexes
1414
/tutorial/example-data
15+
/tutorial/tailable-cursor

source/tutorial/tailable-cursor.txt

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
===============
2+
Tailable Cursor
3+
===============
4+
5+
.. default-domain:: mongodb
6+
7+
.. contents:: On this page
8+
:local:
9+
:backlinks: none
10+
:depth: 2
11+
:class: singlecol
12+
13+
Overview
14+
--------
15+
16+
When the driver executes a query or command (e.g.
17+
:manual:`aggregate </reference/command/aggregate>`), results from the operation
18+
are returned via a :php:`MongoDB\\Driver\\Cursor <class.mongodb-driver-cursor>`
19+
object. The Cursor class implements PHP's :php:`Traversable <traversable>`
20+
interface, which allows it to be iterated with ``foreach`` and interface with
21+
any PHP functions that work with :php:`iterables <types.iterable>`. Similar to
22+
result objects in other database drivers, cursors in MongoDB only support
23+
forward iteration, which means they cannot be rewound or used with ``foreach``
24+
multiple times.
25+
26+
:manual:`Tailable cursors </core/tailable-cursors>` are a special type of
27+
MongoDB cursor that allows the client to read some results and then wait until
28+
more documents become available. These cursors are primarily used with
29+
:manual:`Capped Collections </core/capped-collections>` and
30+
:manual:`Change Streams </changeStreams>`.
31+
32+
While normal cursors can be iterated once with ``foreach``, that approach will
33+
not work with tailable cursors. When ``foreach`` is used with a tailable cursor,
34+
the loop will stop upon reaching the end of the initial result set. Attempting
35+
to continue iteration on the cursor with a second ``foreach`` would throw an
36+
exception, since PHP attempts to rewind the cursor.
37+
38+
In order to continuously read from a tailable cursor, we will need to wrap the
39+
Cursor object with an :php:`IteratorIterator <iteratoriterator>`. This will
40+
allow us to directly control the cursor's iteration (e.g. call ``next()``),
41+
avoid inadvertently rewinding the cursor, and decide when to wait for new
42+
results or stop iteration entirely.
43+
44+
Wrapping a Normal Cursor
45+
------------------------
46+
47+
Before looking at how a tailable cursor can be wrapped with
48+
:php:`IteratorIterator <iteratoriterator>`, we'll start by examining how the
49+
class interacts with a normal cursor.
50+
51+
The following example finds five restaurants and uses ``foreach`` to view the
52+
results:
53+
54+
.. code-block:: php
55+
56+
<?php
57+
58+
$collection = (new MongoDB\Client)->test->restaurants;
59+
60+
$cursor = $collection->find([], ['limit' => 5]);
61+
62+
foreach ($cursor as $document) {
63+
var_dump($document);
64+
}
65+
66+
While this example is quite concise, there is actually quite a bit going on. The
67+
``foreach`` construct begins by rewinding the iterable (``$cursor`` in this
68+
case). It then checks if the current position is valid. If the position is not
69+
valid, the loop ends. Otherwise, the current key and value are accessed
70+
accordingly and the loop body is executed. Assuming a :php:`break <break>` has
71+
not occurred, the iterator then advances to the next position, control jumps
72+
back to the validity check, and the loop continues.
73+
74+
With the inner workings of ``foreach`` under our belt, we can now translate the
75+
preceding example to use IteratorIterator:
76+
77+
.. code-block:: php
78+
79+
<?php
80+
81+
$collection = (new MongoDB\Client)->test->restaurants;
82+
83+
$cursor = $collection->find([], ['limit' => 5]);
84+
85+
$iterator = new IteratorIterator($cursor);
86+
87+
$iterator->rewind();
88+
89+
while ($iterator->valid()) {
90+
$document = $iterator->current();
91+
var_dump($document);
92+
$iterator->next();
93+
}
94+
95+
.. note::
96+
97+
Calling ``$iterator->next()`` after the ``while`` loop naturally ends would
98+
throw an exception, since all results on the cursor have been exhausted.
99+
100+
The purpose of this example is simply to demonstrate the functional equivalence
101+
between ``foreach`` and manual iteration with PHP's :php:`Iterator <iterator>`
102+
API. For normal cursors, there is little reason to use IteratorIterator instead
103+
of a concise ``foreach`` loop.
104+
105+
Wrapping a Tailable Cursor
106+
--------------------------
107+
108+
In order to demonstrate a tailable cursor in action, we'll need two scripts: a
109+
"producer" and a "consumer". The producer script will create a new capped
110+
collection using :phpmethod:`MongoDB\\Database::createCollection()` and proceed
111+
to insert a new document into that collection each second.
112+
113+
.. code-block:: php
114+
115+
<?php
116+
117+
$database = (new MongoDB\Client)->test;
118+
119+
$database->createCollection('capped', [
120+
'capped' => true,
121+
'size' => 16777216,
122+
]);
123+
124+
$collection = $database->selectCollection('capped');
125+
126+
while (true) {
127+
$collection->insertOne(['createdAt' => new MongoDB\BSON\UTCDateTime()]);
128+
sleep(1);
129+
}
130+
131+
With the producer script still running, we will now execute a consumer script to
132+
read the inserted documents using a tailable cursor, indicated by the
133+
``cursorType`` option to :phpmethod:`MongoDB\\Collection::find()`. We'll start
134+
by using ``foreach`` to illustrate its shortcomings:
135+
136+
.. code-block:: php
137+
138+
<?php
139+
140+
$collection = (new MongoDB\Client)->test->capped;
141+
142+
$cursor = $collection->find([], [
143+
'cursorType' => MongoDB\Operation\Find::TAILABLE_AWAIT,
144+
'maxAwaitTimeMS' => 100,
145+
]);
146+
147+
foreach ($cursor as $document) {
148+
printf("Consumed document created at: %s\n", $document->createdAt);
149+
}
150+
151+
If you execute this consumer script, you'll notice that it quickly exhausts all
152+
results in the capped collection and then terminates. We cannot add a second
153+
``foreach``, as that would throw an exception when attempting to rewind the
154+
cursor. This is a ripe use case for directly controlling the iteration process
155+
using :php:`IteratorIterator <iteratoriterator>`.
156+
157+
.. code-block:: php
158+
159+
<?php
160+
161+
$collection = (new MongoDB\Client)->test->capped;
162+
163+
$cursor = $collection->find([], [
164+
'cursorType' => MongoDB\Operation\Find::TAILABLE_AWAIT,
165+
'maxAwaitTimeMS' => 100,
166+
]);
167+
168+
$iterator = new IteratorIterator($cursor);
169+
170+
$iterator->rewind();
171+
172+
while (true) {
173+
if ($iterator->valid()) {
174+
$document = $iterator->current();
175+
printf("Consumed document created at: %s\n", $document->createdAt);
176+
}
177+
178+
$iterator->next();
179+
}
180+
}
181+
182+
Much like the ``foreach`` example, this version on the consumer script will
183+
start by quickly printing all results in the capped collection; however, it will
184+
not terminate upon reaching the end of the initial result set. Since we're
185+
working with a tailable cursor, calling ``next()`` will block and wait for
186+
additional results rather than throw an exception. We will also use ``valid()``
187+
to check if there is actually data available to read at each step.
188+
189+
Since we've elected to use a ``TAILABLE_AWAIT`` cursor, the server will delay
190+
its response to the driver for a set amount of time. In this example, we've
191+
requested that the server block for approximately 100 milliseconds by specifying
192+
the ``maxAwaitTimeMS`` option to :phpmethod:`MongoDB\\Collection::find()`.

0 commit comments

Comments
 (0)