Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Contribute to GitLab
Sign in
Toggle navigation
M
mongo-php-library
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
sinan
mongo-php-library
Commits
8c7a92cc
Commit
8c7a92cc
authored
Jul 09, 2019
by
Jeremy Mikola
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #636
parents
80cee6f6
84c365c0
Show whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
364 additions
and
269 deletions
+364
-269
ChangeStream.php
src/ChangeStream.php
+9
-14
TailableCursorIterator.php
src/Model/TailableCursorIterator.php
+80
-0
Watch.php
src/Operation/Watch.php
+28
-18
TailableCursorIteratorTest.php
tests/Model/TailableCursorIteratorTest.php
+85
-0
WatchFunctionalTest.php
tests/Operation/WatchFunctionalTest.php
+137
-233
ChangeStreamsProseTest.php
tests/SpecTests/ChangeStreamsProseTest.php
+2
-1
ChangeStreamsSpecTest.php
tests/SpecTests/ChangeStreamsSpecTest.php
+14
-3
Operation.php
tests/SpecTests/Operation.php
+9
-0
No files found.
src/ChangeStream.php
View file @
8c7a92cc
...
@@ -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
Iterat
orIterator
;
use
MongoDB\Model\TailableCurs
orIterator
;
use
Iterator
;
use
Iterator
;
/**
/**
...
@@ -63,11 +63,12 @@ class ChangeStream implements Iterator
...
@@ -63,11 +63,12 @@ class ChangeStream implements Iterator
* @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
);
}
}
...
...
src/Model/TailableCursorIterator.php
0 → 100644
View file @
8c7a92cc
<?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
();
}
}
src/Operation/Watch.php
View file @
8c7a92cc
...
@@ -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
{
...
...
tests/Model/TailableCursorIteratorTest.php
0 → 100644
View file @
8c7a92cc
<?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
);
}
}
tests/Operation/WatchFunctionalTest.php
View file @
8c7a92cc
...
@@ -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\ConnectionTimeoutExceptio
n
;
use
MongoDB\Driver\
WriteConcer
n
;
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
->
assert
Null
(
$changeStream
->
current
());
$this
->
assert
False
(
$changeStream
->
valid
());
$this
->
insertDocument
([
'_id'
=>
2
,
'x'
=>
'bar'
]);
$this
->
insertDocument
([
'_id'
=>
2
,
'x'
=>
'bar'
]);
...
@@ -84,13 +84,10 @@ class WatchFunctionalTest extends FunctionalTestCase
...
@@ -84,13 +84,10 @@ 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
();
...
@@ -99,15 +96,13 @@ class WatchFunctionalTest extends FunctionalTestCase
...
@@ -99,15 +96,13 @@ class WatchFunctionalTest extends FunctionalTestCase
$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
->
assert
Null
(
$changeStream
->
current
());
$this
->
assert
False
(
$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
->
assert
Null
(
$changeStream
->
current
());
$this
->
assert
False
(
$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
testRewind
ResumesAfterCursorNotFound
()
public
function
testRewind
ExtractsResumeTokenAndNextResumes
()
{
{
$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
->
assert
Null
(
$changeStream
->
current
());
$this
->
assert
False
(
$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
->
fail
(
'ResumeTokenException was not thrown'
);
$this
->
assertFalse
(
$changeStream
->
valid
());
}
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
());
}
}
...
...
tests/SpecTests/ChangeStreamsProseTest.php
View file @
8c7a92cc
...
@@ -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
()
...
...
tests/SpecTests/ChangeStreamsSpecTest.php
View file @
8c7a92cc
...
@@ -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
;
...
...
tests/SpecTests/Operation.php
View file @
8c7a92cc
...
@@ -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
;
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment