Skip to content

Commit fcbe831

Browse files
committed
PHPLIB-456: Always invalidate ChangeStream position if iteration fails
1 parent fcd0d78 commit fcbe831

File tree

2 files changed

+38
-21
lines changed

2 files changed

+38
-21
lines changed

src/Model/ChangeStreamIterator.php

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ class ChangeStreamIterator extends IteratorIterator implements CommandSubscriber
4848
private $batchPosition = 0;
4949
private $batchSize;
5050
private $isRewindNop;
51+
private $isValid = false;
5152
private $postBatchResumeToken;
5253
private $resumeToken;
5354

@@ -117,6 +118,15 @@ final public function commandSucceeded(CommandSucceededEvent $event)
117118
}
118119
}
119120

121+
/**
122+
* @see https://php.net/iteratoriterator.current
123+
* @return mixed
124+
*/
125+
public function current()
126+
{
127+
return $this->isValid ? parent::current() : null;
128+
}
129+
120130
/**
121131
* Returns the resume token for the iterator's current position.
122132
*
@@ -131,6 +141,15 @@ public function getResumeToken()
131141
return $this->resumeToken;
132142
}
133143

144+
/**
145+
* @see https://php.net/iteratoriterator.key
146+
* @return mixed
147+
*/
148+
public function key()
149+
{
150+
return $this->isValid ? parent::key() : null;
151+
}
152+
134153
/**
135154
* @see https://php.net/iteratoriterator.rewind
136155
* @return void
@@ -172,6 +191,15 @@ public function rewind()
172191
$this->onIteration(false);
173192
}
174193

194+
/**
195+
* @see https://php.net/iteratoriterator.valid
196+
* @return boolean
197+
*/
198+
public function valid()
199+
{
200+
return $this->isValid;
201+
}
202+
175203
/**
176204
* Extracts the resume token (i.e. "_id" field) from a change document.
177205
*
@@ -195,10 +223,12 @@ private function extractResumeToken($document)
195223
: (isset($document->_id) ? $document->_id : null);
196224

197225
if (! isset($resumeToken)) {
226+
$this->isValid = false;
198227
throw ResumeTokenException::notFound();
199228
}
200229

201230
if (! is_array($resumeToken) && ! is_object($resumeToken)) {
231+
$this->isValid = false;
202232
throw ResumeTokenException::invalidType($resumeToken);
203233
}
204234

@@ -223,16 +253,16 @@ private function isAtEndOfBatch()
223253
*/
224254
private function onIteration($incrementBatchPosition)
225255
{
226-
$isValid = $this->valid();
256+
$this->isValid = parent::valid();
227257

228258
/* Disable rewind()'s NOP behavior once we advance to a valid position.
229259
* This will allow the driver to throw a LogicException if rewind() is
230260
* called after the cursor has advanced past its first element. */
231-
if ($this->isRewindNop && $isValid) {
261+
if ($this->isRewindNop && $this->isValid) {
232262
$this->isRewindNop = false;
233263
}
234264

235-
if ($incrementBatchPosition && $isValid) {
265+
if ($incrementBatchPosition && $this->isValid) {
236266
$this->batchPosition++;
237267
}
238268

@@ -244,7 +274,7 @@ private function onIteration($incrementBatchPosition)
244274
* from the current document if possible. */
245275
if ($this->isAtEndOfBatch() && $this->postBatchResumeToken !== null) {
246276
$this->resumeToken = $this->postBatchResumeToken;
247-
} elseif ($isValid) {
277+
} elseif ($this->isValid) {
248278
$this->resumeToken = $this->extractResumeToken($this->current());
249279
}
250280
}

tests/Operation/WatchFunctionalTest.php

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1123,34 +1123,21 @@ public function testResumeTokenNotFoundDoesNotAdvanceKey()
11231123
$changeStream->next();
11241124
$this->fail('Exception for missing resume token was not thrown');
11251125
} catch (ResumeTokenException $e) {
1126-
/* If a client-side error is thrown (server < 4.1.8), the tailable
1127-
* cursor's position is still valid. This may change once PHPLIB-456
1128-
* is implemented. */
1129-
$expectedValid = true;
1130-
$expectedKey = 0;
11311126
} catch (ServerException $e) {
1132-
/* If a server-side error is thrown (server >= 4.1.8), the tailable
1133-
* cursor's position is not valid. */
1134-
$expectedValid = false;
1135-
$expectedKey = null;
11361127
}
11371128

1138-
$this->assertSame($expectedValid, $changeStream->valid());
1139-
$this->assertSame($expectedKey, $changeStream->key());
1129+
$this->assertFalse($changeStream->valid());
1130+
$this->assertNull($changeStream->key());
11401131

11411132
try {
11421133
$changeStream->next();
11431134
$this->fail('Exception for missing resume token was not thrown');
11441135
} catch (ResumeTokenException $e) {
1145-
$expectedValid = true;
1146-
$expectedKey = 0;
11471136
} catch (ServerException $e) {
1148-
$expectedValid = false;
1149-
$expectedKey = null;
11501137
}
11511138

1152-
$this->assertSame($expectedValid, $changeStream->valid());
1153-
$this->assertSame($expectedKey, $changeStream->key());
1139+
$this->assertFalse($changeStream->valid());
1140+
$this->assertNull($changeStream->key());
11541141
}
11551142

11561143
public function testSessionPersistsAfterResume()

0 commit comments

Comments
 (0)