diff options
author | samantharitter <samantha.ritter@10gen.com> | 2017-06-06 13:49:35 -0400 |
---|---|---|
committer | samantharitter <samantha.ritter@10gen.com> | 2017-06-15 13:54:58 -0400 |
commit | eddb3e4535a9bf9198b207bf155889bf4625cdf0 (patch) | |
tree | d6b941bb582d5abbfef293503727d5c340410c54 /src | |
parent | 1efbf03521e9c6d85df33ab6786f6c121d8b56c2 (diff) | |
download | mongo-eddb3e4535a9bf9198b207bf155889bf4625cdf0.tar.gz |
SERVER-29417 Attach logical session ids to cursors
Diffstat (limited to 'src')
-rw-r--r-- | src/mongo/db/SConscript | 1 | ||||
-rw-r--r-- | src/mongo/db/clientcursor.cpp | 4 | ||||
-rw-r--r-- | src/mongo/db/clientcursor.h | 13 | ||||
-rw-r--r-- | src/mongo/db/cursor_manager.cpp | 69 | ||||
-rw-r--r-- | src/mongo/db/cursor_manager.h | 17 | ||||
-rw-r--r-- | src/mongo/db/db_raii.cpp | 8 | ||||
-rw-r--r-- | src/mongo/db/db_raii.h | 43 | ||||
-rw-r--r-- | src/mongo/db/logical_session_id.h | 1 | ||||
-rw-r--r-- | src/mongo/db/query/SConscript | 1 | ||||
-rw-r--r-- | src/mongo/db/query/query_test_service_context.cpp | 9 | ||||
-rw-r--r-- | src/mongo/db/query/query_test_service_context.h | 5 | ||||
-rw-r--r-- | src/mongo/dbtests/cursor_manager_test.cpp | 189 |
12 files changed, 354 insertions, 6 deletions
diff --git a/src/mongo/db/SConscript b/src/mongo/db/SConscript index 5d2149b9e55..c7649153f78 100644 --- a/src/mongo/db/SConscript +++ b/src/mongo/db/SConscript @@ -613,6 +613,7 @@ env.Library( ], LIBDEPS=[ "$BUILD_DIR/mongo/base", + "$BUILD_DIR/mongo/db/logical_session_id", "$BUILD_DIR/mongo/util/background_job", "query/query", "background", diff --git a/src/mongo/db/clientcursor.cpp b/src/mongo/db/clientcursor.cpp index b6a686ba717..889d9961607 100644 --- a/src/mongo/db/clientcursor.cpp +++ b/src/mongo/db/clientcursor.cpp @@ -77,13 +77,15 @@ long long ClientCursor::totalOpen() { return cursorStatsOpen.get(); } -ClientCursor::ClientCursor(ClientCursorParams&& params, +ClientCursor::ClientCursor(ClientCursorParams params, CursorManager* cursorManager, CursorId cursorId, + boost::optional<LogicalSessionId> lsid, Date_t now) : _cursorid(cursorId), _nss(std::move(params.nss)), _authenticatedUsers(std::move(params.authenticatedUsers)), + _lsid(std::move(lsid)), _isReadCommitted(params.isReadCommitted), _cursorManager(cursorManager), _originatingCommand(params.originatingCommandObj), diff --git a/src/mongo/db/clientcursor.h b/src/mongo/db/clientcursor.h index d2f216b01e1..32fb018a8e1 100644 --- a/src/mongo/db/clientcursor.h +++ b/src/mongo/db/clientcursor.h @@ -28,10 +28,13 @@ #pragma once +#include <boost/optional.hpp> + #include "mongo/client/dbclientinterface.h" #include "mongo/db/auth/user_name.h" #include "mongo/db/cursor_id.h" #include "mongo/db/jsobj.h" +#include "mongo/db/logical_session_id.h" #include "mongo/db/query/plan_executor.h" #include "mongo/db/record_id.h" #include "mongo/stdx/functional.h" @@ -108,6 +111,10 @@ public: return makeUserNameIterator(_authenticatedUsers.begin(), _authenticatedUsers.end()); } + boost::optional<LogicalSessionId> getSessionId() const { + return _lsid; + } + bool isReadCommitted() const { return _isReadCommitted; } @@ -218,9 +225,10 @@ private: * Constructs a ClientCursor. Since cursors must come into being registered and pinned, this is * private. See cursor_manager.h for more details. */ - ClientCursor(ClientCursorParams&& params, + ClientCursor(ClientCursorParams params, CursorManager* cursorManager, CursorId cursorId, + boost::optional<LogicalSessionId> lsid, Date_t now); /** @@ -257,6 +265,9 @@ private: // The set of authenticated users when this cursor was created. std::vector<UserName> _authenticatedUsers; + // A logical session id for this cursor, if it is running inside of a session. + const boost::optional<LogicalSessionId> _lsid; + const bool _isReadCommitted = false; CursorManager* _cursorManager; diff --git a/src/mongo/db/cursor_manager.cpp b/src/mongo/db/cursor_manager.cpp index f1bdd8a9442..a2e9687f5b1 100644 --- a/src/mongo/db/cursor_manager.cpp +++ b/src/mongo/db/cursor_manager.cpp @@ -101,6 +101,8 @@ public: std::size_t timeoutCursors(OperationContext* opCtx, Date_t now); + void appendActiveSessions(OperationContext* opCtx, LogicalSessionIdSet* lsids); + int64_t nextSeed(); private: @@ -267,12 +269,47 @@ std::size_t GlobalCursorIdCache::timeoutCursors(OperationContext* opCtx, Date_t } } // namespace +void GlobalCursorIdCache::appendActiveSessions(OperationContext* opCtx, + LogicalSessionIdSet* lsids) { + // Get active session ids from the global cursor manager + globalCursorManager->appendActiveSessions(lsids); + + // Compute the set of collection names that we have to get sessions for + vector<NamespaceString> namespaces; + { + stdx::lock_guard<SimpleMutex> lk(_mutex); + for (auto&& entry : _idToNss) { + namespaces.push_back(entry.second); + } + } + + // For each collection, get its sessions under the collection lock (to prevent the + // collection from going away during the erase). + for (auto&& ns : namespaces) { + AutoGetCollectionOrView ctx(opCtx, NamespaceString(ns), MODE_IS); + if (!ctx.getDb()) { + continue; + } + + Collection* collection = ctx.getCollection(); + if (!collection) { + continue; + } + + collection->getCursorManager()->appendActiveSessions(lsids); + } +} + // --- CursorManager* CursorManager::getGlobalCursorManager() { return globalCursorManager.get(); } +void CursorManager::appendAllActiveSessions(OperationContext* opCtx, LogicalSessionIdSet* lsids) { + globalCursorIdCache->appendActiveSessions(opCtx, lsids); +} + std::size_t CursorManager::timeoutCursorsGlobal(OperationContext* opCtx, Date_t now) { return globalCursorIdCache->timeoutCursors(opCtx, now); } @@ -482,6 +519,34 @@ void CursorManager::getCursorIds(std::set<CursorId>* openCursors) const { } } +void CursorManager::appendActiveSessions(LogicalSessionIdSet* lsids) const { + auto allPartitions = _cursorMap->lockAllPartitions(); + for (auto&& partition : allPartitions) { + for (auto&& entry : partition) { + auto cursor = entry.second; + if (auto id = cursor->getSessionId()) { + lsids->insert(id.value()); + } + } + } +} + +stdx::unordered_set<CursorId> CursorManager::getCursorsForSession(LogicalSessionId lsid) const { + stdx::unordered_set<CursorId> cursors; + + auto allPartitions = _cursorMap->lockAllPartitions(); + for (auto&& partition : allPartitions) { + for (auto&& entry : partition) { + auto cursor = entry.second; + if (cursor->getSessionId() == lsid) { + cursors.insert(cursor->cursorid()); + } + } + } + + return cursors; +} + size_t CursorManager::numCursors() const { return _cursorMap->size(); } @@ -526,8 +591,8 @@ ClientCursorPin CursorManager::registerCursor(OperationContext* opCtx, // we don't insert two cursors with the same cursor id. stdx::lock_guard<SimpleMutex> lock(_registrationLock); CursorId cursorId = allocateCursorId_inlock(); - std::unique_ptr<ClientCursor, ClientCursor::Deleter> clientCursor( - new ClientCursor(std::move(cursorParams), this, cursorId, now)); + std::unique_ptr<ClientCursor, ClientCursor::Deleter> clientCursor(new ClientCursor( + std::move(cursorParams), this, cursorId, opCtx->getLogicalSessionId(), now)); // Transfer ownership of the cursor to '_cursorMap'. auto partition = _cursorMap->lockOnePartition(cursorId); diff --git a/src/mongo/db/cursor_manager.h b/src/mongo/db/cursor_manager.h index 40408c48830..0b243e21996 100644 --- a/src/mongo/db/cursor_manager.h +++ b/src/mongo/db/cursor_manager.h @@ -36,6 +36,7 @@ #include "mongo/db/record_id.h" #include "mongo/platform/unordered_map.h" #include "mongo/platform/unordered_set.h" +#include "mongo/stdx/unordered_set.h" #include "mongo/util/concurrency/mutex.h" #include "mongo/util/duration.h" @@ -78,6 +79,12 @@ public: static constexpr Minutes kDefaultCursorTimeoutMinutes{10}; using RegistrationToken = Partitioned<unordered_set<PlanExecutor*>>::PartitionId; + /** + * Appends the sessions that have open cursors on the global cursor manager and across + * all collection-level cursor managers to the given set of lsids. + */ + static void appendAllActiveSessions(OperationContext* opCtx, LogicalSessionIdSet* lsids); + CursorManager(NamespaceString nss); /** @@ -159,6 +166,16 @@ public: void getCursorIds(std::set<CursorId>* openCursors) const; /** + * Appends sessions that have open cursors in this cursor manager to the given set of lsids. + */ + void appendActiveSessions(LogicalSessionIdSet* lsids) const; + + /* + * Returns a list of all open cursors for the given session. + */ + stdx::unordered_set<CursorId> getCursorsForSession(LogicalSessionId lsid) const; + + /** * Returns the number of ClientCursors currently registered. Excludes any registered bare * PlanExecutors. */ diff --git a/src/mongo/db/db_raii.cpp b/src/mongo/db/db_raii.cpp index 478047eb0cc..bff855fcc5b 100644 --- a/src/mongo/db/db_raii.cpp +++ b/src/mongo/db/db_raii.cpp @@ -72,6 +72,14 @@ AutoGetCollection::AutoGetCollection(OperationContext* opCtx, } } +AutoGetCollectionOrView::AutoGetCollectionOrView(OperationContext* opCtx, + const NamespaceString& nss, + LockMode modeAll) + : _autoColl(opCtx, nss, modeAll, modeAll, AutoGetCollection::ViewMode::kViewsPermitted), + _view(_autoColl.getDb() && !_autoColl.getCollection() + ? _autoColl.getDb()->getViewCatalog()->lookup(opCtx, nss.ns()) + : nullptr) {} + AutoGetOrCreateDb::AutoGetOrCreateDb(OperationContext* opCtx, StringData ns, LockMode mode) : _dbLock(opCtx, ns, mode), _db(dbHolder().get(opCtx, ns)) { invariant(mode == MODE_IX || mode == MODE_X); diff --git a/src/mongo/db/db_raii.h b/src/mongo/db/db_raii.h index 753909e3c6a..641b64e7988 100644 --- a/src/mongo/db/db_raii.h +++ b/src/mongo/db/db_raii.h @@ -128,12 +128,55 @@ private: const Lock::CollectionLock _collLock; Collection* const _coll; + friend class AutoGetCollectionOrView; friend class AutoGetCollectionForRead; friend class AutoGetCollectionForReadCommand; friend class AutoGetCollectionOrViewForReadCommand; }; /** + * RAII-style class which acquires the appropriate hierarchy of locks for a collection or + * view. The pointer to a view definition is nullptr if it does not exist. + * + * Use this when you have not yet determined if the namespace is a view or a collection. + * For example, you can use this to access a namespace's CursorManager. + * + * It is guaranteed that locks will be released when this object goes out of scope, therefore + * the view returned by this class should not be retained. + */ +class AutoGetCollectionOrView { + MONGO_DISALLOW_COPYING(AutoGetCollectionOrView); + +public: + AutoGetCollectionOrView(OperationContext* opCtx, const NamespaceString& nss, LockMode modeAll); + + /** + * Returns nullptr if the database didn't exist. + */ + Database* getDb() const { + return _autoColl.getDb(); + } + + /** + * Returns nullptr if the collection didn't exist. + */ + Collection* getCollection() const { + return _autoColl.getCollection(); + } + + /** + * Returns nullptr if the view didn't exist. + */ + ViewDefinition* getView() const { + return _view.get(); + } + +private: + const AutoGetCollection _autoColl; + std::shared_ptr<ViewDefinition> _view; +}; + +/** * RAII-style class, which acquires a lock on the specified database in the requested mode and * obtains a reference to the database, creating it was non-existing. Used as a shortcut for * calls to dbHolder().openDb(), taking care of locking details. The requested mode must be diff --git a/src/mongo/db/logical_session_id.h b/src/mongo/db/logical_session_id.h index eca1d8a33f5..f1fb85ce89e 100644 --- a/src/mongo/db/logical_session_id.h +++ b/src/mongo/db/logical_session_id.h @@ -32,6 +32,7 @@ #include "mongo/base/status_with.h" #include "mongo/db/logical_session_id_gen.h" +#include "mongo/stdx/unordered_set.h" #include "mongo/util/uuid.h" namespace mongo { diff --git a/src/mongo/db/query/SConscript b/src/mongo/db/query/SConscript index 7c42568f7c0..854f3045506 100644 --- a/src/mongo/db/query/SConscript +++ b/src/mongo/db/query/SConscript @@ -255,6 +255,7 @@ env.Library( ], LIBDEPS=[ "collation/collator_factory_mock", + "$BUILD_DIR/mongo/db/logical_session_id", "$BUILD_DIR/mongo/db/service_context", ], ) diff --git a/src/mongo/db/query/query_test_service_context.cpp b/src/mongo/db/query/query_test_service_context.cpp index e2682fe44d4..4ad6ac8e857 100644 --- a/src/mongo/db/query/query_test_service_context.cpp +++ b/src/mongo/db/query/query_test_service_context.cpp @@ -44,4 +44,13 @@ ServiceContext::UniqueOperationContext QueryTestServiceContext::makeOperationCon return _uniqueClient->makeOperationContext(); } +ServiceContext::UniqueOperationContext QueryTestServiceContext::makeOperationContext( + LogicalSessionId lsid) { + return _uniqueClient->makeOperationContext(std::move(lsid)); +} + +Client* QueryTestServiceContext::getClient() const { + return _uniqueClient.get(); +} + } // namespace mongo diff --git a/src/mongo/db/query/query_test_service_context.h b/src/mongo/db/query/query_test_service_context.h index 7466116d354..658b73ef36a 100644 --- a/src/mongo/db/query/query_test_service_context.h +++ b/src/mongo/db/query/query_test_service_context.h @@ -29,6 +29,7 @@ #pragma once #include "mongo/db/client.h" +#include "mongo/db/logical_session_id.h" #include "mongo/db/service_context_noop.h" namespace mongo { @@ -44,6 +45,10 @@ public: ServiceContext::UniqueOperationContext makeOperationContext(); + ServiceContext::UniqueOperationContext makeOperationContext(LogicalSessionId lsid); + + Client* getClient() const; + private: ServiceContextNoop _serviceContext; ServiceContext::UniqueClient _uniqueClient; diff --git a/src/mongo/dbtests/cursor_manager_test.cpp b/src/mongo/dbtests/cursor_manager_test.cpp index 07ec00df8fc..9b0d495e5df 100644 --- a/src/mongo/dbtests/cursor_manager_test.cpp +++ b/src/mongo/dbtests/cursor_manager_test.cpp @@ -28,6 +28,11 @@ #include "mongo/platform/basic.h" +#include <algorithm> + +#include <boost/optional/optional.hpp> +#include <boost/optional/optional_io.hpp> + #include "mongo/db/client.h" #include "mongo/db/clientcursor.h" #include "mongo/db/cursor_manager.h" @@ -35,6 +40,7 @@ #include "mongo/db/exec/working_set.h" #include "mongo/db/exec/working_set_common.h" #include "mongo/db/operation_context.h" +#include "mongo/db/operation_context_noop.h" #include "mongo/db/query/plan_executor.h" #include "mongo/db/query/query_test_service_context.h" #include "mongo/dbtests/dbtests.h" @@ -85,15 +91,28 @@ public: } std::unique_ptr<PlanExecutor, PlanExecutor::Deleter> makeFakePlanExecutor() { + return makeFakePlanExecutor(_opCtx.get()); + } + + std::unique_ptr<PlanExecutor, PlanExecutor::Deleter> makeFakePlanExecutor( + OperationContext* opCtx) { auto workingSet = stdx::make_unique<WorkingSet>(); - auto queuedDataStage = stdx::make_unique<QueuedDataStage>(_opCtx.get(), workingSet.get()); - return unittest::assertGet(PlanExecutor::make(_opCtx.get(), + auto queuedDataStage = stdx::make_unique<QueuedDataStage>(opCtx, workingSet.get()); + return unittest::assertGet(PlanExecutor::make(opCtx, std::move(workingSet), std::move(queuedDataStage), kTestNss, PlanExecutor::YieldPolicy::NO_YIELD)); } + ClientCursorParams makeParams(OperationContext* opCtx) { + return {makeFakePlanExecutor(opCtx), kTestNss, {}, false, BSONObj()}; + } + + ClientCursorPin makeCursor(OperationContext* opCtx) { + return _cursorManager.registerCursor(opCtx, makeParams(opCtx)); + } + ClockSourceMock* useClock() { return _clock; } @@ -111,6 +130,16 @@ private: CursorManager _cursorManager{kTestNss}; }; +class CursorManagerTestCustomOpCtx : public CursorManagerTest { + void setUp() override { + _queryServiceContext->getClient()->resetOperationContext(); + } + + void tearDown() override { + _queryServiceContext->getClient()->setOperationContext(_opCtx.get()); + } +}; + TEST_F(CursorManagerTest, GlobalCursorManagerShouldReportOwnershipOfCursorsItCreated) { for (int i = 0; i < 1000; i++) { auto cursorPin = CursorManager::getGlobalCursorManager()->registerCursor( @@ -397,5 +426,161 @@ TEST_F(CursorManagerTest, CursorShouldNotTimeOutUntilIdleForLongEnoughAfterBeing ASSERT_EQ(1UL, cursorManager->timeoutCursors(_opCtx.get(), clock->now())); ASSERT_EQ(0UL, cursorManager->numCursors()); } + +/** + * Test that cursors inherit the logical session id from their operation context + */ +TEST_F(CursorManagerTestCustomOpCtx, LogicalSessionIdOnOperationCtxTest) { + // Cursors created on an op ctx without a session id have no session id. + { + auto opCtx = _queryServiceContext->makeOperationContext(); + auto pinned = makeCursor(opCtx.get()); + + ASSERT_EQUALS(pinned.getCursor()->getSessionId(), boost::none); + } + + // Cursors created on an op ctx with a session id have a session id. + { + auto lsid = LogicalSessionId::gen(); + auto opCtx2 = _queryServiceContext->makeOperationContext(lsid); + auto pinned2 = makeCursor(opCtx2.get()); + + ASSERT_EQUALS(pinned2.getCursor()->getSessionId(), lsid); + } +} + +/** + * Test that a manager whose cursors do not have sessions does not return them. + */ +TEST_F(CursorManagerTestCustomOpCtx, CursorsWithoutSessions) { + // Add a cursor with no session to the cursor manager. + auto opCtx = _queryServiceContext->makeOperationContext(); + auto pinned = makeCursor(opCtx.get()); + ASSERT_EQUALS(pinned.getCursor()->getSessionId(), boost::none); + + // Retrieve all sessions active in manager - set should be empty. + LogicalSessionIdSet lsids; + useCursorManager()->appendActiveSessions(&lsids); + ASSERT(lsids.empty()); +} + +/** + * Test a manager that has one cursor running inside of a session. + */ +TEST_F(CursorManagerTestCustomOpCtx, OneCursorWithASession) { + // Add a cursor with a session to the cursor manager. + auto lsid = LogicalSessionId::gen(); + auto opCtx = _queryServiceContext->makeOperationContext(lsid); + auto pinned = makeCursor(opCtx.get()); + + // Retrieve all sessions active in manager - set should contain just lsid. + LogicalSessionIdSet lsids; + useCursorManager()->appendActiveSessions(&lsids); + ASSERT_EQ(lsids.size(), size_t(1)); + ASSERT(lsids.find(lsid) != lsids.end()); + + // Retrieve all cursors for this lsid - should be just ours. + auto cursors = useCursorManager()->getCursorsForSession(lsid); + ASSERT_EQ(cursors.size(), size_t(1)); + auto cursorId = pinned.getCursor()->cursorid(); + ASSERT(cursors.find(cursorId) != cursors.end()); + + // Remove the cursor from the manager. + pinned.release(); + ASSERT_OK(useCursorManager()->eraseCursor(opCtx.get(), cursorId, false)); + + // There should be no more cursor entries by session id. + LogicalSessionIdSet sessions; + useCursorManager()->appendActiveSessions(&sessions); + ASSERT(sessions.empty()); + ASSERT(useCursorManager()->getCursorsForSession(lsid).empty()); +} + +/** + * Test a manager with multiple cursors running inside of the same session. + */ +TEST_F(CursorManagerTestCustomOpCtx, MultipleCursorsWithSameSession) { + // Add two cursors on the same session to the cursor manager. + auto lsid = LogicalSessionId::gen(); + auto opCtx = _queryServiceContext->makeOperationContext(lsid); + auto pinned = makeCursor(opCtx.get()); + auto pinned2 = makeCursor(opCtx.get()); + + auto cursorId1 = pinned.getCursor()->cursorid(); + auto cursorId2 = pinned2.getCursor()->cursorid(); + + // Retrieve all sessions - set should contain just lsid. + stdx::unordered_set<LogicalSessionId, LogicalSessionId::Hash> lsids; + useCursorManager()->appendActiveSessions(&lsids); + ASSERT_EQ(lsids.size(), size_t(1)); + ASSERT(lsids.find(lsid) != lsids.end()); + + // Retrieve all cursors for session - should be both cursors. + auto cursors = useCursorManager()->getCursorsForSession(lsid); + ASSERT_EQ(cursors.size(), size_t(2)); + ASSERT(cursors.find(cursorId1) != cursors.end()); + ASSERT(cursors.find(cursorId2) != cursors.end()); + + // Remove one cursor from the manager. + pinned.release(); + ASSERT_OK(useCursorManager()->eraseCursor(opCtx.get(), cursorId1, false)); + + // Should still be able to retrieve the session. + lsids.clear(); + useCursorManager()->appendActiveSessions(&lsids); + ASSERT_EQ(lsids.size(), size_t(1)); + ASSERT(lsids.find(lsid) != lsids.end()); + + // Should still be able to retrieve remaining cursor by session. + cursors = useCursorManager()->getCursorsForSession(lsid); + ASSERT_EQ(cursors.size(), size_t(1)); + ASSERT(cursors.find(cursorId2) != cursors.end()); +} + +/** + * Test a manager with multiple cursors running inside of different sessions. + */ +TEST_F(CursorManagerTestCustomOpCtx, MultipleCursorsMultipleSessions) { + auto lsid1 = LogicalSessionId::gen(); + auto lsid2 = LogicalSessionId::gen(); + + CursorId cursor1; + CursorId cursor2; + + // Cursor with session 1. + { + auto opCtx1 = _queryServiceContext->makeOperationContext(lsid1); + cursor1 = makeCursor(opCtx1.get()).getCursor()->cursorid(); + } + + // Cursor with session 2. + { + auto opCtx2 = _queryServiceContext->makeOperationContext(lsid2); + cursor2 = makeCursor(opCtx2.get()).getCursor()->cursorid(); + } + + // Cursor with no session. + { + auto opCtx3 = _queryServiceContext->makeOperationContext(); + makeCursor(opCtx3.get()).getCursor(); + } + + // Retrieve all sessions - should be both lsids. + LogicalSessionIdSet lsids; + useCursorManager()->appendActiveSessions(&lsids); + ASSERT_EQ(lsids.size(), size_t(2)); + ASSERT(lsids.find(lsid1) != lsids.end()); + ASSERT(lsids.find(lsid2) != lsids.end()); + + // Retrieve cursors for each session - should be just one. + auto cursors1 = useCursorManager()->getCursorsForSession(lsid1); + ASSERT_EQ(cursors1.size(), size_t(1)); + ASSERT(cursors1.find(cursor1) != cursors1.end()); + + auto cursors2 = useCursorManager()->getCursorsForSession(lsid2); + ASSERT_EQ(cursors2.size(), size_t(1)); + ASSERT(cursors2.find(cursor2) != cursors2.end()); +} + } // namespace } // namespace mongo |