|
| 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