Commit 8c7a92cc authored by Jeremy Mikola's avatar Jeremy Mikola

Merge pull request #636

parents 80cee6f6 84c365c0
...@@ -24,7 +24,7 @@ use MongoDB\Driver\Exception\RuntimeException; ...@@ -24,7 +24,7 @@ use MongoDB\Driver\Exception\RuntimeException;
use MongoDB\Driver\Exception\ServerException; use MongoDB\Driver\Exception\ServerException;
use MongoDB\Exception\InvalidArgumentException; use MongoDB\Exception\InvalidArgumentException;
use MongoDB\Exception\ResumeTokenException; use MongoDB\Exception\ResumeTokenException;
use IteratorIterator; use MongoDB\Model\TailableCursorIterator;
use Iterator; use Iterator;
/** /**
...@@ -61,13 +61,14 @@ class ChangeStream implements Iterator ...@@ -61,13 +61,14 @@ class ChangeStream implements Iterator
* Constructor. * Constructor.
* *
* @internal * @internal
* @param Cursor $cursor * @param Cursor $cursor
* @param callable $resumeCallable * @param callable $resumeCallable
* @param boolean $isFirstBatchEmpty
*/ */
public function __construct(Cursor $cursor, callable $resumeCallable) public function __construct(Cursor $cursor, callable $resumeCallable, $isFirstBatchEmpty)
{ {
$this->resumeCallable = $resumeCallable; $this->resumeCallable = $resumeCallable;
$this->csIt = new IteratorIterator($cursor); $this->csIt = new TailableCursorIterator($cursor, $isFirstBatchEmpty);
} }
/** /**
...@@ -242,17 +243,11 @@ class ChangeStream implements Iterator ...@@ -242,17 +243,11 @@ class ChangeStream implements Iterator
*/ */
private function resume() private function resume()
{ {
$newChangeStream = call_user_func($this->resumeCallable, $this->resumeToken); list($cursor, $isFirstBatchEmpty) = call_user_func($this->resumeCallable, $this->resumeToken);
$this->csIt = $newChangeStream->csIt;
$this->csIt = new TailableCursorIterator($cursor, $isFirstBatchEmpty);
$this->csIt->rewind(); $this->csIt->rewind();
/* Note: if we are resuming after a call to ChangeStream::rewind(),
* $hasAdvanced will always be false. For it to be true, rewind() would
* need to have thrown a RuntimeException with a resumable error, which
* can only happen during the first call to IteratorIterator::rewind()
* before onIteration() has a chance to set $hasAdvanced to true.
* Otherwise, IteratorIterator::rewind() would either NOP (consecutive
* rewinds) or throw a LogicException (rewind after next), neither of
* which would result in a call to resume(). */
$this->onIteration($this->hasAdvanced); $this->onIteration($this->hasAdvanced);
} }
......
<?php
/*
* Copyright 2019 MongoDB, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace MongoDB\Model;
use MongoDB\Driver\Cursor;
use IteratorIterator;
/**
* Iterator for tailable cursors.
*
* This iterator may be used to wrap a tailable cursor. By indicating whether
* the cursor's first batch of results is empty, this iterator can NOP initial
* calls to rewind() and prevent it from executing a getMore command.
*
* @internal
*/
class TailableCursorIterator extends IteratorIterator
{
private $isRewindNop;
/**
* Constructor.
*
* @internal
* @param Cursor $cursor
* @param boolean $isFirstBatchEmpty
*/
public function __construct(Cursor $cursor, $isFirstBatchEmpty)
{
parent::__construct($cursor);
$this->isRewindNop = $isFirstBatchEmpty;
}
/**
* @see https://php.net/iteratoriterator.rewind
* @return void
*/
public function next()
{
try {
parent::next();
} finally {
/* If the cursor ever advances to a valid position, do not prevent
* future attempts to rewind the cursor. This will allow the driver
* to throw a LogicException if the cursor has been advanced past
* its first element. */
if ($this->valid()) {
$this->isRewindNop = false;
}
}
}
/**
* @see https://php.net/iteratoriterator.rewind
* @return void
*/
public function rewind()
{
if ($this->isRewindNop) {
return;
}
parent::rewind();
}
}
...@@ -57,6 +57,7 @@ class Watch implements Executable, /* @internal */ CommandSubscriber ...@@ -57,6 +57,7 @@ class Watch implements Executable, /* @internal */ CommandSubscriber
private $changeStreamOptions; private $changeStreamOptions;
private $collectionName; private $collectionName;
private $databaseName; private $databaseName;
private $isFirstBatchEmpty = false;
private $operationTime; private $operationTime;
private $pipeline; private $pipeline;
private $resumeCallable; private $resumeCallable;
...@@ -200,6 +201,11 @@ class Watch implements Executable, /* @internal */ CommandSubscriber ...@@ -200,6 +201,11 @@ class Watch implements Executable, /* @internal */ CommandSubscriber
/** @internal */ /** @internal */
final public function commandStarted(CommandStartedEvent $event) final public function commandStarted(CommandStartedEvent $event)
{ {
if ($event->getCommandName() !== 'aggregate') {
return;
}
$this->isFirstBatchEmpty = false;
} }
/** @internal */ /** @internal */
...@@ -211,9 +217,15 @@ class Watch implements Executable, /* @internal */ CommandSubscriber ...@@ -211,9 +217,15 @@ class Watch implements Executable, /* @internal */ CommandSubscriber
$reply = $event->getReply(); $reply = $event->getReply();
if (isset($reply->operationTime) && $reply->operationTime instanceof TimestampInterface) { /* Note: the spec only refers to collecting an operation time from the
* "original aggregation", so only capture it if we've not already. */
if (!isset($this->operationTime) && isset($reply->operationTime) && $reply->operationTime instanceof TimestampInterface) {
$this->operationTime = $reply->operationTime; $this->operationTime = $reply->operationTime;
} }
if (isset($reply->cursor->firstBatch) && is_array($reply->cursor->firstBatch)) {
$this->isFirstBatchEmpty = empty($reply->cursor->firstBatch);
}
} }
/** /**
...@@ -227,7 +239,9 @@ class Watch implements Executable, /* @internal */ CommandSubscriber ...@@ -227,7 +239,9 @@ class Watch implements Executable, /* @internal */ CommandSubscriber
*/ */
public function execute(Server $server) public function execute(Server $server)
{ {
return new ChangeStream($this->executeAggregate($server), $this->resumeCallable); $cursor = $this->executeAggregate($server);
return new ChangeStream($cursor, $this->resumeCallable, $this->isFirstBatchEmpty);
} }
/** /**
...@@ -255,40 +269,36 @@ class Watch implements Executable, /* @internal */ CommandSubscriber ...@@ -255,40 +269,36 @@ class Watch implements Executable, /* @internal */ CommandSubscriber
unset($this->changeStreamOptions['startAtOperationTime']); unset($this->changeStreamOptions['startAtOperationTime']);
} }
// Select a new server using the original read preference
$server = $manager->selectServer($this->aggregateOptions['readPreference']);
/* If we captured an operation time from the first aggregate command /* If we captured an operation time from the first aggregate command
* and there is no "resumeAfter" option, set "startAtOperationTime" * and there is no "resumeAfter" option, set "startAtOperationTime"
* so that we can resume from the original aggregate's time. */ * so that we can resume from the original aggregate's time. */
if ($this->operationTime !== null && ! isset($this->changeStreamOptions['resumeAfter'])) { if ($this->operationTime !== null && ! isset($this->changeStreamOptions['resumeAfter']) &&
\MongoDB\server_supports_feature($server, self::$wireVersionForStartAtOperationTime)) {
$this->changeStreamOptions['startAtOperationTime'] = $this->operationTime; $this->changeStreamOptions['startAtOperationTime'] = $this->operationTime;
} }
// Recreate the aggregate command and execute to obtain a new cursor
$this->aggregate = $this->createAggregate(); $this->aggregate = $this->createAggregate();
$cursor = $this->executeAggregate($server);
/* Select a new server using the read preference, execute this return [$cursor, $this->isFirstBatchEmpty];
* operation on it, and return the new ChangeStream. */
$server = $manager->selectServer($this->aggregateOptions['readPreference']);
return $this->execute($server);
}; };
} }
/** /**
* Execute the aggregate command and optionally capture its operation time. * Execute the aggregate command.
*
* The command will be executed using APM so that we can capture its
* operation time and/or firstBatch size.
* *
* @param Server $server * @param Server $server
* @return Cursor * @return Cursor
*/ */
private function executeAggregate(Server $server) private function executeAggregate(Server $server)
{ {
/* If we've already captured an operation time or the server does not
* support resuming from an operation time (e.g. MongoDB 3.6), execute
* the aggregation directly and return its cursor. */
if ($this->operationTime !== null || ! \MongoDB\server_supports_feature($server, self::$wireVersionForStartAtOperationTime)) {
return $this->aggregate->execute($server);
}
/* Otherwise, execute the aggregation using command monitoring so that
* we can capture its operation time with commandSucceeded(). */
\MongoDB\Driver\Monitoring\addSubscriber($this); \MongoDB\Driver\Monitoring\addSubscriber($this);
try { try {
......
<?php
namespace MongoDB\Tests\Model;
use MongoDB\Collection;
use MongoDB\Driver\Exception\LogicException;
use MongoDB\Model\TailableCursorIterator;
use MongoDB\Operation\Find;
use MongoDB\Operation\CreateCollection;
use MongoDB\Operation\DropCollection;
use MongoDB\Tests\CommandObserver;
use MongoDB\Tests\FunctionalTestCase;
class TailableCursorIteratorTest extends FunctionalTestCase
{
private $collection;
public function setUp()
{
parent::setUp();
$operation = new DropCollection($this->getDatabaseName(), $this->getCollectionName());
$operation->execute($this->getPrimaryServer());
$operation = new CreateCollection($this->getDatabaseName(), $this->getCollectionName(), ['capped' => true, 'size' => 8192]);
$operation->execute($this->getPrimaryServer());
$this->collection = new Collection($this->manager, $this->getDatabaseName(), $this->getCollectionName());
}
public function testFirstBatchIsEmpty()
{
$this->collection->insertOne(['x' => 1]);
$cursor = $this->collection->find(['x' => ['$gt' => 1]], ['cursorType' => Find::TAILABLE]);
$iterator = new TailableCursorIterator($cursor, true);
$this->assertNoCommandExecuted(function() use ($iterator) { $iterator->rewind(); });
$this->assertFalse($iterator->valid());
$this->collection->insertOne(['x' => 2]);
$iterator->next();
$this->assertTrue($iterator->valid());
$this->assertMatchesDocument(['x' => 2], $iterator->current());
$this->expectException(LogicException::class);
$iterator->rewind();
}
public function testFirstBatchIsNotEmpty()
{
$this->collection->insertOne(['x' => 1]);
$cursor = $this->collection->find([], ['cursorType' => Find::TAILABLE]);
$iterator = new TailableCursorIterator($cursor, false);
$this->assertNoCommandExecuted(function() use ($iterator) { $iterator->rewind(); });
$this->assertTrue($iterator->valid());
$this->assertMatchesDocument(['x' => 1], $iterator->current());
$this->collection->insertOne(['x' => 2]);
$iterator->next();
$this->assertTrue($iterator->valid());
$this->assertMatchesDocument(['x' => 2], $iterator->current());
$this->expectException(LogicException::class);
$iterator->rewind();
}
private function assertNoCommandExecuted(callable $callable)
{
$commands = [];
(new CommandObserver)->observe(
$callable,
function(array $event) use (&$commands) {
$this->fail(sprintf('"%s" command was executed', $event['started']->getCommandName()));
}
);
$this->assertEmpty($commands);
}
}
...@@ -7,7 +7,7 @@ use MongoDB\BSON\TimestampInterface; ...@@ -7,7 +7,7 @@ use MongoDB\BSON\TimestampInterface;
use MongoDB\Driver\Manager; use MongoDB\Driver\Manager;
use MongoDB\Driver\ReadPreference; use MongoDB\Driver\ReadPreference;
use MongoDB\Driver\Server; use MongoDB\Driver\Server;
use MongoDB\Driver\Exception\ConnectionTimeoutException; use MongoDB\Driver\WriteConcern;
use MongoDB\Driver\Exception\LogicException; use MongoDB\Driver\Exception\LogicException;
use MongoDB\Exception\ResumeTokenException; use MongoDB\Exception\ResumeTokenException;
use MongoDB\Operation\CreateCollection; use MongoDB\Operation\CreateCollection;
...@@ -39,7 +39,7 @@ class WatchFunctionalTest extends FunctionalTestCase ...@@ -39,7 +39,7 @@ class WatchFunctionalTest extends FunctionalTestCase
$changeStream = $operation->execute($this->getPrimaryServer()); $changeStream = $operation->execute($this->getPrimaryServer());
$changeStream->rewind(); $changeStream->rewind();
$this->assertNull($changeStream->current()); $this->assertFalse($changeStream->valid());
$this->insertDocument(['_id' => 2, 'x' => 'bar']); $this->insertDocument(['_id' => 2, 'x' => 'bar']);
...@@ -84,30 +84,25 @@ class WatchFunctionalTest extends FunctionalTestCase ...@@ -84,30 +84,25 @@ class WatchFunctionalTest extends FunctionalTestCase
$operation = new Watch($manager, $this->getDatabaseName(), $this->getCollectionName(), [], $this->defaultOptions); $operation = new Watch($manager, $this->getDatabaseName(), $this->getCollectionName(), [], $this->defaultOptions);
$changeStream = $operation->execute($primaryServer); $changeStream = $operation->execute($primaryServer);
$changeStream->rewind();
/* Note: we intentionally do not start iteration with rewind() to ensure
* that we test resume functionality within next(). */
$commands = []; $commands = [];
try { (new CommandObserver)->observe(
(new CommandObserver)->observe( function() use ($changeStream) {
function() use ($changeStream) { $changeStream->next();
$changeStream->next(); },
}, function(array $event) use (&$commands) {
function(array $event) use (&$commands) { $commands[] = $event['started']->getCommandName();
$commands[] = $event['started']->getCommandName(); }
} );
);
$this->fail('ConnectionTimeoutException was not thrown');
} catch (ConnectionTimeoutException $e) {}
$expectedCommands = [ $expectedCommands = [
/* The initial aggregate command for change streams returns a cursor /* The initial aggregate command for change streams returns a cursor
* envelope with an empty initial batch, since there are no changes * envelope with an empty initial batch, since there are no changes
* to report at the moment the change stream is created. Therefore, * to report at the moment the change stream is created. Therefore,
* we expect a getMore to be issued when we first advance the change * we expect a getMore to be issued when we first advance the change
* stream (with either rewind() or next()). */ * stream with next(). */
'getMore', 'getMore',
/* Since socketTimeoutMS is less than maxAwaitTimeMS, the previous /* Since socketTimeoutMS is less than maxAwaitTimeMS, the previous
* getMore command encounters a client socket timeout and leaves the * getMore command encounters a client socket timeout and leaves the
...@@ -119,9 +114,6 @@ class WatchFunctionalTest extends FunctionalTestCase ...@@ -119,9 +114,6 @@ class WatchFunctionalTest extends FunctionalTestCase
* removes the last reference to the old cursor, which causes the * removes the last reference to the old cursor, which causes the
* driver to kill it (via mongoc_cursor_destroy()). */ * driver to kill it (via mongoc_cursor_destroy()). */
'killCursors', 'killCursors',
/* Finally, ChangeStream will rewind the new cursor as the last step
* of the resume process. This results in one last getMore. */
'getMore',
]; ];
$this->assertSame($expectedCommands, $commands); $this->assertSame($expectedCommands, $commands);
...@@ -152,38 +144,10 @@ class WatchFunctionalTest extends FunctionalTestCase ...@@ -152,38 +144,10 @@ class WatchFunctionalTest extends FunctionalTestCase
$operationTime = $reply->operationTime; $operationTime = $reply->operationTime;
$this->assertInstanceOf(TimestampInterface::class, $operationTime); $this->assertInstanceOf(TimestampInterface::class, $operationTime);
$this->assertNull($changeStream->current()); $this->assertFalse($changeStream->valid());
$this->killChangeStreamCursor($changeStream); $this->killChangeStreamCursor($changeStream);
$events = []; $this->assertNoCommandExecuted(function() use ($changeStream) { $changeStream->rewind(); });
(new CommandObserver)->observe(
function() use ($changeStream) {
$changeStream->rewind();
},
function (array $event) use (&$events) {
$events[] = $event;
}
);
$this->assertCount(4, $events);
$this->assertSame('getMore', $events[0]['started']->getCommandName());
$this->arrayHasKey('failed', $events[0]);
$this->assertSame('aggregate', $events[1]['started']->getCommandName());
$this->assertStartAtOperationTime($operationTime, $events[1]['started']->getCommand());
$this->arrayHasKey('succeeded', $events[1]);
// Original cursor is freed immediately after the change stream resumes
$this->assertSame('killCursors', $events[2]['started']->getCommandName());
$this->arrayHasKey('succeeded', $events[2]);
$this->assertSame('getMore', $events[3]['started']->getCommandName());
$this->arrayHasKey('succeeded', $events[3]);
$this->assertNull($changeStream->current());
$this->killChangeStreamCursor($changeStream);
$events = []; $events = [];
...@@ -196,7 +160,7 @@ class WatchFunctionalTest extends FunctionalTestCase ...@@ -196,7 +160,7 @@ class WatchFunctionalTest extends FunctionalTestCase
} }
); );
$this->assertCount(4, $events); $this->assertCount(3, $events);
$this->assertSame('getMore', $events[0]['started']->getCommandName()); $this->assertSame('getMore', $events[0]['started']->getCommandName());
$this->arrayHasKey('failed', $events[0]); $this->arrayHasKey('failed', $events[0]);
...@@ -209,10 +173,7 @@ class WatchFunctionalTest extends FunctionalTestCase ...@@ -209,10 +173,7 @@ class WatchFunctionalTest extends FunctionalTestCase
$this->assertSame('killCursors', $events[2]['started']->getCommandName()); $this->assertSame('killCursors', $events[2]['started']->getCommandName());
$this->arrayHasKey('succeeded', $events[2]); $this->arrayHasKey('succeeded', $events[2]);
$this->assertSame('getMore', $events[3]['started']->getCommandName()); $this->assertFalse($changeStream->valid());
$this->arrayHasKey('succeeded', $events[3]);
$this->assertNull($changeStream->current());
} }
private function assertStartAtOperationTime(TimestampInterface $expectedOperationTime, stdClass $command) private function assertStartAtOperationTime(TimestampInterface $expectedOperationTime, stdClass $command)
...@@ -233,19 +194,30 @@ class WatchFunctionalTest extends FunctionalTestCase ...@@ -233,19 +194,30 @@ class WatchFunctionalTest extends FunctionalTestCase
$this->insertDocument(['x' => 1]); $this->insertDocument(['x' => 1]);
$this->insertDocument(['x' => 2]); $this->insertDocument(['x' => 2]);
$changeStream->rewind(); $this->assertNoCommandExecuted(function() use ($changeStream) { $changeStream->rewind(); });
$this->assertFalse($changeStream->valid());
$this->assertNull($changeStream->key());
$this->assertNull($changeStream->current());
// Subsequent rewind does not change iterator state
$this->assertNoCommandExecuted(function() use ($changeStream) { $changeStream->rewind(); });
$this->assertFalse($changeStream->valid());
$this->assertNull($changeStream->key());
$this->assertNull($changeStream->current());
$changeStream->next();
$this->assertTrue($changeStream->valid()); $this->assertTrue($changeStream->valid());
$this->assertSame(0, $changeStream->key()); $this->assertSame(0, $changeStream->key());
$this->assertNotNull($changeStream->current()); $this->assertNotNull($changeStream->current());
// Subsequent rewind does not change iterator state /* Rewinding when the iterator is still at its first element is a NOP.
$changeStream->rewind(); * Note: PHPLIB-448 may see rewind() throw after any call to next() */
$this->assertNoCommandExecuted(function() use ($changeStream) { $changeStream->rewind(); });
$this->assertTrue($changeStream->valid()); $this->assertTrue($changeStream->valid());
$this->assertSame(0, $changeStream->key()); $this->assertSame(0, $changeStream->key());
$this->assertNotNull($changeStream->current()); $this->assertNotNull($changeStream->current());
$changeStream->next(); $changeStream->next();
$this->assertTrue($changeStream->valid()); $this->assertTrue($changeStream->valid());
$this->assertSame(1, $changeStream->key()); $this->assertSame(1, $changeStream->key());
$this->assertNotNull($changeStream->current()); $this->assertNotNull($changeStream->current());
...@@ -260,13 +232,13 @@ class WatchFunctionalTest extends FunctionalTestCase ...@@ -260,13 +232,13 @@ class WatchFunctionalTest extends FunctionalTestCase
$operation = new Watch($this->manager, $this->getDatabaseName(), $this->getCollectionName(), [], $this->defaultOptions); $operation = new Watch($this->manager, $this->getDatabaseName(), $this->getCollectionName(), [], $this->defaultOptions);
$changeStream = $operation->execute($this->getPrimaryServer()); $changeStream = $operation->execute($this->getPrimaryServer());
$changeStream->rewind(); $this->assertNoCommandExecuted(function() use ($changeStream) { $changeStream->rewind(); });
$this->assertFalse($changeStream->valid()); $this->assertFalse($changeStream->valid());
$this->assertNull($changeStream->key()); $this->assertNull($changeStream->key());
$this->assertNull($changeStream->current()); $this->assertNull($changeStream->current());
// Subsequent rewind does not change iterator state // Subsequent rewind does not change iterator state
$changeStream->rewind(); $this->assertNoCommandExecuted(function() use ($changeStream) { $changeStream->rewind(); });
$this->assertFalse($changeStream->valid()); $this->assertFalse($changeStream->valid());
$this->assertNull($changeStream->key()); $this->assertNull($changeStream->key());
$this->assertNull($changeStream->current()); $this->assertNull($changeStream->current());
...@@ -276,59 +248,12 @@ class WatchFunctionalTest extends FunctionalTestCase ...@@ -276,59 +248,12 @@ class WatchFunctionalTest extends FunctionalTestCase
$this->assertNull($changeStream->key()); $this->assertNull($changeStream->key());
$this->assertNull($changeStream->current()); $this->assertNull($changeStream->current());
// Rewinding after advancing the iterator is an error /* Rewinding when the iterator hasn't advanced to an element is a NOP.
$this->expectException(LogicException::class); * Note: PHPLIB-448 may see rewind() throw after any call to next() */
$changeStream->rewind(); $this->assertNoCommandExecuted(function() use ($changeStream) { $changeStream->rewind(); });
} $this->assertFalse($changeStream->valid());
$this->assertNull($changeStream->key());
public function testRewindResumesAfterConnectionException() $this->assertNull($changeStream->current());
{
/* In order to trigger a dropped connection, we'll use a new client with
* a socket timeout that is less than the change stream's maxAwaitTimeMS
* option. */
$manager = new Manager(static::getUri(), ['socketTimeoutMS' => 50]);
$primaryServer = $manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
$operation = new Watch($manager, $this->getDatabaseName(), $this->getCollectionName(), [], $this->defaultOptions);
$changeStream = $operation->execute($primaryServer);
$commands = [];
try {
(new CommandObserver)->observe(
function() use ($changeStream) {
$changeStream->rewind();
},
function(array $event) use (&$commands) {
$commands[] = $event['started']->getCommandName();
}
);
$this->fail('ConnectionTimeoutException was not thrown');
} catch (ConnectionTimeoutException $e) {}
$expectedCommands = [
/* The initial aggregate command for change streams returns a cursor
* envelope with an empty initial batch, since there are no changes
* to report at the moment the change stream is created. Therefore,
* we expect a getMore to be issued when we first advance the change
* stream (with either rewind() or next()). */
'getMore',
/* Since socketTimeoutMS is less than maxAwaitTimeMS, the previous
* getMore command encounters a client socket timeout and leaves the
* cursor open on the server. ChangeStream should catch this error
* and resume by issuing a new aggregate command. */
'aggregate',
/* When ChangeStream resumes, it overwrites its original cursor with
* the new cursor resulting from the last aggregate command. This
* removes the last reference to the old cursor, which causes the
* driver to kill it (via mongoc_cursor_destroy()). */
'killCursors',
/* Finally, ChangeStream will rewind the new cursor as the last step
* of the resume process. This results in one last getMore. */
'getMore',
];
$this->assertSame($expectedCommands, $commands);
} }
public function testNoChangeAfterResumeBeforeInsert() public function testNoChangeAfterResumeBeforeInsert()
...@@ -338,8 +263,8 @@ class WatchFunctionalTest extends FunctionalTestCase ...@@ -338,8 +263,8 @@ class WatchFunctionalTest extends FunctionalTestCase
$operation = new Watch($this->manager, $this->getDatabaseName(), $this->getCollectionName(), [], $this->defaultOptions); $operation = new Watch($this->manager, $this->getDatabaseName(), $this->getCollectionName(), [], $this->defaultOptions);
$changeStream = $operation->execute($this->getPrimaryServer()); $changeStream = $operation->execute($this->getPrimaryServer());
$changeStream->rewind(); $this->assertNoCommandExecuted(function() use ($changeStream) { $changeStream->rewind(); });
$this->assertNull($changeStream->current()); $this->assertFalse($changeStream->valid());
$this->insertDocument(['_id' => 2, 'x' => 'bar']); $this->insertDocument(['_id' => 2, 'x' => 'bar']);
...@@ -360,7 +285,6 @@ class WatchFunctionalTest extends FunctionalTestCase ...@@ -360,7 +285,6 @@ class WatchFunctionalTest extends FunctionalTestCase
$changeStream->next(); $changeStream->next();
$this->assertFalse($changeStream->valid()); $this->assertFalse($changeStream->valid());
$this->assertNull($changeStream->current());
$this->insertDocument(['_id' => 3, 'x' => 'baz']); $this->insertDocument(['_id' => 3, 'x' => 'baz']);
...@@ -387,50 +311,53 @@ class WatchFunctionalTest extends FunctionalTestCase ...@@ -387,50 +311,53 @@ class WatchFunctionalTest extends FunctionalTestCase
$changeStream = $operation->execute($this->getPrimaryServer()); $changeStream = $operation->execute($this->getPrimaryServer());
/* Killing the cursor when there are no results will test that neither /* Killing the cursor when there are no results will test that neither
* the initial rewind() nor its resume attempt incremented the key. */ * the initial rewind() nor a resume attempt via next() increment the
* key. */
$this->killChangeStreamCursor($changeStream); $this->killChangeStreamCursor($changeStream);
$changeStream->rewind(); $this->assertNoCommandExecuted(function() use ($changeStream) { $changeStream->rewind(); });
$this->assertFalse($changeStream->valid()); $this->assertFalse($changeStream->valid());
$this->assertNull($changeStream->key()); $this->assertNull($changeStream->key());
$this->assertNull($changeStream->current()); $this->assertNull($changeStream->current());
$this->insertDocument(['_id' => 1]); $changeStream->next();
$this->assertFalse($changeStream->valid());
$this->assertNull($changeStream->key());
$this->assertNull($changeStream->current());
// A consecutive resume attempt should still not increment the key
$this->killChangeStreamCursor($changeStream);
$changeStream->next();
$this->assertFalse($changeStream->valid());
$this->assertNull($changeStream->key());
$this->assertNull($changeStream->current());
/* Insert a document and advance the change stream to ensure we capture /* Insert a document and advance the change stream to ensure we capture
* a resume token. This is necessary when startAtOperationTime is not * a resume token. This is necessary when startAtOperationTime is not
* supported (i.e. 3.6 server version). */ * supported (i.e. 3.6 server version). */
$changeStream->next(); $this->insertDocument(['_id' => 1]);
$this->assertTrue($changeStream->valid());
$this->assertSame(0, $changeStream->key());
$this->insertDocument(['_id' => 2]);
/* Killing the cursor and advancing when there is a result will test
* that next()'s resume attempt picks up the latest change. */
$this->killChangeStreamCursor($changeStream);
$changeStream->next(); $changeStream->next();
$this->assertTrue($changeStream->valid()); $this->assertTrue($changeStream->valid());
$this->assertSame(1, $changeStream->key()); $this->assertSame(0, $changeStream->key());
$expectedResult = [ $expectedResult = [
'_id' => $changeStream->current()->_id, '_id' => $changeStream->current()->_id,
'operationType' => 'insert', 'operationType' => 'insert',
'fullDocument' => ['_id' => 2], 'fullDocument' => ['_id' => 1],
'ns' => ['db' => $this->getDatabaseName(), 'coll' => $this->getCollectionName()], 'ns' => ['db' => $this->getDatabaseName(), 'coll' => $this->getCollectionName()],
'documentKey' => ['_id' => 2], 'documentKey' => ['_id' => 1],
]; ];
$this->assertMatchesDocument($expectedResult, $changeStream->current()); $this->assertMatchesDocument($expectedResult, $changeStream->current());
/* Killing the cursor a second time will not trigger a resume until /* Insert another document and kill the cursor. ChangeStream::next()
* ChangeStream::next() is called. A successive call to rewind() should * should resume and pick up the last insert. */
* not change the iterator's state and preserve the current result. $this->insertDocument(['_id' => 2]);
* Note: PHPLIB-448 may require this rewind() to throw an exception. */
$this->killChangeStreamCursor($changeStream); $this->killChangeStreamCursor($changeStream);
$changeStream->rewind(); $changeStream->next();
$this->assertTrue($changeStream->valid()); $this->assertTrue($changeStream->valid());
$this->assertSame(1, $changeStream->key()); $this->assertSame(1, $changeStream->key());
...@@ -444,8 +371,22 @@ class WatchFunctionalTest extends FunctionalTestCase ...@@ -444,8 +371,22 @@ class WatchFunctionalTest extends FunctionalTestCase
$this->assertMatchesDocument($expectedResult, $changeStream->current()); $this->assertMatchesDocument($expectedResult, $changeStream->current());
/* Insert another document and kill the cursor. It is technically
* permissable to call ChangeStream::rewind() since the previous call to
* next() will have left the cursor positioned at its first and only
* result. Assert that rewind() does not execute a getMore nor does it
* modify the iterator's state.
*
* Note: PHPLIB-448 may require rewind() to throw an exception here. */
$this->insertDocument(['_id' => 3]); $this->insertDocument(['_id' => 3]);
$this->killChangeStreamCursor($changeStream);
$this->assertNoCommandExecuted(function() use ($changeStream) { $changeStream->rewind(); });
$this->assertTrue($changeStream->valid());
$this->assertSame(1, $changeStream->key());
$this->assertMatchesDocument($expectedResult, $changeStream->current());
// ChangeStream::next() should resume and pick up the last insert
$changeStream->next(); $changeStream->next();
$this->assertTrue($changeStream->valid()); $this->assertTrue($changeStream->valid());
$this->assertSame(2, $changeStream->key()); $this->assertSame(2, $changeStream->key());
...@@ -460,9 +401,9 @@ class WatchFunctionalTest extends FunctionalTestCase ...@@ -460,9 +401,9 @@ class WatchFunctionalTest extends FunctionalTestCase
$this->assertMatchesDocument($expectedResult, $changeStream->current()); $this->assertMatchesDocument($expectedResult, $changeStream->current());
$this->killChangeStreamCursor($changeStream); // Test one final, consecutive resume via ChangeStream::next()
$this->insertDocument(['_id' => 4]); $this->insertDocument(['_id' => 4]);
$this->killChangeStreamCursor($changeStream);
$changeStream->next(); $changeStream->next();
$this->assertTrue($changeStream->valid()); $this->assertTrue($changeStream->valid());
...@@ -477,28 +418,6 @@ class WatchFunctionalTest extends FunctionalTestCase ...@@ -477,28 +418,6 @@ class WatchFunctionalTest extends FunctionalTestCase
]; ];
$this->assertMatchesDocument($expectedResult, $changeStream->current()); $this->assertMatchesDocument($expectedResult, $changeStream->current());
/* Triggering a consecutive failure will allow us to test whether the
* resume token was properly updated after the last resume. If the
* resume token updated, the next result will be {_id: 4}; otherwise,
* we'll see {_id: 3} returned again. */
$this->killChangeStreamCursor($changeStream);
$this->insertDocument(['_id' => 5]);
$changeStream->next();
$this->assertTrue($changeStream->valid());
$this->assertSame(4, $changeStream->key());
$expectedResult = [
'_id' => $changeStream->current()->_id,
'operationType' => 'insert',
'fullDocument' => ['_id' => 5],
'ns' => ['db' => $this->getDatabaseName(), 'coll' => $this->getCollectionName()],
'documentKey' => ['_id' => 5],
];
$this->assertMatchesDocument($expectedResult, $changeStream->current());
} }
public function testKey() public function testKey()
...@@ -509,9 +428,13 @@ class WatchFunctionalTest extends FunctionalTestCase ...@@ -509,9 +428,13 @@ class WatchFunctionalTest extends FunctionalTestCase
$this->assertFalse($changeStream->valid()); $this->assertFalse($changeStream->valid());
$this->assertNull($changeStream->key()); $this->assertNull($changeStream->key());
$this->assertNoCommandExecuted(function() use ($changeStream) { $changeStream->rewind(); });
$this->assertFalse($changeStream->valid());
$this->assertNull($changeStream->key());
$this->insertDocument(['_id' => 1, 'x' => 'foo']); $this->insertDocument(['_id' => 1, 'x' => 'foo']);
$changeStream->rewind(); $changeStream->next();
$this->assertTrue($changeStream->valid()); $this->assertTrue($changeStream->valid());
$this->assertSame(0, $changeStream->key()); $this->assertSame(0, $changeStream->key());
...@@ -546,6 +469,9 @@ class WatchFunctionalTest extends FunctionalTestCase ...@@ -546,6 +469,9 @@ class WatchFunctionalTest extends FunctionalTestCase
$this->insertDocument(['_id' => 1]); $this->insertDocument(['_id' => 1]);
$changeStream->rewind(); $changeStream->rewind();
$this->assertFalse($changeStream->valid());
$changeStream->next();
$this->assertTrue($changeStream->valid()); $this->assertTrue($changeStream->valid());
$expectedResult = [ $expectedResult = [
...@@ -603,24 +529,6 @@ class WatchFunctionalTest extends FunctionalTestCase ...@@ -603,24 +529,6 @@ class WatchFunctionalTest extends FunctionalTestCase
$changeStream->next(); $changeStream->next();
} }
public function testRewindResumeTokenNotFound()
{
if (version_compare($this->getServerVersion(), '4.1.8', '>=')) {
$this->markTestSkipped('Server rejects change streams that modify resume token (SERVER-37786)');
}
$pipeline = [['$project' => ['_id' => 0 ]]];
$operation = new Watch($this->manager, $this->getDatabaseName(), $this->getCollectionName(), $pipeline, $this->defaultOptions);
$changeStream = $operation->execute($this->getPrimaryServer());
$this->insertDocument(['x' => 1]);
$this->expectException(ResumeTokenException::class);
$this->expectExceptionMessage('Resume token not found in change document');
$changeStream->rewind();
}
public function testNextResumeTokenInvalidType() public function testNextResumeTokenInvalidType()
{ {
if (version_compare($this->getServerVersion(), '4.1.8', '>=')) { if (version_compare($this->getServerVersion(), '4.1.8', '>=')) {
...@@ -641,24 +549,6 @@ class WatchFunctionalTest extends FunctionalTestCase ...@@ -641,24 +549,6 @@ class WatchFunctionalTest extends FunctionalTestCase
$changeStream->next(); $changeStream->next();
} }
public function testRewindResumeTokenInvalidType()
{
if (version_compare($this->getServerVersion(), '4.1.8', '>=')) {
$this->markTestSkipped('Server rejects change streams that modify resume token (SERVER-37786)');
}
$pipeline = [['$project' => ['_id' => ['$literal' => 'foo']]]];
$operation = new Watch($this->manager, $this->getDatabaseName(), $this->getCollectionName(), $pipeline, $this->defaultOptions);
$changeStream = $operation->execute($this->getPrimaryServer());
$this->insertDocument(['x' => 1]);
$this->expectException(ResumeTokenException::class);
$this->expectExceptionMessage('Expected resume token to have type "array or object" but found "string"');
$changeStream->rewind();
}
public function testMaxAwaitTimeMS() public function testMaxAwaitTimeMS()
{ {
/* On average, an acknowledged write takes about 20 ms to appear in a /* On average, an acknowledged write takes about 20 ms to appear in a
...@@ -679,21 +569,18 @@ class WatchFunctionalTest extends FunctionalTestCase ...@@ -679,21 +569,18 @@ class WatchFunctionalTest extends FunctionalTestCase
$operation = new Watch($this->manager, $this->getDatabaseName(), $this->getCollectionName(), [], ['maxAwaitTimeMS' => $maxAwaitTimeMS]); $operation = new Watch($this->manager, $this->getDatabaseName(), $this->getCollectionName(), [], ['maxAwaitTimeMS' => $maxAwaitTimeMS]);
$changeStream = $operation->execute($this->getPrimaryServer()); $changeStream = $operation->execute($this->getPrimaryServer());
/* The initial change stream is empty so we should expect a delay when // Rewinding does not issue a getMore, so we should not expect a delay.
* we call rewind, since it issues a getMore. Expect to wait at least
* maxAwaitTimeMS, since no new documents should be inserted to wake up
* the server's query thread. Also ensure we don't wait too long (server
* default is one second). */
$startTime = microtime(true); $startTime = microtime(true);
$changeStream->rewind(); $changeStream->rewind();
$duration = microtime(true) - $startTime; $duration = microtime(true) - $startTime;
$this->assertGreaterThan($pivot, $duration); $this->assertLessThan($pivot, $duration);
$this->assertLessThan($upperBound, $duration);
$this->assertFalse($changeStream->valid()); $this->assertFalse($changeStream->valid());
/* Advancing again on a change stream will issue a getMore, so we should /* Advancing again on a change stream will issue a getMore, so we should
* expect a delay again. */ * expect a delay. Expect to wait at least maxAwaitTimeMS, since no new
* documents will be inserted to wake up the server's query thread. Also
* ensure we don't wait too long (server default is one second). */
$startTime = microtime(true); $startTime = microtime(true);
$changeStream->next(); $changeStream->next();
$duration = microtime(true) - $startTime; $duration = microtime(true) - $startTime;
...@@ -702,10 +589,10 @@ class WatchFunctionalTest extends FunctionalTestCase ...@@ -702,10 +589,10 @@ class WatchFunctionalTest extends FunctionalTestCase
$this->assertFalse($changeStream->valid()); $this->assertFalse($changeStream->valid());
/* After inserting a document, the change stream will not issue a
* getMore so we should not expect a delay. */
$this->insertDocument(['_id' => 1]); $this->insertDocument(['_id' => 1]);
/* Advancing the change stream again will issue a getMore, but the
* server should not block since a document has been inserted. */
$startTime = microtime(true); $startTime = microtime(true);
$changeStream->next(); $changeStream->next();
$duration = microtime(true) - $startTime; $duration = microtime(true) - $startTime;
...@@ -713,34 +600,38 @@ class WatchFunctionalTest extends FunctionalTestCase ...@@ -713,34 +600,38 @@ class WatchFunctionalTest extends FunctionalTestCase
$this->assertTrue($changeStream->valid()); $this->assertTrue($changeStream->valid());
} }
public function testRewindResumesAfterCursorNotFound() public function testRewindExtractsResumeTokenAndNextResumes()
{ {
$operation = new Watch($this->manager, $this->getDatabaseName(), $this->getCollectionName(), [], $this->defaultOptions); $operation = new Watch($this->manager, $this->getDatabaseName(), $this->getCollectionName(), [], $this->defaultOptions);
$changeStream = $operation->execute($this->getPrimaryServer()); $changeStream = $operation->execute($this->getPrimaryServer());
$this->killChangeStreamCursor($changeStream); $this->insertDocument(['_id' => 1, 'x' => 'foo']);
$this->insertDocument(['_id' => 2, 'x' => 'bar']);
$this->insertDocument(['_id' => 3, 'x' => 'baz']);
/* Obtain a resume token for the first insert. This will allow us to
* start a change stream from that point and ensure aggregate returns
* the second insert in its first batch, which in turn will serve as a
* resume token for rewind() to extract. */
$changeStream->rewind(); $changeStream->rewind();
$this->assertFalse($changeStream->valid()); $this->assertFalse($changeStream->valid());
$this->assertNull($changeStream->current());
}
public function testRewindExtractsResumeTokenAndNextResumes() $changeStream->next();
{ $this->assertTrue($changeStream->valid());
$operation = new Watch($this->manager, $this->getDatabaseName(), $this->getCollectionName(), [], $this->defaultOptions);
$changeStream = $operation->execute($this->getPrimaryServer());
$this->insertDocument(['_id' => 1, 'x' => 'foo']); $options = ['resumeAfter' => $changeStream->current()->_id] + $this->defaultOptions;
$this->insertDocument(['_id' => 2, 'x' => 'bar']); $operation = new Watch($this->manager, $this->getDatabaseName(), $this->getCollectionName(), [], $options);
$changeStream = $operation->execute($this->getPrimaryServer());
$changeStream->rewind(); $changeStream->rewind();
$this->assertTrue($changeStream->valid()); $this->assertTrue($changeStream->valid());
$this->assertSame(0, $changeStream->key());
$expectedResult = [ $expectedResult = [
'_id' => $changeStream->current()->_id, '_id' => $changeStream->current()->_id,
'operationType' => 'insert', 'operationType' => 'insert',
'fullDocument' => ['_id' => 1, 'x' => 'foo'], 'fullDocument' => ['_id' => 2, 'x' => 'bar'],
'ns' => ['db' => $this->getDatabaseName(), 'coll' => $this->getCollectionName()], 'ns' => ['db' => $this->getDatabaseName(), 'coll' => $this->getCollectionName()],
'documentKey' => ['_id' => 1], 'documentKey' => ['_id' => 2],
]; ];
$this->assertMatchesDocument($expectedResult, $changeStream->current()); $this->assertMatchesDocument($expectedResult, $changeStream->current());
...@@ -748,13 +639,14 @@ class WatchFunctionalTest extends FunctionalTestCase ...@@ -748,13 +639,14 @@ class WatchFunctionalTest extends FunctionalTestCase
$changeStream->next(); $changeStream->next();
$this->assertTrue($changeStream->valid()); $this->assertTrue($changeStream->valid());
$this->assertSame(1, $changeStream->key());
$expectedResult = [ $expectedResult = [
'_id' => $changeStream->current()->_id, '_id' => $changeStream->current()->_id,
'operationType' => 'insert', 'operationType' => 'insert',
'fullDocument' => ['_id' => 2, 'x' => 'bar'], 'fullDocument' => ['_id' => 3, 'x' => 'baz'],
'ns' => ['db' => $this->getDatabaseName(), 'coll' => $this->getCollectionName()], 'ns' => ['db' => $this->getDatabaseName(), 'coll' => $this->getCollectionName()],
'documentKey' => ['_id' => 2], 'documentKey' => ['_id' => 3],
]; ];
$this->assertMatchesDocument($expectedResult, $changeStream->current()); $this->assertMatchesDocument($expectedResult, $changeStream->current());
} }
...@@ -840,7 +732,7 @@ class WatchFunctionalTest extends FunctionalTestCase ...@@ -840,7 +732,7 @@ class WatchFunctionalTest extends FunctionalTestCase
$changeStream = $operation->execute($this->getPrimaryServer()); $changeStream = $operation->execute($this->getPrimaryServer());
$changeStream->rewind(); $changeStream->rewind();
$this->assertNull($changeStream->current()); $this->assertFalse($changeStream->valid());
$this->insertDocument(['_id' => 1, 'x' => 'foo']); $this->insertDocument(['_id' => 1, 'x' => 'foo']);
...@@ -918,12 +810,8 @@ class WatchFunctionalTest extends FunctionalTestCase ...@@ -918,12 +810,8 @@ class WatchFunctionalTest extends FunctionalTestCase
$this->insertDocument(['x' => 2]); $this->insertDocument(['x' => 2]);
$this->insertDocument(['x' => 3]); $this->insertDocument(['x' => 3]);
try { $changeStream->rewind();
$changeStream->rewind(); $this->assertFalse($changeStream->valid());
$this->fail('ResumeTokenException was not thrown');
} catch (ResumeTokenException $e) {}
$this->assertSame(0, $changeStream->key());
try { try {
$changeStream->next(); $changeStream->next();
...@@ -990,9 +878,6 @@ class WatchFunctionalTest extends FunctionalTestCase ...@@ -990,9 +878,6 @@ class WatchFunctionalTest extends FunctionalTestCase
* removes the last reference to the old cursor, which causes the * removes the last reference to the old cursor, which causes the
* driver to kill it (via mongoc_cursor_destroy()). */ * driver to kill it (via mongoc_cursor_destroy()). */
'killCursors', 'killCursors',
/* Finally, ChangeStream will rewind the new cursor as the last step
* of the resume process. This results in one last getMore. */
'getMore',
]; ];
$this->assertSame($expectedCommands, $commands); $this->assertSame($expectedCommands, $commands);
...@@ -1024,9 +909,28 @@ class WatchFunctionalTest extends FunctionalTestCase ...@@ -1024,9 +909,28 @@ class WatchFunctionalTest extends FunctionalTestCase
$this->assertNull($rp->getValue($changeStream)); $this->assertNull($rp->getValue($changeStream));
} }
private function assertNoCommandExecuted(callable $callable)
{
$commands = [];
(new CommandObserver)->observe(
$callable,
function(array $event) use (&$commands) {
$this->fail(sprintf('"%s" command was executed', $event['started']->getCommandName()));
}
);
$this->assertEmpty($commands);
}
private function insertDocument($document) private function insertDocument($document)
{ {
$insertOne = new InsertOne($this->getDatabaseName(), $this->getCollectionName(), $document); $insertOne = new InsertOne(
$this->getDatabaseName(),
$this->getCollectionName(),
$document,
['writeConcern' => new WriteConcern(WriteConcern::MAJORITY)]
);
$writeResult = $insertOne->execute($this->getPrimaryServer()); $writeResult = $insertOne->execute($this->getPrimaryServer());
$this->assertEquals(1, $writeResult->getInsertedCount()); $this->assertEquals(1, $writeResult->getInsertedCount());
} }
......
...@@ -54,10 +54,11 @@ class ChangeStreamsProseTest extends FunctionalTestCase ...@@ -54,10 +54,11 @@ class ChangeStreamsProseTest extends FunctionalTestCase
$this->createCollection(); $this->createCollection();
$changeStream = $this->collection->watch(); $changeStream = $this->collection->watch();
$changeStream->rewind();
$this->expectException(ServerException::class); $this->expectException(ServerException::class);
$this->expectExceptionCode($errorCode); $this->expectExceptionCode($errorCode);
$changeStream->rewind(); $changeStream->next();
} }
public function provideNonResumableErrorCodes() public function provideNonResumableErrorCodes()
......
...@@ -233,22 +233,33 @@ class ChangeStreamsSpecTest extends FunctionalTestCase ...@@ -233,22 +233,33 @@ class ChangeStreamsSpecTest extends FunctionalTestCase
* Iterate a change stream. * Iterate a change stream.
* *
* @param ChangeStream $changeStream * @param ChangeStream $changeStream
* @param integer $limit
* @return BSONDocument[] * @return BSONDocument[]
*/ */
private function iterateChangeStream(ChangeStream $changeStream, $limit = 0) private function iterateChangeStream(ChangeStream $changeStream, $limit = 0)
{ {
if ($limit < 0) {
throw new LogicException('$limit is negative');
}
/* Limit iterations to guard against an infinite loop should a test fail
* to return as many results as are expected. Require at least one
* iteration to allow next() a chance to throw for error tests. */
$maxIterations = $limit + 1;
$events = []; $events = [];
for ($changeStream->rewind(); count($events) < $limit; $changeStream->next()) { for ($i = 0, $changeStream->rewind(); $i < $maxIterations; $i++, $changeStream->next()) {
if ( ! $changeStream->valid()) { if ( ! $changeStream->valid()) {
continue; continue;
} }
$event = $changeStream->current(); $event = $changeStream->current();
$this->assertInstanceOf(BSONDocument::class, $event); $this->assertInstanceOf(BSONDocument::class, $event);
$events[] = $event; $events[] = $event;
if (count($events) >= $limit) {
break;
}
} }
return $events; return $events;
......
...@@ -6,6 +6,7 @@ use MongoDB\Collection; ...@@ -6,6 +6,7 @@ use MongoDB\Collection;
use MongoDB\Database; use MongoDB\Database;
use MongoDB\Driver\Cursor; use MongoDB\Driver\Cursor;
use MongoDB\Driver\Session; use MongoDB\Driver\Session;
use MongoDB\Driver\WriteConcern;
use MongoDB\Driver\Exception\BulkWriteException; use MongoDB\Driver\Exception\BulkWriteException;
use MongoDB\Driver\Exception\Exception; use MongoDB\Driver\Exception\Exception;
use MongoDB\Operation\FindOneAndReplace; use MongoDB\Operation\FindOneAndReplace;
...@@ -53,6 +54,11 @@ final class Operation ...@@ -53,6 +54,11 @@ final class Operation
{ {
$o = new self($operation); $o = new self($operation);
/* Note: change streams only return majority-committed writes, so ensure
* each operation applies that write concern. This will avoid spurious
* test failures. */
$writeConcern = new WriteConcern(WriteConcern::MAJORITY);
// Expect all operations to succeed // Expect all operations to succeed
$o->errorExpectation = ErrorExpectation::noError(); $o->errorExpectation = ErrorExpectation::noError();
...@@ -66,6 +72,8 @@ final class Operation ...@@ -66,6 +72,8 @@ final class Operation
$o->arguments = ['command' => [ $o->arguments = ['command' => [
'renameCollection' => $operation->database . '.' . $operation->collection, 'renameCollection' => $operation->database . '.' . $operation->collection,
'to' => $operation->database . '.' . $operation->arguments->to, 'to' => $operation->database . '.' . $operation->arguments->to,
// Note: Database::command() does not inherit WC, so be explicit
'writeConcern' => $writeConcern,
]]; ]];
return $o; return $o;
...@@ -73,6 +81,7 @@ final class Operation ...@@ -73,6 +81,7 @@ final class Operation
$o->databaseName = $operation->database; $o->databaseName = $operation->database;
$o->collectionName = $operation->collection; $o->collectionName = $operation->collection;
$o->collectionOptions = ['writeConcern' => $writeConcern];
$o->object = self::OBJECT_SELECT_COLLECTION; $o->object = self::OBJECT_SELECT_COLLECTION;
return $o; return $o;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment