Skip to content

Commit e635cc1

Browse files
xdgacmorrow
authored andcommitted
CXX-1258 Keep cursor iterators in lockstep at end
This commit fixes a bug where iterators might not always compare equal to cursor.end() when the cursor was actually exhausted. It adds extensive tests for cursor lockstep and invariants. It also substantially revises cursor/iterator documentation for exhaustion, equality comparison, and tailable cursor behavior. (cherry picked from commit 667a833)
1 parent 18f9e17 commit e635cc1

File tree

6 files changed

+249
-34
lines changed

6 files changed

+249
-34
lines changed

examples/mongocxx/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ set(MONGOCXX_EXAMPLES
3535
query.cpp
3636
query_projection.cpp
3737
remove.cpp
38+
tailable_cursor.cpp
3839
update.cpp
3940
view_or_value_variant.cpp
4041
)

examples/mongocxx/tailable_cursor.cpp

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// Copyright 2017 MongoDB Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#include <chrono>
16+
#include <iostream>
17+
#include <thread>
18+
19+
#include <bsoncxx/builder/basic/document.hpp>
20+
#include <bsoncxx/json.hpp>
21+
22+
#include <mongocxx/client.hpp>
23+
#include <mongocxx/collection.hpp>
24+
#include <mongocxx/database.hpp>
25+
#include <mongocxx/instance.hpp>
26+
#include <mongocxx/options/create_collection.hpp>
27+
#include <mongocxx/options/find.hpp>
28+
#include <mongocxx/uri.hpp>
29+
30+
using bsoncxx::builder::basic::document;
31+
using bsoncxx::builder::basic::kvp;
32+
33+
//
34+
// Document number counter for sample inserted documents. This just
35+
// makes the tailed document more obviously in sequence.
36+
//
37+
38+
static std::int32_t counter = 0;
39+
40+
//
41+
// Drop and recreate capped collection. Inserts a doc as tailing an empty
42+
// collection returns a closed cursor.
43+
//
44+
// TODO CDRIVER-2093: After CDRIVER-2093 is fixed, we can provide better
45+
// diagnostics as to whether the cursor is alive or closed and won't need
46+
// to prime the collection with a document.
47+
//
48+
void init_capped_collection(mongocxx::client* conn, std::string name) {
49+
auto db = (*conn)["test"];
50+
auto coll = db[name];
51+
52+
coll.drop();
53+
auto create_opts = mongocxx::options::create_collection{}.capped(true).size(1024 * 1024);
54+
db.create_collection(name, create_opts);
55+
56+
document builder{};
57+
builder.append(kvp("n", counter++));
58+
db[name].insert_one(builder.extract());
59+
}
60+
61+
//
62+
// Insert 5 documents so there are more documents to tail.
63+
//
64+
void insert_docs(mongocxx::collection* coll) {
65+
std::cout << "Inserting batch... " << std::endl;
66+
for (int j = 0; j < 5; j++) {
67+
document builder{};
68+
builder.append(kvp("n", counter++));
69+
coll->insert_one(builder.extract());
70+
}
71+
}
72+
73+
int main(int, char**) {
74+
// The mongocxx::instance constructor and destructor initialize and shut down the driver,
75+
// respectively. Therefore, a mongocxx::instance must be created before using the driver and
76+
// must remain alive for as long as the driver is in use.
77+
mongocxx::instance inst{};
78+
mongocxx::client conn{mongocxx::uri{}};
79+
std::string name{"capped_coll"};
80+
81+
// Create the capped collection.
82+
init_capped_collection(&conn, name);
83+
84+
// Construct a tailable cursor.
85+
auto coll = conn["test"][name];
86+
mongocxx::options::find opts{};
87+
opts.cursor_type(mongocxx::cursor::type::k_tailable);
88+
auto cursor = coll.find({}, opts);
89+
90+
// Loop "forever", or in this case, until we find >= 25 documents.
91+
std::cout << "Tailing the collection..." << std::endl;
92+
int docs_found = 0;
93+
for (;;) {
94+
// Loop over the cursor until no more documents are available.
95+
// On the next iteration of the outer loop, if more documents
96+
// are available, the tailable cursor will return them.
97+
for (auto&& doc : cursor) {
98+
std::cout << bsoncxx::to_json(doc) << std::endl;
99+
docs_found++;
100+
}
101+
102+
if (docs_found >= 25) {
103+
break;
104+
}
105+
106+
// No documents are available, so sleep a bit before trying again.
107+
std::this_thread::sleep_for(std::chrono::milliseconds(100));
108+
109+
// For the sake of this example, add more documents before the next loop.
110+
insert_docs(&coll);
111+
}
112+
113+
return EXIT_SUCCESS;
114+
}

src/mongocxx/cursor.cpp

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ cursor::iterator& cursor::iterator::operator++() {
5757
throw_exception<query_exception>(error);
5858
} else {
5959
_cursor->_impl->mark_nothing_left();
60-
_cursor = nullptr; // Set iterator equal to end().
6160
}
6261
return *this;
6362
}
@@ -82,6 +81,14 @@ cursor::iterator::iterator(cursor* cursor) : _cursor(cursor) {
8281
operator++();
8382
}
8483

84+
//
85+
// An iterator is exhausted if it is the end-iterator (_cursor == nullptr)
86+
// or if the underlying _cursor is marked exhausted.
87+
//
88+
bool cursor::iterator::is_exhausted() const {
89+
return !_cursor || _cursor->_impl->is_exhausted();
90+
}
91+
8592
const bsoncxx::document::view& cursor::iterator::operator*() const {
8693
return _cursor->_impl->doc;
8794
}
@@ -90,8 +97,13 @@ const bsoncxx::document::view* cursor::iterator::operator->() const {
9097
return &_cursor->_impl->doc;
9198
}
9299

100+
//
101+
// Iterators are equal if they point to the same underlying _cursor or if they
102+
// both are "at the end". We check for exhaustion first because the most
103+
// common check is `iter != cursor.end()`.
104+
//
93105
bool operator==(const cursor::iterator& lhs, const cursor::iterator& rhs) {
94-
return lhs._cursor == rhs._cursor;
106+
return ((rhs.is_exhausted() && lhs.is_exhausted()) || (lhs._cursor == rhs._cursor));
95107
}
96108

97109
bool operator!=(const cursor::iterator& lhs, const cursor::iterator& rhs) {

src/mongocxx/cursor.hpp

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -60,17 +60,19 @@ class MONGOCXX_API cursor {
6060
/// returned points to the next remaining result, not the result of
6161
/// the original call to begin().
6262
///
63+
/// For a tailable cursor, when cursor.begin() == cursor.end(), no
64+
/// documents are available. Each call to cursor.begin() checks again
65+
/// for newly-available documents.
66+
///
6367
/// @return the cursor::iterator
6468
///
6569
/// @throws mongocxx::query_exception if the query failed
6670
///
6771
iterator begin();
6872

6973
///
70-
/// A cursor::iterator that points to the end of the results. In the
71-
/// case of a tailable cursor, this iterator will compare equal to an
72-
/// exhausted tailable cursor iterator, even if more results are available
73-
/// the next time the cursor is iterated.
74+
/// A cursor::iterator indicating cursor exhaustion, meaning that
75+
/// no documents are available from the cursor.
7476
///
7577
/// @return the cursor::iterator
7678
///
@@ -80,6 +82,7 @@ class MONGOCXX_API cursor {
8082
friend class collection;
8183
friend class client;
8284
friend class database;
85+
friend class cursor::iterator;
8386

8487
MONGOCXX_PRIVATE cursor(void* cursor_ptr,
8588
bsoncxx::stdx::optional<type> cursor_type = bsoncxx::stdx::nullopt);
@@ -92,10 +95,19 @@ class MONGOCXX_API cursor {
9295
/// Class representing an input iterator of documents in a MongoDB cursor
9396
/// result set.
9497
///
95-
/// All non-empty iterators derived from the same mongocxx::cursor move in
96-
/// lock-step. Dereferencing any non-empty iterator always gives the first
97-
/// remaining document in the cursor. Incrementing one iterator is equivalent
98-
/// to incrementing them all.
98+
/// All non-end iterators derived from the same mongocxx::cursor move in
99+
/// lock-step. Dereferencing any non-end() iterator always gives the first
100+
/// remaining document in the cursor. Incrementing one non-end iterator is
101+
/// equivalent to incrementing them all.
102+
///
103+
/// An iterator is 'exhausted' when no documents are available. An
104+
/// end-iterator is always exhausted. A non-end iterator is exhausted when the
105+
/// originating mongocxx::cursor has no more documents. When an iterator is
106+
/// exhausted, it must not be dereferenced or incremented.
107+
///
108+
/// For iterators of a tailable cursor, calling cursor.begin() may revive an
109+
/// exhausted iterator so that it no longer compares equal to the
110+
/// end-iterator.
99111
///
100112
class MONGOCXX_API cursor::iterator
101113
: public std::iterator<std::input_iterator_tag, bsoncxx::document::view> {
@@ -130,7 +142,8 @@ class MONGOCXX_API cursor::iterator
130142
///
131143
/// @{
132144
///
133-
/// Compare two iterators for (in)-equality
145+
/// Compare two iterators for (in)-equality. Iterators compare equal if
146+
/// they point to the same underlying cursor or if both are exhausted.
134147
///
135148
/// @relates iterator
136149
///
@@ -140,6 +153,8 @@ class MONGOCXX_API cursor::iterator
140153
/// @}
141154
///
142155

156+
MONGOCXX_PRIVATE bool is_exhausted() const;
157+
143158
MONGOCXX_PRIVATE explicit iterator(cursor* cursor);
144159

145160
// If this pointer is null, the iterator is considered "past-the-end".

src/mongocxx/private/cursor.hh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class cursor::impl {
3434
impl(mongoc_cursor_t* cursor, bsoncxx::stdx::optional<cursor::type> cursor_type)
3535
: cursor_t(cursor),
3636
status{cursor ? state::k_pending : state::k_dead},
37+
exhausted(!cursor),
3738
tailable{cursor && cursor_type && (*cursor_type == cursor::type::k_tailable ||
3839
*cursor_type == cursor::type::k_tailable_await)} {
3940
}
@@ -50,6 +51,10 @@ class cursor::impl {
5051
return status == state::k_dead;
5152
}
5253

54+
bool is_exhausted() const {
55+
return exhausted;
56+
}
57+
5358
bool is_tailable() const {
5459
return tailable;
5560
}
@@ -61,16 +66,19 @@ class cursor::impl {
6166

6267
void mark_nothing_left() {
6368
doc = bsoncxx::document::view{};
69+
exhausted = true;
6470
status = tailable ? state::k_pending : state::k_dead;
6571
}
6672

6773
void mark_started() {
6874
status = state::k_started;
75+
exhausted = false;
6976
}
7077

7178
mongoc_cursor_t* cursor_t;
7279
bsoncxx::document::view doc;
7380
state status;
81+
bool exhausted;
7482
bool tailable;
7583
};
7684

0 commit comments

Comments
 (0)