mongo/jstests/sharding/query/let_rand.js

186 lines
5.6 KiB
JavaScript

/**
* Test that $rand in a command-level 'let' is evaluated only once.
*
* @tags: [
* # The bulkWrite command is not enabled on versions below 8.0.
* requires_fcv_80,
* ]
*/
import {FeatureFlagUtil} from "jstests/libs/feature_flag_util.js";
import {ShardingTest} from "jstests/libs/shardingtest.js";
const dbName = "test";
const st = new ShardingTest({shards: 2});
const db = st.s.getDB(dbName);
const coll = db.coll;
coll.drop();
assert.commandWorked(coll.insert([{x: -2}, {x: 2}]));
assert.commandWorked(coll.createIndex({x: 1}));
const viewName = "view";
assert.commandWorked(db.createView(viewName, coll.getName(), []));
const view = db.getCollection(viewName);
// Split at zero so each shard gets one document.
st.shardColl(coll, {x: 1}, {x: 0}, {x: 1});
// Run the command and assert that both documents have the same value of 'rand'.
function test(cmd) {
let result = assert.commandWorked(db.runCommand(cmd));
let batch = result.cursor.firstBatch;
assert.eq(batch.length, 2, result);
const rand0 = batch[0].rand;
const rand1 = batch[1].rand;
assert.eq("number", typeof rand0, result);
assert.eq("number", typeof rand1, result);
// Assert that both shards see the same pseudo-random value.
assert.eq(rand0, rand1, result);
}
test({
find: coll.getName(),
projection: {_id: 0, rand: "$$randVal"},
let: {randVal: {$rand: {}}},
});
test({
aggregate: coll.getName(),
pipeline: [{$project: {_id: 0, rand: "$$randVal"}}],
let: {randVal: {$rand: {}}},
cursor: {},
});
test({
find: view.getName(),
projection: {_id: 0, rand: "$$randVal"},
let: {randVal: {$rand: {}}},
});
test({
aggregate: view.getName(),
pipeline: [{$project: {_id: 0, rand: "$$randVal"}}],
let: {randVal: {$rand: {}}},
cursor: {},
});
// Test update command.
//
// Pick a random value and update all documents with it.
// Then we expect all documents to have the same value.
assert.commandWorked(
db.runCommand({
update: coll.getName(),
updates: [
{
q: {},
u: [{$set: {rand: "$$a"}}],
multi: true,
},
],
let: {a: {$rand: {}}},
}),
);
test({
find: coll.getName(),
projection: {rand: 1},
});
// Undo changes.
assert.commandWorked(coll.updateMany({}, {$unset: {rand: 1}}));
// Test the 'bulkWrite' command.
if (FeatureFlagUtil.isEnabled(db, "BulkWriteCommand")) {
assert.commandWorked(
db.adminCommand("bulkWrite", {
nsInfo: [{ns: coll.getFullName()}],
ops: [
{
update: 0,
filter: {},
multi: true,
updateMods: [{$set: {rand: "$$a"}}],
},
],
let: {a: {$rand: {}}},
}),
);
test({
find: coll.getName(),
projection: {rand: 1},
});
// Undo changes.
assert.commandWorked(coll.updateMany({}, {$unset: {rand: 1}}));
}
// Test the 'findAndModify' command.
//
// Randomly choose an 'x' value to update, and update it.
// If we correctly evaluate $rand once up front, then we'll always have exactly one update.
for (let trial = 0; trial < 100; ++trial) {
// Pick either {x: 2} or {x: -2} randomly, and update it.
assert.commandWorked(
db.runCommand({
findAndModify: coll.getName(),
query: {$expr: {$eq: ["$x", "$$r"]}},
update: {$inc: {hit: 1}},
let: {
r: {
$cond: {
if: {$lt: [{$rand: {}}, 0.5]},
then: -2,
else: +2,
},
},
},
}),
);
// If we incorrectly evaluated $rand independently per shard, then it's possible for each
// shard to pick the value it does not contain, resulting in no updates. If we correctly
// evaluate $rand once, we expect exactly one update.
const docs = coll.find().toArray();
assert.eq(docs.length, 2);
assert.eq(docs.filter((doc) => doc.hit).length, 1, "Expected exactly one hit: " + tojson(docs));
// Reset for next trial.
assert.commandWorked(coll.updateMany({}, {$unset: {hit: 1}}));
}
// Test the 'delete' command.
//
// Put the values 0..99 on each shard.
// Pick 0..9 randomly and deleteMany that value.
// If we incorrectly evaluate per shard, we will very likely remove a different value.
// Put the values 0..99 on each shard.
const N = 100;
assert.commandWorked(coll.deleteMany({}));
assert.commandWorked(coll.insert(Array.from({length: N}, (_, i) => ({x: -2, i}))));
assert.commandWorked(coll.insert(Array.from({length: N}, (_, i) => ({x: +2, i}))));
// Randomly choose 0..99 to delete.
// If we correctly evaluate $rand once, we'll delete the same value on each shard.
// If we incorrectly evaluate per shard, we're 99% likely to delete two different values.
assert.commandWorked(
db.runCommand({
delete: coll.getName(),
deletes: [
{
q: {$expr: {$eq: ["$i", "$$r"]}},
limit: 0, // no limit: delete all matching documents.
},
],
let: {r: {$floor: {$multiply: [N, {$rand: {}}]}}},
}),
);
const groups = coll.aggregate([{$group: {_id: "$x", nums: {$push: "$i"}}}, {$sort: {_id: 1}}]).toArray();
groups[0].nums.sort();
groups[1].nums.sort();
const originalSum = Array.sum(Array.from({length: N}, (_, i) => i));
const deletedLeft = originalSum - Array.sum(groups[0].nums);
const deletedRight = originalSum - Array.sum(groups[1].nums);
assert.eq(deletedLeft, deletedRight, "Deleted a different value on each shard");
st.stop();