Skip to content

Commit 30db519

Browse files
committed
CDRIVER-3969 error when creating uncompleted cursor with no server ID (#1321)
* fix type of BCON_INT32 value * require serverID for open cursor with `mongoc_cursor_new_from_command_reply_with_opts` * document expectations of server ID option * assert `found` after `mongoc_cursor_error` If an error occurs, `found` is false. Assert on the error first to print the error. * remove unnecessary nested `if`
1 parent e55ac6b commit 30db519

File tree

4 files changed

+207
-2
lines changed

4 files changed

+207
-2
lines changed

src/libmongoc/doc/mongoc_cursor_new_from_command_reply.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ The server replies:
5555
5656
``mongoc_cursor_new_from_command_reply`` is a low-level function that initializes a :symbol:`mongoc_cursor_t` from such a reply. Additional options such as "tailable" or "awaitData" can be included in the reply.
5757

58-
When synthesizing a completed cursor response that has no more batches (i.e. with cursor id 0), set ``server_id`` to 0 as well.
58+
When synthesizing a completed cursor response that has no more batches (i.e. with cursor id 0), ``server_id`` may be 0. If the cursor response is not completed (i.e. with non-zero cursor id), pass the ``server_id`` of the server used to create the cursor.
5959

6060
Use this function only for building a language driver that wraps the C Driver. When writing applications in C, higher-level functions such as :symbol:`mongoc_collection_aggregate` are more appropriate, and ensure compatibility with a range of MongoDB versions.
6161

src/libmongoc/doc/mongoc_cursor_new_from_command_reply_with_opts.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ The server replies:
5555
5656
``mongoc_cursor_new_from_command_reply_with_opts`` is a low-level function that initializes a :symbol:`mongoc_cursor_t` from such a reply.
5757

58+
When synthesizing a completed cursor response that has no more batches (i.e. with cursor id 0), ``serverId`` may be 0. If the cursor response is not completed (i.e. with non-zero cursor id), pass the ``serverId`` of the server used to create the cursor.
59+
5860
Use this function only for building a language driver that wraps the C Driver. When writing applications in C, higher-level functions such as :symbol:`mongoc_collection_aggregate` are more appropriate, and ensure compatibility with a range of MongoDB versions.
5961

6062
Returns

src/libmongoc/src/mongoc/mongoc-cursor-cmd.c

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,5 +216,26 @@ _mongoc_cursor_cmd_new_from_reply (mongoc_client_t *client,
216216
MONGOC_ERROR_CURSOR_INVALID_CURSOR,
217217
"Couldn't parse cursor document");
218218
}
219+
220+
if (0 != cursor->cursor_id && 0 == cursor->server_id) {
221+
// A non-zero cursor_id means the cursor is still open on the server.
222+
// Expect the "serverId" option to have been passed. The "serverId" option
223+
// identifies the server with the cursor.
224+
// The server with the cursor is required to send a "getMore" or
225+
// "killCursors" command.
226+
bson_set_error (
227+
&cursor->error,
228+
MONGOC_ERROR_CURSOR,
229+
MONGOC_ERROR_CURSOR_INVALID_CURSOR,
230+
"Expected `serverId` option to identify server with open cursor "
231+
"(cursor ID is %" PRId64 "). "
232+
"Consider using `mongoc_client_select_server` and using the "
233+
"resulting server ID to create the cursor.",
234+
cursor->cursor_id);
235+
// Reset cursor_id to 0 to avoid an assertion error in
236+
// `mongoc_cursor_destroy` when attempting to send "killCursors".
237+
cursor->cursor_id = 0;
238+
}
239+
219240
return cursor;
220241
}

src/libmongoc/tests/test-mongoc-cursor.c

Lines changed: 183 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2519,7 +2519,7 @@ test_cursor_batchsize_override_range_warning (void)
25192519
{
25202520
mongoc_client_t *client;
25212521
mongoc_collection_t *coll;
2522-
bson_t *findopts = BCON_NEW ("batchSize", BCON_INT32 (1.0));
2522+
bson_t *findopts = BCON_NEW ("batchSize", BCON_INT32 (1));
25232523

25242524
client = test_framework_new_default_client ();
25252525
coll = mongoc_client_get_collection (client, "db", "coll");
@@ -2547,6 +2547,186 @@ test_cursor_batchsize_override_range_warning (void)
25472547
mongoc_client_destroy (client);
25482548
}
25492549

2550+
// Test using an open cursor created by
2551+
// `mongoc_cursor_new_from_command_reply_with_opts`.
2552+
// This is a regression test for CDRIVER-3969.
2553+
static void
2554+
test_open_cursor_from_reply (void)
2555+
{
2556+
mongoc_client_t *client;
2557+
mongoc_collection_t *coll;
2558+
bson_error_t error;
2559+
bool ok;
2560+
2561+
client = test_framework_new_default_client ();
2562+
coll = get_test_collection (client, "test_open_cursor_from_reply");
2563+
2564+
// Drop collection to remove data from prior runs.
2565+
// Ignore errors. Dropping a non-existing collection may return an "ns not
2566+
// found" error.
2567+
mongoc_collection_drop (coll, &error);
2568+
2569+
// Insert two documents.
2570+
{
2571+
ok = mongoc_collection_insert_one (coll,
2572+
tmp_bson ("{'_id': 0}"),
2573+
NULL /* opts */,
2574+
NULL /* reply */,
2575+
&error);
2576+
ASSERT_OR_PRINT (ok, error);
2577+
ok = mongoc_collection_insert_one (coll,
2578+
tmp_bson ("{'_id': 1}"),
2579+
NULL /* opts */,
2580+
NULL /* reply */,
2581+
&error);
2582+
ASSERT_OR_PRINT (ok, error);
2583+
}
2584+
2585+
// Test creating an open cursor created without a serverId. Expect error.
2586+
{
2587+
mongoc_cursor_t *cursor;
2588+
bson_t reply;
2589+
// Use a smaller batchSize than the number of documents. The smaller
2590+
// batchSize will result in the cursor being left open on the server.
2591+
bson_t *cmd = tmp_bson ("{'find': '%s', 'batchSize': 1}",
2592+
mongoc_collection_get_name (coll));
2593+
ok = mongoc_collection_command_simple (
2594+
coll, cmd, NULL /* read_prefs */, &reply, &error);
2595+
ASSERT_OR_PRINT (ok, error);
2596+
2597+
// Assert that the cursor has a non-zero cursorId. A non-zero cursorId
2598+
// means the cursor is open on the server.
2599+
{
2600+
bson_iter_t iter;
2601+
ASSERT (bson_iter_init (&iter, &reply));
2602+
ASSERT (bson_iter_find_descendant (&iter, "cursor.id", &iter));
2603+
ASSERT (BSON_ITER_HOLDS_INT64 (&iter));
2604+
ASSERT_CMPINT64 (bson_iter_int64 (&iter), >, 0);
2605+
}
2606+
2607+
// `reply` is destroyed by
2608+
// `mongoc_cursor_new_from_command_reply_with_opts`.
2609+
cursor = mongoc_cursor_new_from_command_reply_with_opts (
2610+
client, &reply, NULL /* opts */);
2611+
2612+
// Expect an error to be returned.
2613+
ASSERT (mongoc_cursor_error (cursor, &error));
2614+
ASSERT_ERROR_CONTAINS (error,
2615+
MONGOC_ERROR_CURSOR,
2616+
MONGOC_ERROR_CURSOR_INVALID_CURSOR,
2617+
"Expected `serverId` option");
2618+
2619+
mongoc_cursor_destroy (cursor);
2620+
}
2621+
2622+
// Test iterating an open cursor created with a serverId. Expect no error.
2623+
{
2624+
// Get a serverID.
2625+
uint32_t server_id;
2626+
{
2627+
mongoc_server_description_t *sd = mongoc_client_select_server (
2628+
client, true /* for_writes */, NULL /* read prefs */, &error);
2629+
ASSERT_OR_PRINT (sd, error);
2630+
server_id = mongoc_server_description_id (sd);
2631+
mongoc_server_description_destroy (sd);
2632+
}
2633+
mongoc_cursor_t *cursor;
2634+
bson_t reply;
2635+
// Use a smaller batchSize than the number of documents. The smaller
2636+
// batchSize will result in the cursor being left open on the server.
2637+
bson_t *cmd =
2638+
tmp_bson ("{'find': '%s', 'batchSize': 1, 'sort': {'_id': 1}}",
2639+
mongoc_collection_get_name (coll));
2640+
ok = mongoc_collection_command_with_opts (
2641+
coll,
2642+
cmd,
2643+
NULL /* read_prefs */,
2644+
tmp_bson ("{'serverId': %" PRIu32 "}", server_id),
2645+
&reply,
2646+
&error);
2647+
ASSERT_OR_PRINT (ok, error);
2648+
2649+
// Assert that the cursor has a non-zero cursorId. A non-zero cursorId
2650+
// means the cursor is open on the server.
2651+
{
2652+
bson_iter_t iter;
2653+
ASSERT (bson_iter_init (&iter, &reply));
2654+
ASSERT (bson_iter_find_descendant (&iter, "cursor.id", &iter));
2655+
ASSERT (BSON_ITER_HOLDS_INT64 (&iter));
2656+
ASSERT_CMPINT64 (bson_iter_int64 (&iter), >, 0);
2657+
}
2658+
2659+
// `reply` is destroyed by
2660+
// `mongoc_cursor_new_from_command_reply_with_opts`.
2661+
cursor = mongoc_cursor_new_from_command_reply_with_opts (
2662+
client, &reply, tmp_bson ("{'serverId': %" PRIu32 "}", server_id));
2663+
2664+
ASSERT_OR_PRINT (!mongoc_cursor_error (cursor, &error), error);
2665+
const bson_t *got;
2666+
bool found = mongoc_cursor_next (cursor, &got);
2667+
ASSERT_OR_PRINT (!mongoc_cursor_error (cursor, &error), error);
2668+
ASSERT (found);
2669+
ASSERT_MATCH (got, "{'_id': 0}");
2670+
found = mongoc_cursor_next (cursor, &got);
2671+
ASSERT_OR_PRINT (!mongoc_cursor_error (cursor, &error), error);
2672+
ASSERT (found);
2673+
ASSERT_MATCH (got, "{'_id': 1}");
2674+
found = mongoc_cursor_next (cursor, &got);
2675+
ASSERT_OR_PRINT (!mongoc_cursor_error (cursor, &error), error);
2676+
ASSERT (!found);
2677+
2678+
mongoc_cursor_destroy (cursor);
2679+
}
2680+
2681+
// Test destroying an open cursor created with a serverId. Expect no error.
2682+
{
2683+
// Get a serverID.
2684+
uint32_t server_id;
2685+
{
2686+
mongoc_server_description_t *sd = mongoc_client_select_server (
2687+
client, true /* for_writes */, NULL /* read prefs */, &error);
2688+
ASSERT_OR_PRINT (sd, error);
2689+
server_id = mongoc_server_description_id (sd);
2690+
mongoc_server_description_destroy (sd);
2691+
}
2692+
mongoc_cursor_t *cursor;
2693+
bson_t reply;
2694+
// Use a smaller batchSize than the number of documents. The smaller
2695+
// batchSize will result in the cursor being left open on the server.
2696+
bson_t *cmd = tmp_bson ("{'find': '%s', 'batchSize': 1}",
2697+
mongoc_collection_get_name (coll));
2698+
ok = mongoc_collection_command_with_opts (
2699+
coll,
2700+
cmd,
2701+
NULL /* read_prefs */,
2702+
tmp_bson ("{'serverId': %" PRIu32 "}", server_id),
2703+
&reply,
2704+
&error);
2705+
ASSERT_OR_PRINT (ok, error);
2706+
2707+
// Assert that the cursor has a non-zero cursorId. A non-zero cursorId
2708+
// means the cursor is open on the server.
2709+
{
2710+
bson_iter_t iter;
2711+
ASSERT (bson_iter_init (&iter, &reply));
2712+
ASSERT (bson_iter_find_descendant (&iter, "cursor.id", &iter));
2713+
ASSERT (BSON_ITER_HOLDS_INT64 (&iter));
2714+
ASSERT_CMPINT64 (bson_iter_int64 (&iter), >, 0);
2715+
}
2716+
2717+
// `reply` is destroyed by
2718+
// `mongoc_cursor_new_from_command_reply_with_opts`.
2719+
cursor = mongoc_cursor_new_from_command_reply_with_opts (
2720+
client, &reply, tmp_bson ("{'serverId': %" PRIu32 "}", server_id));
2721+
2722+
ASSERT_OR_PRINT (!mongoc_cursor_error (cursor, &error), error);
2723+
mongoc_cursor_destroy (cursor);
2724+
}
2725+
2726+
mongoc_collection_destroy (coll);
2727+
mongoc_client_destroy (client);
2728+
}
2729+
25502730
void
25512731
test_cursor_install (TestSuite *suite)
25522732
{
@@ -2647,4 +2827,6 @@ test_cursor_install (TestSuite *suite)
26472827
TestSuite_AddLive (suite,
26482828
"/Cursor/batchsize_override_range_warning",
26492829
test_cursor_batchsize_override_range_warning);
2830+
TestSuite_AddLive (
2831+
suite, "/Cursor/open_cursor_from_reply", test_open_cursor_from_reply);
26502832
}

0 commit comments

Comments
 (0)