Commit 9ccfae84 authored by Jeremy Mikola's avatar Jeremy Mikola

PHPLIB-351: Cluster and DB-level change streams and startAtOperationTime

parent 0f848cbd
......@@ -29,6 +29,7 @@ use MongoDB\Exception\UnsupportedException;
use MongoDB\Model\DatabaseInfoIterator;
use MongoDB\Operation\DropDatabase;
use MongoDB\Operation\ListDatabases;
use MongoDB\Operation\Watch;
class Client
{
......@@ -37,9 +38,12 @@ class Client
'document' => 'MongoDB\Model\BSONDocument',
'root' => 'MongoDB\Model\BSONDocument',
];
private static $wireVersionForReadConcern = 4;
private static $wireVersionForWritableCommandWriteConcern = 5;
private $manager;
private $readConcern;
private $readPreference;
private $uri;
private $typeMap;
private $writeConcern;
......@@ -81,6 +85,8 @@ class Client
unset($driverOptions['typeMap']);
$this->manager = new Manager($uri, $uriOptions, $driverOptions);
$this->readConcern = $this->manager->getReadConcern();
$this->readPreference = $this->manager->getReadPreference();
$this->writeConcern = $this->manager->getWriteConcern();
}
......@@ -173,7 +179,7 @@ class Client
*/
public function getReadConcern()
{
return $this->manager->getReadConcern();
return $this->readConcern;
}
/**
......@@ -183,7 +189,7 @@ class Client
*/
public function getReadPreference()
{
return $this->manager->getReadPreference();
return $this->readPreference;
}
/**
......@@ -268,4 +274,34 @@ class Client
{
return $this->manager->startSession($options);
}
/**
* Create a change stream for watching changes to the cluster.
*
* @see Watch::__construct() for supported options
* @param array $pipeline List of pipeline operations
* @param array $options Command options
* @return ChangeStream
* @throws InvalidArgumentException for parameter/option parsing errors
*/
public function watch(array $pipeline = [], array $options = [])
{
if ( ! isset($options['readPreference'])) {
$options['readPreference'] = $this->readPreference;
}
$server = $this->manager->selectServer($options['readPreference']);
if ( ! isset($options['readConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
$options['readConcern'] = $this->readConcern;
}
if ( ! isset($options['typeMap'])) {
$options['typeMap'] = $this->typeMap;
}
$operation = new Watch($this->manager, null, null, $pipeline, $options);
return $operation->execute($server);
}
}
......@@ -34,6 +34,7 @@ use MongoDB\Operation\DropCollection;
use MongoDB\Operation\DropDatabase;
use MongoDB\Operation\ListCollections;
use MongoDB\Operation\ModifyCollection;
use MongoDB\Operation\Watch;
class Database
{
......@@ -42,6 +43,7 @@ class Database
'document' => 'MongoDB\Model\BSONDocument',
'root' => 'MongoDB\Model\BSONDocument',
];
private static $wireVersionForReadConcern = 4;
private static $wireVersionForWritableCommandWriteConcern = 5;
private $databaseName;
......@@ -409,6 +411,36 @@ class Database
return new Bucket($this->manager, $this->databaseName, $options);
}
/**
* Create a change stream for watching changes to the database.
*
* @see Watch::__construct() for supported options
* @param array $pipeline List of pipeline operations
* @param array $options Command options
* @return ChangeStream
* @throws InvalidArgumentException for parameter/option parsing errors
*/
public function watch(array $pipeline = [], array $options = [])
{
if ( ! isset($options['readPreference'])) {
$options['readPreference'] = $this->readPreference;
}
$server = $this->manager->selectServer($options['readPreference']);
if ( ! isset($options['readConcern']) && \MongoDB\server_supports_feature($server, self::$wireVersionForReadConcern)) {
$options['readConcern'] = $this->readConcern;
}
if ( ! isset($options['typeMap'])) {
$options['typeMap'] = $this->typeMap;
}
$operation = new Watch($this->manager, $this->databaseName, null, $pipeline, $options);
return $operation->execute($server);
}
/**
* Get a clone of this database with different options.
*
......
This diff is collapsed.
......@@ -3,6 +3,7 @@
namespace MongoDB\Tests\Operation;
use MongoDB\ChangeStream;
use MongoDB\BSON\TimestampInterface;
use MongoDB\Driver\Manager;
use MongoDB\Driver\ReadPreference;
use MongoDB\Driver\Server;
......@@ -130,6 +131,100 @@ class WatchFunctionalTest extends FunctionalTestCase
$this->assertSame($expectedCommands, $commands);
}
public function testResumeBeforeReceivingAnyResultsIncludesStartAtOperationTime()
{
$operation = new Watch($this->manager, $this->getDatabaseName(), $this->getCollectionName(), [], $this->defaultOptions);
$operationTime = null;
$events = [];
(new CommandObserver)->observe(
function() use ($operation, &$changeStream) {
$changeStream = $operation->execute($this->getPrimaryServer());
},
function (array $event) use (&$events) {
$events[] = $event;
}
);
$this->assertCount(1, $events);
$this->assertSame('aggregate', $events[0]['started']->getCommandName());
$operationTime = $events[0]['succeeded']->getReply()->operationTime;
$this->assertInstanceOf(TimestampInterface::class, $operationTime);
$this->assertNull($changeStream->current());
$this->killChangeStreamCursor($changeStream);
$events = [];
(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 = [];
(new CommandObserver)->observe(
function() use ($changeStream) {
$changeStream->next();
},
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());
}
private function assertStartAtOperationTime(TimestampInterface $expectedOperationTime, stdClass $command)
{
$this->assertObjectHasAttribute('pipeline', $command);
$this->assertInternalType('array', $command->pipeline);
$this->assertArrayHasKey(0, $command->pipeline);
$this->assertObjectHasAttribute('$changeStream', $command->pipeline[0]);
$this->assertObjectHasAttribute('startAtOperationTime', $command->pipeline[0]->{'$changeStream'});
$this->assertEquals($expectedOperationTime, $command->pipeline[0]->{'$changeStream'}->startAtOperationTime);
}
public function testRewindResumesAfterConnectionException()
{
/* In order to trigger a dropped connection, we'll use a new client with
......
......@@ -4,6 +4,7 @@ namespace MongoDB\Tests\Operation;
use MongoDB\Exception\InvalidArgumentException;
use MongoDB\Operation\Watch;
use stdClass;
/**
* Although these are unit tests, we extend FunctionalTestCase because Watch is
......@@ -11,6 +12,14 @@ use MongoDB\Operation\Watch;
*/
class WatchTest extends FunctionalTestCase
{
public function testConstructorCollectionNameShouldBeNullIfDatabaseNameIsNull()
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('$collectionName should also be null if $databaseName is null');
new Watch($this->manager, null, 'foo', []);
}
public function testConstructorPipelineArgumentMustBeAList()
{
$this->expectException(InvalidArgumentException::class);
......@@ -67,10 +76,19 @@ class WatchTest extends FunctionalTestCase
$options[][] = ['session' => $value];
}
foreach ($this->getInvalidTimestampValues() as $value) {
$options[][] = ['startAtOperationTime' => $value];
}
foreach ($this->getInvalidArrayValues() as $value) {
$options[][] = ['typeMap' => $value];
}
return $options;
}
private function getInvalidTimestampValues()
{
return [123, 3.14, 'foo', true, [], new stdClass];
}
}
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