summaryrefslogtreecommitdiff
path: root/jstests/noPassthrough/merge_causes_infinite_loop.js
blob: 8b96d433d3bc6d2374855ae459ecfcf2e181fea8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/**
 * Test that exposes the Halloween problem.
 *
 * The Halloween problem describes the potential for a document to be visited more than once
 * following an update operation that changes its physical location.
 */
(function() {
"use strict";

const conn = MongoRunner.runMongod();
const db = conn.getDB("merge_causes_infinite_loop");
const coll = db.getCollection("merge_causes_infinite_loop");
const out = db.getCollection("merge_causes_infinite_loop_out");
coll.drop();
out.drop();

const nDocs = 50;
// We seed the documents with large values for a. This enables the pipeline that exposes the
// halloween problem to overflow and fail more quickly.
const largeNum = 1000 * 1000 * 1000;

// We set internalQueryExecYieldPeriodMS to 1 ms to have query execution yield as often as
// possible.
assert.commandWorked(db.adminCommand({setParameter: 1, internalQueryExecYieldPeriodMS: 1}));

// Insert documents into both collections. We populate the output collection to verify that
// updates behave as expected when the source collection isn't the same as the target collection.
//
// Note that the largeArray field is included to force documents to be written to disk and not
// simply be updated in the cache. This is crucial to exposing the halloween problem as the
// physical location of each document needs to change for each document to be visited and updated
// multiple times.
function insertDocuments(collObject) {
    const bulk = collObject.initializeUnorderedBulkOp();
    for (let i = 0; i < nDocs; i++) {
        bulk.insert({_id: i, a: i * largeNum, largeArray: (new Array(1024 * 1024).join("a"))});
    }
    assert.commandWorked(bulk.execute());
}

insertDocuments(coll);
insertDocuments(out);

// Build an index over a, the field to be updated, so that updates will push modified documents
// forward in the index when outputting to the collection being aggregated.
assert.commandWorked(coll.createIndex({a: 1}));
assert.commandWorked(out.createIndex({a: 1}));

// Returns a pipeline which outputs to the specified collection.
function pipeline(outColl) {
    return [
        {$match: {a: {$gt: 0}}},
        {$merge: {into: outColl, whenMatched: [{$addFields: {a: {$multiply: ["$a", 2]}}}]}}
    ];
}

const differentCollPipeline = pipeline(out.getName());
const sameCollPipeline = pipeline(coll.getName());

// Targeting a collection that is not the collection being agggregated over will result in each
// document's value of 'a' being updated exactly once.
assert.commandWorked(
    db.runCommand({aggregate: coll.getName(), pipeline: differentCollPipeline, cursor: {}}));

// Filter out 'largeArray' as we are only interested in verifying the value of "a" in each
// document.
const result = out.find({}, {largeArray: 0}).toArray();

for (const doc of result) {
    assert(doc.hasOwnProperty("a"), doc);
    const expectedVal = doc["_id"] * 2 * largeNum;
    assert.eq(doc["a"], expectedVal, doc);
}

// Because this pipeline writes to the collection being aggregated, it will cause documents to be
// updated and pushed forward indefinitely. This will cause the computed values to eventually
// overflow.
assert.commandFailedWithCode(
    db.runCommand({aggregate: coll.getName(), pipeline: sameCollPipeline, cursor: {}}), 31109);
MongoRunner.stopMongod(conn);
}());