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
Expand all
Hide 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
;
/**
/**
...
@@ -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
);
}
}
...
...
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
This diff is collapsed.
Click to expand it.
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