tailable-cursor.txt 6.65 KB
Newer Older
1 2
.. _php-tailable-cursor:

3 4 5
=========================
Tailable Cursor Iteration
=========================
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193

.. default-domain:: mongodb

.. contents:: On this page
   :local:
   :backlinks: none
   :depth: 2
   :class: singlecol

Overview
--------

When the driver executes a query or command (e.g.
:manual:`aggregate </reference/command/aggregate>`), results from the operation
are returned via a :php:`MongoDB\\Driver\\Cursor <class.mongodb-driver-cursor>`
object. The Cursor class implements PHP's :php:`Traversable <traversable>`
interface, which allows it to be iterated with ``foreach`` and interface with
any PHP functions that work with :php:`iterables <types.iterable>`. Similar to
result objects in other database drivers, cursors in MongoDB only support
forward iteration, which means they cannot be rewound or used with ``foreach``
multiple times.

:manual:`Tailable cursors </core/tailable-cursors>` are a special type of
MongoDB cursor that allows the client to read some results and then wait until
more documents become available. These cursors are primarily used with
:manual:`Capped Collections </core/capped-collections>` and
:manual:`Change Streams </changeStreams>`.

While normal cursors can be iterated once with ``foreach``, that approach will
not work with tailable cursors. When ``foreach`` is used with a tailable cursor,
the loop will stop upon reaching the end of the initial result set. Attempting
to continue iteration on the cursor with a second ``foreach`` would throw an
exception, since PHP attempts to rewind the cursor.

In order to continuously read from a tailable cursor, we will need to wrap the
Cursor object with an :php:`IteratorIterator <iteratoriterator>`. This will
allow us to directly control the cursor's iteration (e.g. call ``next()``),
avoid inadvertently rewinding the cursor, and decide when to wait for new
results or stop iteration entirely.

Wrapping a Normal Cursor
------------------------

Before looking at how a tailable cursor can be wrapped with
:php:`IteratorIterator <iteratoriterator>`, we'll start by examining how the
class interacts with a normal cursor.

The following example finds five restaurants and uses ``foreach`` to view the
results:

.. code-block:: php

   <?php

   $collection = (new MongoDB\Client)->test->restaurants;

   $cursor = $collection->find([], ['limit' => 5]);

   foreach ($cursor as $document) {
      var_dump($document);
   }

While this example is quite concise, there is actually quite a bit going on. The
``foreach`` construct begins by rewinding the iterable (``$cursor`` in this
case). It then checks if the current position is valid. If the position is not
valid, the loop ends. Otherwise, the current key and value are accessed
accordingly and the loop body is executed. Assuming a :php:`break <break>` has
not occurred, the iterator then advances to the next position, control jumps
back to the validity check, and the loop continues.

With the inner workings of ``foreach`` under our belt, we can now translate the
preceding example to use IteratorIterator:

.. code-block:: php

   <?php

   $collection = (new MongoDB\Client)->test->restaurants;

   $cursor = $collection->find([], ['limit' => 5]);

   $iterator = new IteratorIterator($cursor);

   $iterator->rewind();

   while ($iterator->valid()) {
      $document = $iterator->current();
      var_dump($document);
      $iterator->next();
   }

.. note::

   Calling ``$iterator->next()`` after the ``while`` loop naturally ends would
   throw an exception, since all results on the cursor have been exhausted.

The purpose of this example is simply to demonstrate the functional equivalence
between ``foreach`` and manual iteration with PHP's :php:`Iterator <iterator>`
API. For normal cursors, there is little reason to use IteratorIterator instead
of a concise ``foreach`` loop.

Wrapping a Tailable Cursor
--------------------------

In order to demonstrate a tailable cursor in action, we'll need two scripts: a
"producer" and a "consumer". The producer script will create a new capped
collection using :phpmethod:`MongoDB\\Database::createCollection()` and proceed
to insert a new document into that collection each second.

.. code-block:: php

   <?php

   $database = (new MongoDB\Client)->test;

   $database->createCollection('capped', [
       'capped' => true,
       'size' => 16777216,
   ]);

   $collection = $database->selectCollection('capped');

   while (true) {
       $collection->insertOne(['createdAt' => new MongoDB\BSON\UTCDateTime()]);
       sleep(1);
   }

With the producer script still running, we will now execute a consumer script to
read the inserted documents using a tailable cursor, indicated by the
``cursorType`` option to :phpmethod:`MongoDB\\Collection::find()`. We'll start
by using ``foreach`` to illustrate its shortcomings:

.. code-block:: php

   <?php

   $collection = (new MongoDB\Client)->test->capped;

   $cursor = $collection->find([], [
       'cursorType' => MongoDB\Operation\Find::TAILABLE_AWAIT,
       'maxAwaitTimeMS' => 100,
   ]);

   foreach ($cursor as $document) {
       printf("Consumed document created at: %s\n", $document->createdAt);
   }

If you execute this consumer script, you'll notice that it quickly exhausts all
results in the capped collection and then terminates. We cannot add a second
``foreach``, as that would throw an exception when attempting to rewind the
cursor. This is a ripe use case for directly controlling the iteration process
using :php:`IteratorIterator <iteratoriterator>`.

.. code-block:: php

   <?php

   $collection = (new MongoDB\Client)->test->capped;

   $cursor = $collection->find([], [
       'cursorType' => MongoDB\Operation\Find::TAILABLE_AWAIT,
       'maxAwaitTimeMS' => 100,
   ]);

   $iterator = new IteratorIterator($cursor);

   $iterator->rewind();

   while (true) {
      if ($iterator->valid()) {
         $document = $iterator->current();
         printf("Consumed document created at: %s\n", $document->createdAt);
      }

      $iterator->next();
   }

Much like the ``foreach`` example, this version on the consumer script will
start by quickly printing all results in the capped collection; however, it will
not terminate upon reaching the end of the initial result set. Since we're
working with a tailable cursor, calling ``next()`` will block and wait for
additional results rather than throw an exception. We will also use ``valid()``
to check if there is actually data available to read at each step.

Since we've elected to use a ``TAILABLE_AWAIT`` cursor, the server will delay
its response to the driver for a set amount of time. In this example, we've
requested that the server block for approximately 100 milliseconds by specifying
the ``maxAwaitTimeMS`` option to :phpmethod:`MongoDB\\Collection::find()`.