Skip to content

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

Merged
merged 3 commits into from
Jun 25, 2019

Conversation

jmikola
Copy link
Member

@jmikola jmikola commented Jun 21, 2019

@jmikola jmikola requested a review from kvwalker June 21, 2019 22:15
@jmikola jmikola changed the base branch from master to v1.4 June 21, 2019 22:15
/* 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) {
Copy link
Member Author

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.

Copy link
Contributor

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);
Copy link
Member Author

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.

Copy link
Contributor

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?

Copy link
Member Author

@jmikola jmikola Jun 25, 2019

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.

Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Member Author

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()
Copy link
Member Author

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()
Copy link
Member Author

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(). */
Copy link
Member Author

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) {
Copy link
Contributor

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);
Copy link
Contributor

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
Copy link
Contributor

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

Copy link
Contributor

@kvwalker kvwalker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm!

@jmikola jmikola merged commit fdf87ad into mongodb:v1.4 Jun 25, 2019
jmikola added a commit that referenced this pull request Jun 25, 2019
@jmikola
Copy link
Member Author

jmikola commented Jun 25, 2019

Test failure fixed in #628.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants