Skip to content

Commit bacefbd

Browse files
alyacbEvergreen Agent
authored andcommitted
SERVER-58654 Add $graphLookup support for sharded views
1 parent 64a119b commit bacefbd

File tree

3 files changed

+382
-2
lines changed

3 files changed

+382
-2
lines changed
Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
// Test that sharded $graphLookup can resolve sharded views correctly.
2+
// @tags: [requires_sharding, requires_fcv_51]
3+
(function() {
4+
"use strict";
5+
6+
load("jstests/aggregation/extras/utils.js"); // For assertArrayEq.
7+
load("jstests/libs/log.js"); // For findMatchingLogLines.
8+
9+
const sharded = new ShardingTest({
10+
mongos: 1,
11+
shards: [{verbose: 3}, {verbose: 3}, {verbose: 3}],
12+
config: 1,
13+
});
14+
assert(sharded.adminCommand({enableSharding: "test"}));
15+
16+
const isShardedLookupEnabled =
17+
sharded.s.adminCommand({getParameter: 1, featureFlagShardedLookup: 1})
18+
.featureFlagShardedLookup.value;
19+
if (!isShardedLookupEnabled) {
20+
sharded.stop();
21+
return;
22+
}
23+
24+
const testDBName = "test";
25+
const testDB = sharded.getDB(testDBName);
26+
sharded.ensurePrimaryShard(testDBName, sharded.shard0.shardName);
27+
28+
// Create 'docs' collection which will be backed by shard 1 from which we will run aggregations.
29+
const docs = testDB.docs;
30+
docs.drop();
31+
const docsDocs = [
32+
{_id: 1, shard_key: "shard1", name: "Carter", subject: "Astrophysics"},
33+
{_id: 2, shard_key: "shard1", name: "Jackson", subject: "Archaeology"},
34+
{_id: 3, shard_key: "shard1", name: "Jones", subject: "Archaeology"},
35+
{_id: 4, shard_key: "shard1", name: "Mann", subject: "Theoretical Physics"},
36+
{_id: 5, shard_key: "shard1", name: "Mann", subject: "Xenobiology"},
37+
];
38+
assert.commandWorked(docs.createIndex({shard_key: 1}));
39+
assert.commandWorked(docs.insertMany(docsDocs));
40+
assert(sharded.s.adminCommand({shardCollection: docs.getFullName(), key: {shard_key: 1}}));
41+
42+
// Create first 'subjects' collection which will be backed by shard 2.
43+
const subjects = testDB.subjects;
44+
subjects.drop();
45+
const subjectsDocs = [
46+
{_id: 1, shard_key: "shard2", name: "Science"},
47+
{_id: 2, shard_key: "shard2", name: "Biology", parent: "Science"},
48+
{_id: 3, shard_key: "shard2", name: "Physics", parent: "Science"},
49+
{_id: 4, shard_key: "shard2", name: "Anthropology", parent: "Humanities"},
50+
{_id: 5, shard_key: "shard2", name: "Astrophysics", parent: "Physics"},
51+
{_id: 6, shard_key: "shard2", name: "Archaeology", parent: "Anthropology"},
52+
{_id: 7, shard_key: "shard2", name: "Theoretical Physics", parent: "Physics"},
53+
{_id: 8, shard_key: "shard2", name: "Xenobiology", parent: "Biology"},
54+
{_id: 9, shard_key: "shard2", name: "Humanities"},
55+
];
56+
assert.commandWorked(subjects.createIndex({shard_key: 1}));
57+
assert.commandWorked(subjects.insertMany(subjectsDocs));
58+
assert(sharded.s.adminCommand({shardCollection: subjects.getFullName(), key: {shard_key: 1}}));
59+
60+
let testCount = 0;
61+
function getMatchingLogsForTestRun(logs, fields) {
62+
let foundTest = false;
63+
64+
// Filter out any logs that happened before the current aggregation.
65+
function getLogsForTestRun(log) {
66+
if (foundTest) {
67+
return true;
68+
}
69+
const m = findMatchingLogLine([log], {comment: "test " + testCount});
70+
if (m !== null) {
71+
foundTest = true;
72+
}
73+
return foundTest;
74+
}
75+
76+
// Pick only those remaining logs which match the input 'fields'.
77+
return [...findMatchingLogLines(logs.filter(getLogsForTestRun), fields)];
78+
}
79+
80+
function getShardedViewExceptions(shard) {
81+
const shardLog = assert.commandWorked(sharded[shard].adminCommand({getLog: "global"})).log;
82+
return ["test.docs", "test.subjects"].map(ns => {
83+
return {ns: ns, count: [...getMatchingLogsForTestRun(shardLog, {id: 5865400, ns})].length};
84+
});
85+
}
86+
87+
function testGraphLookupView({collection, pipeline, expectedResults, expectedExceptions}) {
88+
assertArrayEq({
89+
actual: collection.aggregate(pipeline, {comment: "test " + testCount}).toArray(),
90+
expected: expectedResults
91+
});
92+
if (expectedExceptions) {
93+
// Count how many CommandOnShardedViewNotSupported exceptions we get and verify that they
94+
// match the number we were expecting.
95+
const exceptionCounts = getShardedViewExceptions(expectedExceptions.shard);
96+
for (const actualEx of exceptionCounts) {
97+
const ns = actualEx.ns;
98+
const actualCount = actualEx.count;
99+
const expectedCount = expectedExceptions[ns];
100+
assert(actualCount == expectedCount,
101+
"expected: " + expectedCount + " exceptions for ns " + ns + ", actually got " +
102+
actualCount + " exceptions.");
103+
}
104+
}
105+
testCount++;
106+
}
107+
108+
function checkView(viewName, expected) {
109+
assertArrayEq({actual: testDB[viewName].find({}).toArray(), expected});
110+
}
111+
112+
function moveChunksByShardKey(collection, shard) {
113+
assert.commandWorked(testDB.adminCommand({
114+
moveChunk: collection.getFullName(),
115+
find: {shard_key: shard},
116+
to: sharded[shard].shardName,
117+
_waitForDelete: true
118+
}));
119+
}
120+
121+
// In order to trigger CommandOnShardedViewNotSupportedOnMongod exceptions where a shard cannot
122+
// resolve a view definition, ensure that:
123+
// - 'docs' is backed by shard 1.
124+
// - 'subjects' is backed by shard 2.
125+
moveChunksByShardKey(docs, "shard1");
126+
moveChunksByShardKey(subjects, "shard2");
127+
128+
// Create a view with an empty pipeline on 'subjects'.
129+
assert.commandWorked(testDB.createView("emptyViewOnSubjects", subjects.getName(), []));
130+
checkView("emptyViewOnSubjects", subjectsDocs);
131+
132+
// Test a $graphLookup that triggers a CommandOnShardedViewNotSupportedOnMongod exception for a view
133+
// with an empty pipeline.
134+
testGraphLookupView({
135+
collection: docs,
136+
pipeline: [
137+
{$graphLookup: {
138+
from: "emptyViewOnSubjects",
139+
startWith: "$subject",
140+
connectFromField: "parent",
141+
connectToField: "name",
142+
as: "subjects",
143+
}},
144+
{$project: {
145+
name: 1,
146+
subjects: "$subjects.name"
147+
}}
148+
],
149+
expectedResults: [
150+
{_id: 1, name: "Carter", subjects: ["Astrophysics", "Physics", "Science"]},
151+
{_id: 2, name: "Jackson", subjects: ["Anthropology", "Archaeology", "Humanities"]},
152+
{_id: 3, name: "Jones", subjects: ["Anthropology", "Archaeology", "Humanities"]},
153+
{_id: 4, name: "Mann", subjects: ["Physics", "Science", "Theoretical Physics"]},
154+
{_id: 5, name: "Mann", subjects: ["Biology", "Science", "Xenobiology"]},
155+
],
156+
// Expect only one exception when trying to resolve the view 'emptyViewOnSubjects'.
157+
expectedExceptions: {"shard": "shard1", "test.docs": 0, "test.subjects": 1},
158+
});
159+
160+
// Test a $graphLookup with a restrictSearchWithMatch that triggers a
161+
// CommandOnShardedViewNotSupportedOnMongod exception for a view with an empty pipeline.
162+
testGraphLookupView({
163+
collection: docs,
164+
pipeline: [
165+
{$graphLookup: {
166+
from: "emptyViewOnSubjects",
167+
startWith: "$subject",
168+
connectFromField: "parent",
169+
connectToField: "name",
170+
as: "subjects",
171+
restrictSearchWithMatch: { "name" : {$nin: ["Anthropology", "Archaeology", "Humanities"]} }
172+
}},
173+
{$project: {
174+
name: 1,
175+
science: {$gt: [{$size: "$subjects"}, 0]}
176+
}}
177+
],
178+
expectedResults: [
179+
{_id: 1, name: "Carter", science: true},
180+
{_id: 2, name: "Jackson", science: false},
181+
{_id: 3, name: "Jones", science: false},
182+
{_id: 4, name: "Mann", science: true},
183+
{_id: 5, name: "Mann", science: true},
184+
],
185+
// Expect only one exception when trying to resolve the view 'emptyViewOnSubjects'.
186+
expectedExceptions: {"shard": "shard1", "test.docs": 0, "test.subjects": 1},
187+
});
188+
189+
// Create a view with an empty pipeline on the existing empty view on 'subjects'.
190+
assert.commandWorked(
191+
testDB.createView("emptyViewOnViewOnSubjects", testDB.emptyViewOnSubjects.getName(), []));
192+
checkView("emptyViewOnViewOnSubjects", subjectsDocs);
193+
194+
// Test a $graphLookup that triggers a CommandOnShardedViewNotSupportedOnMongod exception for a view
195+
// on another view.
196+
testGraphLookupView({
197+
collection: docs,
198+
pipeline: [
199+
{$graphLookup: {
200+
from: "emptyViewOnViewOnSubjects",
201+
startWith: "$subject",
202+
connectFromField: "parent",
203+
connectToField: "name",
204+
as: "subjects",
205+
}},
206+
{$project: {
207+
name: 1,
208+
subjects: "$subjects.name"
209+
}}
210+
],
211+
expectedResults: [
212+
{_id: 1, name: "Carter", subjects: ["Astrophysics", "Physics", "Science"]},
213+
{_id: 2, name: "Jackson", subjects: ["Anthropology", "Archaeology", "Humanities"]},
214+
{_id: 3, name: "Jones", subjects: ["Anthropology", "Archaeology", "Humanities"]},
215+
{_id: 4, name: "Mann", subjects: ["Physics", "Science", "Theoretical Physics"]},
216+
{_id: 5, name: "Mann", subjects: ["Biology", "Science", "Xenobiology"]},
217+
],
218+
// Expect only one exception when trying to resolve the view 'emptyViewOnSubjects'.
219+
expectedExceptions: {"shard": "shard1", "test.docs": 0, "test.subjects": 1},
220+
});
221+
222+
// Create a view with a pipeline on 'docs' that runs another $graphLookup.
223+
assert.commandWorked(testDB.createView("physicists", docs.getName(), [
224+
{$graphLookup: {
225+
from: "emptyViewOnSubjects",
226+
startWith: "$subject",
227+
connectFromField: "parent",
228+
connectToField: "name",
229+
as: "subjects",
230+
}},
231+
{$match: {
232+
"subjects.name": "Physics"
233+
}},
234+
{$project: {
235+
name: 1,
236+
specialty: "$subject",
237+
subjects: "$subjects.name"
238+
}}
239+
]));
240+
checkView("physicists", [
241+
{
242+
_id: 1,
243+
name: "Carter",
244+
specialty: "Astrophysics",
245+
subjects: ["Astrophysics", "Physics", "Science"]
246+
},
247+
{
248+
_id: 4,
249+
name: "Mann",
250+
specialty: "Theoretical Physics",
251+
subjects: ["Physics", "Science", "Theoretical Physics"]
252+
},
253+
]);
254+
255+
// Test a $graphLookup that triggers a CommandOnShardedViewNotSupportedOnMongod exception for a view
256+
// with a pipeline that contains a $graphLookup.
257+
testGraphLookupView({
258+
collection: subjects,
259+
pipeline: [
260+
{$graphLookup: {
261+
from: "physicists",
262+
startWith: "$name",
263+
connectFromField: "subjects",
264+
connectToField: "specialty",
265+
as: "practitioner",
266+
}},
267+
{$unwind: "$practitioner"},
268+
],
269+
expectedResults: [
270+
{_id: 5, shard_key: "shard2", name: "Astrophysics", parent: "Physics",
271+
practitioner: {_id: 1, name: "Carter", specialty: "Astrophysics",
272+
subjects: ["Astrophysics", "Physics", "Science"]}},
273+
{_id: 7, shard_key: "shard2", name: "Theoretical Physics", parent: "Physics",
274+
practitioner: {_id: 4, name: "Mann", specialty: "Theoretical Physics",
275+
subjects: ["Physics", "Science", "Theoretical Physics"]}},
276+
],
277+
// Expect one exception when trying to resolve the view 'physicists' on collection 'docs' and
278+
// another four on 'subjects' when trying to resolve 'emptyViewOnSubjects'.
279+
expectedExceptions: {"shard": "shard2", "test.docs": 1, "test.subjects": 4},
280+
});
281+
282+
// Create a view with a pipeline on 'physicists' to test resolution of a view on another view.
283+
assert.commandWorked(testDB.createView("physicist", testDB.physicists.getName(), [
284+
{$match: {"specialty": "Astrophysics"}},
285+
]));
286+
287+
// Test a $graphLookup that triggers a CommandOnShardedViewNotSupportedOnMongod exception for a view
288+
// on another view.
289+
testGraphLookupView({
290+
collection: subjects,
291+
pipeline: [
292+
{$graphLookup: {
293+
from: "physicist",
294+
startWith: "$name",
295+
connectFromField: "subjects",
296+
connectToField: "specialty",
297+
as: "practitioner",
298+
}},
299+
{$unwind: "$practitioner"},
300+
],
301+
expectedResults: [
302+
{_id: 5, shard_key: "shard2", name: "Astrophysics", parent: "Physics",
303+
practitioner: {_id: 1, name: "Carter", specialty: "Astrophysics",
304+
subjects: ["Astrophysics", "Physics", "Science"]}},
305+
],
306+
// Expect one exception when trying to resolve the view 'physicists' on collection 'docs' and
307+
// one on 'subjects' when trying to resolve 'emptyViewOnSubjects'.
308+
expectedExceptions: {"shard": "shard2", "test.docs": 1, "test.subjects": 1},
309+
});
310+
311+
// Test a $graphLookup with restrictSearchWithMatch that triggers a
312+
// CommandOnShardedViewNotSupportedOnMongod exception for a view with a pipeline that contains a
313+
// $graphLookup.
314+
testGraphLookupView({
315+
collection: subjects,
316+
pipeline: [
317+
{$graphLookup: {
318+
from: "physicists",
319+
startWith: "$name",
320+
connectFromField: "subjects",
321+
connectToField: "specialty",
322+
as: "practitioner",
323+
restrictSearchWithMatch: { name: "Mann" }
324+
}},
325+
{$unwind: "$practitioner"},
326+
],
327+
expectedResults: [
328+
{_id: 7, shard_key: "shard2", name: "Theoretical Physics", parent: "Physics",
329+
practitioner: {_id: 4, name: "Mann", specialty: "Theoretical Physics",
330+
subjects: ["Physics", "Science", "Theoretical Physics"]}},
331+
],
332+
// Expect one exception when trying to resolve the view 'physicists' on collection 'docs' and
333+
// another two on 'subjects' when trying to resolve 'emptyViewOnSubjects'.
334+
expectedExceptions: {"shard": "shard2", "test.docs": 1, "test.subjects": 2},
335+
});
336+
337+
sharded.stop();
338+
}());

0 commit comments

Comments
 (0)