-
Notifications
You must be signed in to change notification settings - Fork 266
PHPLIB-441: Increment ChangeStream key consistently when resuming #626
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
/* Increment the key if the iteration event was a call to next() and we | ||
* have already advanced past the first result. */ | ||
if ($isNext && $this->hasAdvanced) { | ||
if ($incrementKey) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I contemplated renaming this to $incrementKeyIfValid
, but adding a comment above the early return for !$this->valid()
seemed preferable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the comment makes this clear.
* LogicException for attempting to rewind an advanced cursor. In the | ||
* latter case, the exception would have bypassed the catch block for | ||
* resuming. */ | ||
$this->onIteration($this->hasAdvanced); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd appreciate a sanity check on this. While coming up with the current implementation in this PR, I ran the test suite with a hard assertion of $this->hasAdvanced === false
placed before the call to resume()
in rewind()
's catch
block, and it was never triggered.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I follow this part:
If it were true, then the previous call to IteratorIterator::rewind() would either have been
a NOP and succeeded because the cursor was still at its first position
How can $hasAdvanced
be true if the cursor was still at its first position?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My point here was that "if it were true" cannot happen in practice. Either the rewind()
succeeds as a NOP and doesn't throw, so we never call resume()
in the first place, or it throws a LogicException, which we don't catch (and also don't call resume()
). I'll reword this a bit to better explain that, as I feel I only conveyed that the LogicException case is when we never reach this point.
How can
$hasAdvanced
be true if the cursor was still at its first position?
If this was a more general question (apart from resume()
being invoked), I can address that. When a cursor is first created (basic or tailable alike), it starts before the first element. valid()
is false
and there is no current element. The initial call to rewind()
or next()
is responsible for advancing to the first position (if any), updating valid()
, and optionally populating the current element.
We use $hasAdvanced
here to track if we've advanced before the initial pre-iteration state. If it's more helpful, I suppose we can rename this to $hasIterationStarted
. Let me know if you think that'd make more sense.
Another edge case is that it's kosher to call rewind()
multiple times in sequence. And since $key
is initialized to 0
, we should never increment the key after a successful call to rewind()
, since, unlike next()
or resuming, rewind()
itself never moves past the first element.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh I see, thanks for the explanation! All of this makes sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose we can rename this to
$hasIterationStarted
.
Ah, I forgot that I actually added a doc block to $hasAdvanced
to explain exactly what it implies (here). I think that's sufficient, as $hasIterated
might also sound confusing since calls to rewind()
and next()
can still leave it false
if the change stream doesn't come across a change event. I'll stick with the shorter name and just revise this comment block to be more clear.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: this is a slight edge case where rewind()
could be called immediately after resuming without encountering a LogicException from the driver. Since resuming creates a fresh cursor and rewinds it, a user could technically call ChangeStream::rewind()
after a resume and not see an error. This is very trivial and may not be worth fixing, but I opened PHPLIB-448 to track it.
@@ -225,6 +226,62 @@ private function assertStartAtOperationTime(TimestampInterface $expectedOperatio | |||
$this->assertEquals($expectedOperationTime, $command->pipeline[0]->{'$changeStream'}->startAtOperationTime); | |||
} | |||
|
|||
public function testRewindMultipleTimesWithResults() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These two methods aren't related to resuming, so I included them in their own commit. This is the rewind()
counterpart to the existing testNextAdvancesKey
test method.
@@ -322,20 +379,67 @@ public function testNoChangeAfterResumeBeforeInsert() | |||
$this->assertMatchesDocument($expectedResult, $changeStream->current()); | |||
} | |||
|
|||
public function testResumeTokenIsUpdatedAfterResuming() | |||
public function testResumeMultipleTimesInSuccession() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method was introduced in #621 for PHPLIB-442 earlier this week, so I didn't feel too bad about renaming it. Refactoring it to also check for key increments (in addition to the existing result assertions) seemed preferable to duplicating the entire test to only check key increments.
I also added some additional resume cases, which weren't in the original test.
@@ -689,6 +795,8 @@ public function testNextAdvancesKey() | |||
$this->insertDocument(['x' => 1]); | |||
$this->insertDocument(['x' => 2]); | |||
|
|||
/* Note: we intentionally do not start iteration with rewind() to ensure | |||
* that next() behaves identically when called without rewind(). */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I happened to read through this test while writing the multiple rewind tests and thought this could use some clarity. This change is also in its own commit, since it doesn't pertain to resuming.
/* Increment the key if the iteration event was a call to next() and we | ||
* have already advanced past the first result. */ | ||
if ($isNext && $this->hasAdvanced) { | ||
if ($incrementKey) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the comment makes this clear.
* LogicException for attempting to rewind an advanced cursor. In the | ||
* latter case, the exception would have bypassed the catch block for | ||
* resuming. */ | ||
$this->onIteration($this->hasAdvanced); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I follow this part:
If it were true, then the previous call to IteratorIterator::rewind() would either have been
a NOP and succeeded because the cursor was still at its first position
How can $hasAdvanced
be true if the cursor was still at its first position?
|
||
$operation = new Watch($this->manager, $this->getDatabaseName(), $this->getCollectionName(), [], $this->defaultOptions); | ||
$changeStream = $operation->execute($this->getPrimaryServer()); | ||
|
||
/* Killing the cursor when there are no results will tests that neither |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: tests
should be `test
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm!
Test failure fixed in #628. |
https://jira.mongodb.org/browse/PHPLIB-441