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);
}());
|