mongo/jstests/sharding/migration_fails_with_spurio...

778 lines
33 KiB
JavaScript

/**
* Tests that chunk migration fails when spurious or orphan documents exist in
* the target range on the recipient shard. This prevents mixing legitimate
* documents (incoming via migration) with invalid ones (incorrectly present
* due to historical reasons like direct connections or range deleter bugs).
*/
import {ShardingTest} from "jstests/libs/shardingtest.js";
import {getTimeseriesCollForDDLOps} from "jstests/core/timeseries/libs/viewless_timeseries_util.js";
import {getRawOperationSpec, getTimeseriesCollForRawOps} from "jstests/libs/raw_operation_utils.js";
const st = new ShardingTest({shards: 2, mongos: 1});
st.stopBalancer();
const mongos = st.s0;
const admin = mongos.getDB("admin");
const config = mongos.getDB("config");
const testDB = mongos.getDB("test");
// Enable sharding on the test database.
assert.commandWorked(admin.runCommand({enableSharding: "test", primaryShard: st.shard0.shardName}));
{
// ========================================================================
// Test 1: Simple shard key scenario.
// ========================================================================
const coll = testDB.getCollection("migration_spurious");
// Shard the collection with _id as the shard key.
assert.commandWorked(admin.runCommand({shardCollection: coll.getFullName(), key: {_id: 1}}));
// Split the collection into two chunks: {_id: MinKey} -> {_id: 50} and {_id: 50} -> {_id: MaxKey}.
assert.commandWorked(admin.runCommand({split: coll.getFullName(), middle: {_id: 50}}));
// Initially, both chunks are on shard0. Move the second [50, MaxKey) chunk to shard1.
assert.commandWorked(
admin.runCommand({
moveChunk: coll.getFullName(),
find: {_id: 60},
to: st.shard1.shardName,
_waitForDelete: true,
}),
);
// Verify initial chunk distribution.
let chunks = config.chunks.find({uuid: coll.getUUID()}).toArray();
assert.eq(2, chunks.length, "Expected 2 chunks");
// Find chunks and their shards.
let chunk1 = chunks.find((c) => c.min._id === MinKey);
let chunk2 = chunks.find((c) => c.min._id === 50);
assert(chunk1, "Could not find first chunk");
assert(chunk2, "Could not find second chunk");
jsTest.log.info("Chunk 1 (_id: MinKey -> 50) is on shard: " + chunk1.shard);
jsTest.log.info("Chunk 2 (_id: 50 -> MaxKey) is on shard: " + chunk2.shard);
assert(chunk1.shard != chunk2.shard, "Chunks should be in two different chunks");
// Insert some legitimate data
assert.commandWorked(coll.insert({_id: 10, data: "legitimate_data_chunk1"}));
assert.commandWorked(coll.insert({_id: 60, data: "legitimate_data_chunk2"}));
// Now create a spurious document scenario:
// Insert a document directly on shard1 that should belong to chunk1 (owned by shard0).
// This simulates historical data corruption or direct connection inserts.
jsTest.log.info("Creating spurious document by inserting directly on shard1 a document that should be on shard0.");
// Connect directly to shard1 and insert a document in the range [MinKey, 50) which belongs to shard0.
const shard1Direct = st.shard1.getDB("test");
const shard1Coll = shard1Direct.getCollection("migration_spurious");
// Insert spurious document with _id: 25 (should be in range [MinKey, 50) owned by shard0).
assert.commandWorked(shard1Coll.insert({_id: 25, data: "spurious_document"}));
// Verify the spurious document exists on shard1.
assert.eq(1, shard1Coll.find({_id: 25}).count(), "Spurious document should exist on shard1");
// Verify it doesn't exist on shard0 (where it should be).
const shard0Direct = st.shard0.getDB("test");
const shard0Coll = shard0Direct.getCollection("migration_spurious");
assert.eq(0, shard0Coll.find({_id: 25}).count(), "Spurious document should not exist on shard0");
// Now try to move chunk1 from shard0 to shard1, expecting failure.
jsTest.log.info("Attempting to move chunk1 to shard1 - this should fail due to spurious document detection...");
let moveChunkResult = admin.runCommand({
moveChunk: coll.getFullName(),
find: {_id: 10}, // This finds chunk1 [MinKey, 50).
to: st.shard1.shardName,
_waitForDelete: true,
});
jsTest.log.info("Move chunk result: " + tojson(moveChunkResult));
chunks = config.chunks.find({uuid: coll.getUUID()}).toArray();
// The migration should fail due to the spurious document check.
assert.commandFailed(moveChunkResult, "Migration should fail due to spurious documents");
// Check that the error message indicates the spurious document issue.
let errorMsg = moveChunkResult.errmsg || moveChunkResult.reason || "";
assert(
errorMsg.includes("found existing document") ||
errorMsg.includes("spurious") ||
errorMsg.includes("already exists in range"),
"Error message should indicate spurious document issue. Got: " + errorMsg,
);
// Verify chunk ownership hasn't changed - chunk1 should still be on shard0.
chunks = config.chunks.find({uuid: coll.getUUID()}).toArray();
chunk1 = chunks.find((c) => c.min._id === MinKey);
assert.eq(st.shard0.shardName, chunk1.shard, "Chunk1 should still be on shard0 after failed migration");
// Clean up the spurious document and verify migration works.
jsTest.log.info("Cleaning up spurious document and retrying migration...");
assert.commandWorked(shard1Coll.remove({_id: 25}));
assert.eq(0, shard1Coll.find({_id: 25}).count(), "Spurious document should be removed");
// Now the migration should succeed.
moveChunkResult = admin.runCommand({
moveChunk: coll.getFullName(),
find: {_id: 10},
to: st.shard1.shardName,
_waitForDelete: true,
});
assert.commandWorked(moveChunkResult, "Migration should succeed after removing spurious document");
// Verify chunk ownership changed.
chunks = config.chunks.find({uuid: coll.getUUID()}).toArray();
chunk1 = chunks.find((c) => c.min._id === MinKey);
assert.eq(st.shard1.shardName, chunk1.shard, "Chunk1 should now be on shard1 after successful migration");
jsTest.log.info("Test completed successfully!");
}
{
// ========================================================================
// Test 2: Compound shard key scenario.
// ========================================================================
jsTest.log.info("=== Starting compound shard key test ===");
const compoundColl = testDB.getCollection("compound_spurious");
// Shard the collection with a compound shard key.
assert.commandWorked(admin.runCommand({shardCollection: compoundColl.getFullName(), key: {category: 1, item: 1}}));
// Split the collection at {category: "electronics", item: MinKey}.
assert.commandWorked(
admin.runCommand({
split: compoundColl.getFullName(),
middle: {category: "electronics", item: MinKey},
}),
);
// Initially, both chunks are on shard0. Move the second chunk to shard1.
assert.commandWorked(
admin.runCommand({
moveChunk: compoundColl.getFullName(),
find: {category: "electronics", item: "laptop"},
to: st.shard1.shardName,
_waitForDelete: true,
}),
);
// Verify initial chunk distribution for compound key.
let compoundChunks = config.chunks.find({uuid: compoundColl.getUUID()}).toArray();
assert.eq(2, compoundChunks.length, "Expected 2 chunks for compound key collection");
let compoundChunk1 = compoundChunks.find((c) => c.min.category === MinKey);
let compoundChunk2 = compoundChunks.find((c) => c.min.category === "electronics");
assert(compoundChunk1, "Could not find first compound chunk");
assert(compoundChunk2, "Could not find second compound chunk");
jsTest.log.info("Compound Chunk 1 (category: MinKey -> electronics) is on shard: " + compoundChunk1.shard);
jsTest.log.info("Compound Chunk 2 (category: electronics -> MaxKey) is on shard: " + compoundChunk2.shard);
// Insert some legitimate data through mongos.
assert.commandWorked(compoundColl.insert({category: "books", item: "novel", data: "legitimate_data"}));
assert.commandWorked(compoundColl.insert({category: "electronics", item: "phone", data: "legitimate_data"}));
// Create spurious document scenario with compound shard key:
// Insert a document directly on shard1 that should belong to the first chunk (owned by shard0).
jsTest.log.info(
"Creating spurious document by inserting directly on shard1 a document that should be on shard0...",
);
const shard1CompoundColl = st.shard1.getDB("test").getCollection("compound_spurious");
// Insert spurious document with {category: "books", item: "textbook"}
// (should be in range [MinKey, "electronics") owned by shard0).
assert.commandWorked(shard1CompoundColl.insert({category: "books", item: "textbook", data: "spurious_document"}));
// Verify the spurious document exists on shard1.
assert.eq(
1,
shard1CompoundColl.find({category: "books", item: "textbook"}).count(),
"Spurious compound document should exist on shard1",
);
// Verify it doesn't exist on shard0 (where it should be).
const shard0CompoundColl = st.shard0.getDB("test").getCollection("compound_spurious");
assert.eq(
0,
shard0CompoundColl.find({category: "books", item: "textbook"}).count(),
"Spurious compound document should not exist on shard0",
);
// Now try to move the first chunk from shard0 to shard1.
// This should fail because shard1 already has a spurious document in that range.
jsTest.log.info(
"Attempting to move compound chunk to shard1 - this should fail due to spurious document detection...",
);
let compoundMoveResult = admin.runCommand({
moveChunk: compoundColl.getFullName(),
find: {category: "books", item: "novel"}, // This finds the first chunk
to: st.shard1.shardName,
_waitForDelete: true,
});
jsTest.log.info("Compound move chunk result: " + tojson(compoundMoveResult));
// The migration should fail due to the spurious document check.
assert.commandFailed(compoundMoveResult, "Compound migration should fail due to spurious documents");
// Check that the error message indicates the spurious document issue and shows compound shard key properly.
let compoundErrorMsg = compoundMoveResult.errmsg || compoundMoveResult.reason || "";
assert(
compoundErrorMsg.includes("found existing document") ||
compoundErrorMsg.includes("spurious") ||
compoundErrorMsg.includes("already exists in range"),
"Compound error message should indicate spurious document issue. Got: " + compoundErrorMsg,
);
// The error should contain the compound shard key in a readable format.
assert(
compoundErrorMsg.includes("books") && compoundErrorMsg.includes("textbook"),
"Error message should contain the compound shard key values. Got: " + compoundErrorMsg,
);
// Verify chunk ownership hasn't changed - compound chunk1 should still be on shard0.
compoundChunks = config.chunks.find({uuid: compoundColl.getUUID()}).toArray();
compoundChunk1 = compoundChunks.find((c) => c.min.category === MinKey);
assert.eq(
st.shard0.shardName,
compoundChunk1.shard,
"Compound chunk1 should still be on shard0 after failed migration",
);
// Clean up the spurious document and verify migration works.
jsTest.log.info("Cleaning up spurious compound document and retrying migration...");
assert.commandWorked(shard1CompoundColl.remove({category: "books", item: "textbook"}));
assert.eq(
0,
shard1CompoundColl.find({category: "books", item: "textbook"}).count(),
"Spurious compound document should be removed",
);
// Now the migration should succeed.
compoundMoveResult = admin.runCommand({
moveChunk: compoundColl.getFullName(),
find: {category: "books", item: "novel"},
to: st.shard1.shardName,
_waitForDelete: true,
});
assert.commandWorked(compoundMoveResult, "Compound migration should succeed after removing spurious document");
// Verify chunk ownership changed.
compoundChunks = config.chunks.find({uuid: compoundColl.getUUID()}).toArray();
compoundChunk1 = compoundChunks.find((c) => c.min.category === MinKey);
assert.eq(
st.shard1.shardName,
compoundChunk1.shard,
"Compound chunk1 should now be on shard1 after successful migration",
);
jsTest.log.info("Compound shard key test completed successfully!");
}
{
// ========================================================================
// Test 3: Time series collection scenario.
// ========================================================================
jsTest.log.info("=== Starting time series collection test ===");
const tsColl = testDB.getCollection("timeseries_spurious");
assert(tsColl.drop());
// Create and Shard the time series collection on {sensor_id: 1, timestamp: 1}.
assert.commandWorked(
admin.runCommand({
shardCollection: tsColl.getFullName(),
timeseries: {timeField: "timestamp", metaField: "sensor_id", granularity: "hours"},
key: {sensor_id: 1, timestamp: 1},
}),
);
let tsCollDDLOps = getTimeseriesCollForDDLOps(testDB, tsColl);
let tsCollDDLOpsFullName = tsCollDDLOps.getFullName();
// Verify the collection is now sharded
let shardedCollections = config.collections.find({_id: tsCollDDLOpsFullName}).toArray();
assert.eq(1, shardedCollections.length, "Time series collection should be sharded");
jsTest.log.info("Time series collection successfully sharded with key: " + tojson(shardedCollections[0].key));
// Split the collection at {sensor_id: "sensor_100", timestamp: MinKey}.
assert.commandWorked(
admin.runCommand({
split: tsCollDDLOpsFullName,
middle: {meta: "sensor_100", "control.min.timestamp": MinKey},
}),
);
// Move the second chunk to shard1.
assert.commandWorked(
admin.runCommand({
moveChunk: tsCollDDLOpsFullName,
find: {meta: "sensor_200", "control.min.timestamp": new Date()},
to: st.shard1.shardName,
_waitForDelete: true,
}),
);
// Verify initial chunk distribution for time series collection.
// Insert some legitimate time series data through mongos.
const baseTime = new Date();
assert.commandWorked(
tsColl.insert({
sensor_id: "sensor_050",
timestamp: new Date(baseTime.getTime() + 1000),
temperature: 23.5,
data: "legitimate_ts_data",
}),
);
assert.commandWorked(
tsColl.insert({
sensor_id: "sensor_150",
timestamp: new Date(baseTime.getTime() + 2000),
temperature: 25.1,
data: "legitimate_ts_data",
}),
);
// Create spurious document scenario with time series data:
// Insert a document directly on shard1 that should belong to the first chunk (owned by shard0).
jsTest.log.info("Creating spurious time series document by inserting directly on shard1 " + tsCollDDLOps.getName());
const shard1DB = st.shard1.getDB("test");
const shard1TsColl = shard1DB.getCollection(tsColl.getName());
// Create a proper bucket document that would map to sensor_025 (which should be in first chunk)
const spuriousTimestamp = new Date(baseTime.getTime() + 3000);
assert.commandWorked(
getTimeseriesCollForRawOps(shard1DB, shard1TsColl).insert(
{
_id: ObjectId(),
meta: "sensor_025", // This corresponds to sensor_id metaField
control: {
version: 1,
min: {
timestamp: spuriousTimestamp,
temperature: 20.0,
},
max: {
timestamp: spuriousTimestamp,
temperature: 20.0,
},
closed: false,
},
data: {
timestamp: {"0": spuriousTimestamp},
temperature: {"0": 20.0},
data: {"0": "spurious_ts_document"},
},
},
getRawOperationSpec(shard1DB),
),
);
// Verify the spurious document exists on shard1.
assert.eq(
1,
getTimeseriesCollForRawOps(shard1DB, shard1TsColl).find({meta: "sensor_025"}).rawData().count(),
"Spurious time series document should exist on shard1",
);
// Now try to move the first chunk from shard0 to shard1
jsTest.log.info(
"Attempting to move time series chunk to shard1 - this should fail due to spurious document detection...",
);
let tsMoveResult = admin.runCommand({
moveChunk: tsCollDDLOpsFullName,
find: {meta: "sensor_050", "control.min.timestamp": new Date()}, // This finds the first chunk
to: st.shard1.shardName,
_waitForDelete: true,
});
jsTest.log.info("Time series move chunk result: " + tojson(tsMoveResult));
// The migration should fail due to the spurious document check.
assert.commandFailed(tsMoveResult, "Time series migration should fail due to spurious documents");
// Check that the error message indicates the spurious document issue.
let tsErrorMsg = tsMoveResult.errmsg || tsMoveResult.reason || "";
assert(
tsErrorMsg.includes("found existing document") ||
tsErrorMsg.includes("spurious") ||
tsErrorMsg.includes("already exists in range"),
"Time series error message should indicate spurious document issue. Got: " + tsErrorMsg,
);
// Clean up the spurious document and verify migration works.
jsTest.log.info("Cleaning up spurious time series document and retrying migration...");
assert.commandWorked(
getTimeseriesCollForRawOps(shard1DB, shard1TsColl).remove({meta: "sensor_025"}, getRawOperationSpec(shard1DB)),
);
assert.eq(
0,
getTimeseriesCollForRawOps(shard1DB, shard1TsColl).find({meta: "sensor_025"}).rawData().count(),
"Spurious time series document should be removed",
);
// Now the migration should succeed.
tsMoveResult = admin.runCommand({
moveChunk: tsCollDDLOpsFullName,
find: {meta: "sensor_050", "control.min.timestamp": new Date()},
to: st.shard1.shardName,
_waitForDelete: true,
});
assert.commandWorked(tsMoveResult, "Time series migration should succeed after removing spurious document");
assert.eq(2, tsColl.find({}).count(), "check after move, should have 2 documents in total.");
jsTest.log.info("Time series collection test completed successfully!");
}
{
// ========================================================================
// Test 4: Hashed shard key scenario.
// ========================================================================
jsTest.log.info("=== Starting hashed shard key test ===");
const hashedColl = testDB.getCollection("hashed_spurious");
// Shard the collection with a hashed shard key.
assert.commandWorked(
admin.runCommand({
shardCollection: hashedColl.getFullName(),
key: {user_id: "hashed"},
}),
);
// For hashed shard keys, chunks are automatically distributed. Verify we have multiple chunks.
let hashedChunks = config.chunks.find({uuid: hashedColl.getUUID()}).toArray();
assert.gte(hashedChunks.length, 2, "Expected at least 2 chunks for hashed shard key collection");
jsTest.log.info("Found " + hashedChunks.length + " chunks for hashed shard key collection");
// Find chunks on different shards.
let shard0Chunks = hashedChunks.filter((c) => c.shard === st.shard0.shardName);
let shard1Chunks = hashedChunks.filter((c) => c.shard === st.shard1.shardName);
jsTest.log.info("Shard0 has " + shard0Chunks.length + " chunks, Shard1 has " + shard1Chunks.length + " chunks");
// Insert some legitimate data through mongos to understand the distribution.
assert.commandWorked(hashedColl.insert({user_id: "user_001", data: "legitimate_hashed_data"}));
assert.commandWorked(hashedColl.insert({user_id: "user_002", data: "legitimate_hashed_data"}));
assert.commandWorked(hashedColl.insert({user_id: "user_003", data: "legitimate_hashed_data"}));
// Find a user_id that would create a cross-shard spurious document scenario.
let spuriousShardColl, targetShard;
// Try to find a user_id that would create a spurious document scenario.
let testUserId = "spurious_user_1";
// Insert through mongos to see where it naturally goes
assert.commandWorked(hashedColl.insert({user_id: testUserId, data: "test_for_distribution"}));
let onShard0 = st.shard0.getDB("test").getCollection("hashed_spurious").find({user_id: testUserId}).count();
let onShard1 = st.shard1.getDB("test").getCollection("hashed_spurious").find({user_id: testUserId}).count();
// Clean up the test document
assert.commandWorked(hashedColl.remove({user_id: testUserId}));
if (onShard0 > 0) {
// This user_id maps to shard0, so we'll insert it directly on shard1 to create a spurious document.
spuriousShardColl = st.shard1.getDB("test").getCollection("hashed_spurious");
targetShard = st.shard0.shardName;
} else if (onShard1 > 0) {
// This user_id maps to shard1, so we'll insert it directly on shard0 to create a spurious document.
spuriousShardColl = st.shard0.getDB("test").getCollection("hashed_spurious");
targetShard = st.shard1.shardName;
}
// Create spurious document scenario with hashed shard key:
jsTest.log.info("Creating spurious hashed document by inserting " + testUserId + " directly on wrong shard...");
// Insert spurious document directly on the wrong shard.
assert.commandWorked(
spuriousShardColl.insert({
user_id: testUserId,
data: "spurious_hashed_document",
}),
);
// Verify the spurious document exists on the wrong shard.
assert.eq(
1,
spuriousShardColl.find({user_id: testUserId}).count(),
"Spurious hashed document should exist on wrong shard",
);
// Find a chunk we can move that would conflict with our spurious document.
let targetChunk = null;
for (let chunk of hashedChunks) {
if (chunk.shard === targetShard) {
targetChunk = chunk;
break;
}
}
assert(targetChunk, "Should have found a chunk to move for hashed shard key test");
// Try to move a chunk that would conflict with our spurious document.
jsTest.log.info("Attempting to move hashed chunk - this should fail due to spurious document detection...");
let hashedMoveResult = admin.runCommand({
moveChunk: hashedColl.getFullName(),
bounds: [targetChunk.min, targetChunk.max],
to: spuriousShardColl.getName().includes("rs0") ? st.shard1.shardName : st.shard0.shardName,
_waitForDelete: true,
});
jsTest.log.info("Hashed move chunk result: " + tojson(hashedMoveResult));
// The migration should fail due to the spurious document check.
assert.commandFailed(hashedMoveResult, "Hashed migration should fail due to spurious documents");
// Check that the error message indicates the spurious document issue.
let hashedErrorMsg = hashedMoveResult.errmsg || hashedMoveResult.reason || "";
assert(
hashedErrorMsg.includes("found existing document") ||
hashedErrorMsg.includes("spurious") ||
hashedErrorMsg.includes("already exists in range"),
"Hashed error message should indicate spurious document issue. Got: " + hashedErrorMsg,
);
// Clean up the spurious document and verify migration works.
jsTest.log.info("Cleaning up spurious hashed document and retrying migration...");
assert.commandWorked(spuriousShardColl.remove({user_id: testUserId}));
assert.eq(0, spuriousShardColl.find({user_id: testUserId}).count(), "Spurious hashed document should be removed");
// Now the migration should succeed.
hashedMoveResult = admin.runCommand({
moveChunk: hashedColl.getFullName(),
bounds: [targetChunk.min, targetChunk.max],
to: spuriousShardColl.getName().includes("rs0") ? st.shard1.shardName : st.shard0.shardName,
_waitForDelete: true,
});
assert.commandWorked(hashedMoveResult, "Hashed migration should succeed after removing spurious document");
jsTest.log.info("Hashed shard key test completed successfully!");
}
{
// ========================================================================
// Test 5: Compound hashed shard key scenario.
// ========================================================================
jsTest.log.info("=== Starting compound hashed shard key test ===");
const compoundHashedColl = testDB.getCollection("compound_hashed_spurious");
// Shard the collection with a compound shard key that includes a hashed field.
assert.commandWorked(
admin.runCommand({
shardCollection: compoundHashedColl.getFullName(),
key: {region: 1, userId: "hashed"},
}),
);
// For compound hashed shard keys, MongoDB doesn't automatically create multiple chunks.
// We need to manually split to create multiple chunks for testing.
assert.commandWorked(
admin.runCommand({
split: compoundHashedColl.getFullName(),
middle: {region: "us-west", userId: MinKey},
}),
);
// Move one chunk to shard1 to ensure chunks are on different shards.
assert.commandWorked(
admin.runCommand({
moveChunk: compoundHashedColl.getFullName(),
bounds: [
{region: "us-west", userId: MinKey},
{region: MaxKey, userId: MaxKey},
],
to: st.shard1.shardName,
_waitForDelete: true,
}),
);
// Verify we now have multiple chunks.
let compoundHashedChunks = config.chunks.find({uuid: compoundHashedColl.getUUID()}).toArray();
assert.eq(compoundHashedChunks.length, 2, "Expected 2 chunks for compound hashed shard key collection");
jsTest.log.info("Found " + compoundHashedChunks.length + " chunks for compound hashed shard key collection");
const shard0Direct = st.shard0.getDB("test");
const shard0Coll = shard0Direct.getCollection("compound_hashed_spurious");
const shard1Direct = st.shard1.getDB("test");
const shard1Coll = shard1Direct.getCollection("compound_hashed_spurious");
// Find chunks on different shards.
let shard0CompoundHashedChunks = compoundHashedChunks.filter((c) => c.shard === st.shard0.shardName);
let shard1CompoundHashedChunks = compoundHashedChunks.filter((c) => c.shard === st.shard1.shardName);
jsTest.log.info(
"Shard0 has " +
shard0CompoundHashedChunks.length +
" chunks, Shard1 has " +
shard1CompoundHashedChunks.length +
" chunks",
);
// Insert some legitimate data through mongos to understand the distribution.
assert.commandWorked(
compoundHashedColl.insert({region: "us-east", userId: "user_001", data: "legitimate_compound_hashed_data"}),
);
assert.commandWorked(
compoundHashedColl.insert({region: "us-west", userId: "user_002", data: "legitimate_compound_hashed_data"}),
);
assert.commandWorked(
compoundHashedColl.insert({region: "eu-central", userId: "user_003", data: "legitimate_compound_hashed_data"}),
);
assert.eq(2, shard0Coll.find().count());
assert.eq(1, shard1Coll.find().count());
let targetCompoundHashedShard = st.shard1.shardName; // The chunk we'll migrate is owned by shard1
// Try to find a region/userId that would create a spurious document scenario.
// Make sure the region falls within the chunk we'll migrate (us-west to MaxKey)
let testRegion = "us-west-1"; // This ensures it's > "us-west" and < MaxKey
let testCompoundHashedUserId = "spurious_compound_user_1";
// Insert through mongos to make sure it naturally goes to shard1
assert.commandWorked(
compoundHashedColl.insert({
region: testRegion,
userId: testCompoundHashedUserId,
data: "test_for_distribution",
}),
);
assert.eq(2, shard0Coll.find().count());
assert.eq(2, shard1Coll.find().count());
assert.eq(0, shard0Coll.find({region: testRegion, userId: testCompoundHashedUserId}).count());
assert.eq(1, shard1Coll.find({region: testRegion, userId: testCompoundHashedUserId}).count());
// Delete the test document
assert.commandWorked(compoundHashedColl.remove({region: testRegion, userId: testCompoundHashedUserId}));
jsTest.log.info(
"Test logic: spurious document region='" +
testRegion +
"' should belong to chunk on " +
targetCompoundHashedShard +
", but we'll insert it on shard0",
);
// Create spurious document scenario with compound hashed shard key:
jsTest.log.info(
"Creating spurious compound hashed document by inserting {region: " +
testRegion +
", userId: " +
testCompoundHashedUserId +
"} directly on wrong shard...",
);
// Insert spurious document directly on the wrong shard.
assert.commandWorked(
shard0Coll.insert({
region: testRegion,
userId: testCompoundHashedUserId,
data: "spurious_compound_hashed_document",
}),
);
// Verify the spurious document exists on the wrong shard: shard0.
assert.eq(
1,
shard0Coll.find({region: testRegion, userId: testCompoundHashedUserId}).count(),
"Spurious compound hashed document should exist on wrong shard",
);
// Find a chunk we can move that would conflict with our spurious document.
let targetCompoundHashedChunk = null;
for (let chunk of shard1CompoundHashedChunks) {
assert.eq(chunk.shard, targetCompoundHashedShard);
targetCompoundHashedChunk = chunk;
break;
}
assert(targetCompoundHashedChunk, "Should have found a chunk to move for compound hashed shard key test");
// Try to move a chunk that would conflict with our spurious document.
jsTest.log.info(
"Attempting to move compound hashed chunk - this should fail due to spurious document detection...",
);
jsTest.log.info(
"Moving chunk with bounds: " +
tojson(targetCompoundHashedChunk.min) +
" to " +
tojson(targetCompoundHashedChunk.max),
);
jsTest.log.info("Spurious document has: {region: " + testRegion + ", userId: " + testCompoundHashedUserId + "}");
// Move the chunk FROM shard1 (where it currently lives) TO shard0 (where spurious doc is)
let compoundHashedMoveResult = admin.runCommand({
moveChunk: compoundHashedColl.getFullName(),
bounds: [targetCompoundHashedChunk.min, targetCompoundHashedChunk.max],
to: st.shard0.shardName, // Moving to shard0 where the spurious document exists
_waitForDelete: true,
});
jsTest.log.info("Compound hashed move chunk result: " + tojson(compoundHashedMoveResult));
// The migration should fail due to the spurious document check.
assert.commandFailed(compoundHashedMoveResult, "Compound hashed migration should fail due to spurious documents");
// Check that the error message indicates the spurious document issue.
let compoundHashedErrorMsg = compoundHashedMoveResult.errmsg || compoundHashedMoveResult.reason || "";
assert(
compoundHashedErrorMsg.includes("found existing document") ||
compoundHashedErrorMsg.includes("spurious") ||
compoundHashedErrorMsg.includes("already exists in range"),
"Compound hashed error message should indicate spurious document issue. Got: " + compoundHashedErrorMsg,
);
// Clean up the spurious document and verify migration works.
jsTest.log.info("Cleaning up spurious compound hashed document and retrying migration...");
assert.commandWorked(shard0Coll.remove({region: testRegion, userId: testCompoundHashedUserId}));
assert.eq(
0,
shard0Coll.find({region: testRegion, userId: testCompoundHashedUserId}).count(),
"Spurious compound hashed document should be removed",
);
// Now the migration should succeed.
compoundHashedMoveResult = admin.runCommand({
moveChunk: compoundHashedColl.getFullName(),
bounds: [targetCompoundHashedChunk.min, targetCompoundHashedChunk.max],
to: st.shard0.shardName, // Same target as before, but now spurious doc is removed
_waitForDelete: true,
});
assert.commandWorked(
compoundHashedMoveResult,
"Compound hashed migration should succeed after removing spurious document",
);
jsTest.log.info("Compound hashed shard key test completed successfully!");
}
st.stop();