311 lines
13 KiB
JavaScript
311 lines
13 KiB
JavaScript
// Tests mongos behavior when reading against views in a transaction.
|
|
//
|
|
// @tags: [
|
|
// requires_find_command,
|
|
// requires_sharding,
|
|
// uses_multi_shard_transaction,
|
|
// uses_transactions,
|
|
// ]
|
|
(function() {
|
|
"use strict";
|
|
|
|
load("jstests/aggregation/extras/utils.js"); // For arrayEq.
|
|
load("jstests/sharding/libs/sharded_transactions_helpers.js");
|
|
|
|
const shardedDbName = "shardedDB";
|
|
const shardedCollName = "sharded";
|
|
const shardedViewName = "sharded_view";
|
|
|
|
const unshardedDbName = "unshardedDB";
|
|
const unshardedCollName = "unsharded";
|
|
const unshardedViewName = "unsharded_view";
|
|
|
|
const viewOnShardedViewName = "sharded_view_view";
|
|
|
|
function setUpUnshardedCollectionAndView(st, session, primaryShard) {
|
|
assert.writeOK(st.s.getDB(unshardedDbName)[unshardedCollName].insert(
|
|
{_id: 1, x: "unsharded"}, {writeConcern: {w: "majority"}}));
|
|
st.ensurePrimaryShard(unshardedDbName, primaryShard);
|
|
|
|
const unshardedView = session.getDatabase(unshardedDbName)[unshardedViewName];
|
|
assert.commandWorked(unshardedView.runCommand(
|
|
"create", {viewOn: unshardedCollName, pipeline: [], writeConcern: {w: "majority"}}));
|
|
|
|
return unshardedView;
|
|
}
|
|
|
|
function setUpShardedCollectionAndView(st, session, primaryShard) {
|
|
const ns = shardedDbName + "." + shardedCollName;
|
|
|
|
assert.writeOK(st.s.getDB(shardedDbName)[shardedCollName].insert(
|
|
{_id: -1}, {writeConcern: {w: "majority"}}));
|
|
assert.writeOK(st.s.getDB(shardedDbName)[shardedCollName].insert(
|
|
{_id: 1}, {writeConcern: {w: "majority"}}));
|
|
assert.commandWorked(st.s.adminCommand({enableSharding: shardedDbName}));
|
|
st.ensurePrimaryShard(shardedDbName, primaryShard);
|
|
|
|
assert.commandWorked(st.s.adminCommand({shardCollection: ns, key: {_id: 1}}));
|
|
assert.commandWorked(st.s.adminCommand({split: ns, middle: {_id: 0}}));
|
|
assert.commandWorked(
|
|
st.s.adminCommand({moveChunk: ns, find: {_id: 1}, to: st.shard1.shardName}));
|
|
|
|
const shardedView = session.getDatabase(shardedDbName)[shardedViewName];
|
|
assert.commandWorked(shardedView.runCommand(
|
|
"create", {viewOn: shardedCollName, pipeline: [], writeConcern: {w: "majority"}}));
|
|
|
|
flushRoutersAndRefreshShardMetadata(st, {ns, dbNames: [shardedDbName, unshardedDbName]});
|
|
|
|
return shardedView;
|
|
}
|
|
|
|
const st = new ShardingTest({shards: 2, mongos: 1, config: 1});
|
|
const session = st.s.startSession();
|
|
|
|
// Set up an unsharded collection on shard0.
|
|
const unshardedView = setUpUnshardedCollectionAndView(st, session, st.shard0.shardName);
|
|
|
|
// Set up a sharded collection with one chunk on each shard in a database with shard0 as its
|
|
// primary shard.
|
|
const shardedView = setUpShardedCollectionAndView(st, session, st.shard0.shardName);
|
|
|
|
// Set up a view on the sharded view, in the same database.
|
|
const viewOnShardedView = session.getDatabase(shardedDbName)[viewOnShardedViewName];
|
|
assert.commandWorked(viewOnShardedView.runCommand(
|
|
"create", {viewOn: shardedViewName, pipeline: [], writeConcern: {w: "majority"}}));
|
|
|
|
//
|
|
// The first statement a participant shard receives reading from a view should succeed.
|
|
//
|
|
|
|
function readFromViewOnFirstParticipantStatement(session, view, viewFunc, numDocsExpected) {
|
|
session.startTransaction();
|
|
assert.eq(viewFunc(view), numDocsExpected);
|
|
session.commitTransaction();
|
|
}
|
|
|
|
// Unsharded view.
|
|
readFromViewOnFirstParticipantStatement(session, unshardedView, (view) => {
|
|
return view.aggregate({$match: {}}).itcount();
|
|
}, 1);
|
|
readFromViewOnFirstParticipantStatement(session, unshardedView, (view) => {
|
|
return view.distinct("_id").length;
|
|
}, 1);
|
|
readFromViewOnFirstParticipantStatement(session, unshardedView, (view) => {
|
|
return view.find().itcount();
|
|
}, 1);
|
|
|
|
// Sharded view.
|
|
readFromViewOnFirstParticipantStatement(session, shardedView, (view) => {
|
|
return view.aggregate({$match: {}}).itcount();
|
|
}, 2);
|
|
readFromViewOnFirstParticipantStatement(session, shardedView, (view) => {
|
|
return view.distinct("_id").length;
|
|
}, 2);
|
|
readFromViewOnFirstParticipantStatement(session, shardedView, (view) => {
|
|
return view.find().itcount();
|
|
}, 2);
|
|
|
|
// View on sharded view.
|
|
readFromViewOnFirstParticipantStatement(session, viewOnShardedView, (view) => {
|
|
return view.aggregate({$match: {}}).itcount();
|
|
}, 2);
|
|
readFromViewOnFirstParticipantStatement(session, viewOnShardedView, (view) => {
|
|
return view.distinct("_id").length;
|
|
}, 2);
|
|
readFromViewOnFirstParticipantStatement(session, viewOnShardedView, (view) => {
|
|
return view.find().itcount();
|
|
}, 2);
|
|
|
|
//
|
|
// A later statement a participant shard receives reading from a view should succeed.
|
|
//
|
|
|
|
function readFromViewOnLaterParticipantStatement(session, view, viewFunc, numDocsExpected) {
|
|
session.startTransaction();
|
|
assert.eq(view.aggregate({$match: {}}).itcount(), numDocsExpected);
|
|
assert.eq(viewFunc(view), numDocsExpected);
|
|
session.commitTransaction();
|
|
}
|
|
|
|
// Unsharded view.
|
|
readFromViewOnLaterParticipantStatement(session, unshardedView, (view) => {
|
|
return view.aggregate({$match: {}}).itcount();
|
|
}, 1);
|
|
readFromViewOnLaterParticipantStatement(session, unshardedView, (view) => {
|
|
return view.distinct("_id").length;
|
|
}, 1);
|
|
readFromViewOnLaterParticipantStatement(session, unshardedView, (view) => {
|
|
return view.find().itcount();
|
|
}, 1);
|
|
|
|
// Sharded view.
|
|
readFromViewOnLaterParticipantStatement(session, shardedView, (view) => {
|
|
return view.aggregate({$match: {}}).itcount();
|
|
}, 2);
|
|
readFromViewOnLaterParticipantStatement(session, shardedView, (view) => {
|
|
return view.distinct("_id").length;
|
|
}, 2);
|
|
readFromViewOnLaterParticipantStatement(session, shardedView, (view) => {
|
|
return view.find().itcount();
|
|
}, 2);
|
|
|
|
// View on sharded view.
|
|
readFromViewOnLaterParticipantStatement(session, viewOnShardedView, (view) => {
|
|
return view.aggregate({$match: {}}).itcount();
|
|
}, 2);
|
|
readFromViewOnLaterParticipantStatement(session, viewOnShardedView, (view) => {
|
|
return view.distinct("_id").length;
|
|
}, 2);
|
|
readFromViewOnLaterParticipantStatement(session, viewOnShardedView, (view) => {
|
|
return view.find().itcount();
|
|
}, 2);
|
|
|
|
//
|
|
// Transactions on shards that return a view resolution error on the first statement remain
|
|
// aborted if the shard is not targeted by the retry on the resolved namespace.
|
|
//
|
|
// This may happen when reading from a sharded view, because mongos will target the primary
|
|
// shard first to resolve the view, but the retry on the underlying sharded collection is not
|
|
// guaranteed to target the primary again.
|
|
//
|
|
|
|
// Assumes the request in viewFunc does not target the primary shard, Shard0.
|
|
function primaryShardNotReTargeted_FirstStatement(session, view, viewFunc, numDocsExpected) {
|
|
session.startTransaction();
|
|
assert.eq(viewFunc(view), numDocsExpected);
|
|
|
|
// There should not be an in-progress transaction on the primary shard.
|
|
assert.commandFailedWithCode(st.rs0.getPrimary().getDB("foo").runCommand({
|
|
find: "bar",
|
|
lsid: session.getSessionId(),
|
|
txnNumber: NumberLong(session.getTxnNumber_forTesting()),
|
|
autocommit: false,
|
|
}),
|
|
ErrorCodes.NoSuchTransaction);
|
|
|
|
assert.commandWorked(session.commitTransaction_forTesting());
|
|
|
|
// The transaction should not have been committed on the primary shard.
|
|
assert.commandFailedWithCode(st.rs0.getPrimary().getDB("foo").runCommand({
|
|
find: "bar",
|
|
lsid: session.getSessionId(),
|
|
txnNumber: NumberLong(session.getTxnNumber_forTesting()),
|
|
autocommit: false,
|
|
}),
|
|
ErrorCodes.NoSuchTransaction);
|
|
}
|
|
|
|
// This is only possible against sharded views.
|
|
primaryShardNotReTargeted_FirstStatement(session, shardedView, (view) => {
|
|
return view.aggregate({$match: {_id: 1}}).itcount();
|
|
}, 1);
|
|
primaryShardNotReTargeted_FirstStatement(session, shardedView, (view) => {
|
|
return view.distinct("_id", {_id: {$gte: 1}}).length;
|
|
}, 1);
|
|
primaryShardNotReTargeted_FirstStatement(session, shardedView, (view) => {
|
|
return view.find({_id: 1}).itcount();
|
|
}, 1);
|
|
|
|
// View on sharded view.
|
|
primaryShardNotReTargeted_FirstStatement(session, viewOnShardedView, (view) => {
|
|
return view.aggregate({$match: {_id: 1}}).itcount();
|
|
}, 1);
|
|
primaryShardNotReTargeted_FirstStatement(session, viewOnShardedView, (view) => {
|
|
return view.distinct("_id", {_id: {$gte: 1}}).length;
|
|
}, 1);
|
|
primaryShardNotReTargeted_FirstStatement(session, viewOnShardedView, (view) => {
|
|
return view.find({_id: 1}).itcount();
|
|
}, 1);
|
|
|
|
//
|
|
// Shards do not abort on a view resolution error if they have already completed a statement for
|
|
// a transaction.
|
|
//
|
|
|
|
// Assumes the primary shard for view is Shard0.
|
|
function primaryShardNotReTargeted_LaterStatement(session, view, viewFunc, numDocsExpected) {
|
|
session.startTransaction();
|
|
// Complete a statement on the primary shard for the view.
|
|
assert.eq(view.aggregate({$match: {_id: -1}}).itcount(), 1);
|
|
// Targets the primary first, but the resolved retry only targets Shard1.
|
|
assert.eq(viewFunc(view), numDocsExpected);
|
|
session.commitTransaction();
|
|
}
|
|
|
|
// This is only possible against sharded views.
|
|
primaryShardNotReTargeted_LaterStatement(session, shardedView, (view) => {
|
|
return view.aggregate({$match: {_id: 1}}).itcount();
|
|
}, 1);
|
|
primaryShardNotReTargeted_LaterStatement(session, shardedView, (view) => {
|
|
return view.distinct("_id", {_id: {$gte: 1}}).length;
|
|
}, 1);
|
|
primaryShardNotReTargeted_LaterStatement(session, shardedView, (view) => {
|
|
return view.find({_id: 1}).itcount();
|
|
}, 1);
|
|
|
|
// View on sharded view.
|
|
primaryShardNotReTargeted_LaterStatement(session, viewOnShardedView, (view) => {
|
|
return view.aggregate({$match: {_id: 1}}).itcount();
|
|
}, 1);
|
|
primaryShardNotReTargeted_LaterStatement(session, viewOnShardedView, (view) => {
|
|
return view.distinct("_id", {_id: {$gte: 1}}).length;
|
|
}, 1);
|
|
primaryShardNotReTargeted_LaterStatement(session, viewOnShardedView, (view) => {
|
|
return view.find({_id: 1}).itcount();
|
|
}, 1);
|
|
|
|
//
|
|
// Reading from a view using $lookup and $graphLookup should succeed.
|
|
//
|
|
|
|
function assertAggResultEqInTransaction(coll, pipeline, expected) {
|
|
session.startTransaction();
|
|
const resArray = coll.aggregate(pipeline).toArray();
|
|
assert(arrayEq(resArray, expected), tojson({got: resArray, expected: expected}));
|
|
session.commitTransaction();
|
|
}
|
|
|
|
// Set up an unsharded collection to use for $lookup. We cannot lookup into sharded collections.
|
|
// TODO SERVER-29159: Add testing for lookup into sharded collections in a transaction once that
|
|
// is supported.
|
|
const lookupDbName = "dbForLookup";
|
|
const lookupCollName = "collForLookup";
|
|
assert.writeOK(
|
|
st.s.getDB(lookupDbName)[lookupCollName].insert({_id: 1}, {writeConcern: {w: "majority"}}));
|
|
const lookupColl = session.getDatabase(unshardedDbName)[unshardedCollName];
|
|
|
|
// Lookup the document in the unsharded collection with _id: 1 through the unsharded view.
|
|
assertAggResultEqInTransaction(
|
|
lookupColl,
|
|
[
|
|
{$match: {_id: 1}},
|
|
{
|
|
$lookup:
|
|
{from: unshardedViewName, localField: "_id", foreignField: "_id", as: "matched"}
|
|
},
|
|
{$unwind: "$matched"},
|
|
{$project: {_id: 1, matchedX: "$matched.x"}}
|
|
],
|
|
[{_id: 1, matchedX: "unsharded"}]);
|
|
|
|
// Find the same document through the view using $graphLookup.
|
|
assertAggResultEqInTransaction(lookupColl,
|
|
[
|
|
{$match: {_id: 1}},
|
|
{
|
|
$graphLookup: {
|
|
from: unshardedViewName,
|
|
startWith: "$_id",
|
|
connectFromField: "_id",
|
|
connectToField: "_id",
|
|
as: "matched"
|
|
}
|
|
},
|
|
{$unwind: "$matched"},
|
|
{$project: {_id: 1, matchedX: "$matched.x"}}
|
|
],
|
|
[{_id: 1, matchedX: "unsharded"}]);
|
|
|
|
st.stop();
|
|
})();
|