데이터 입력
const DB_NAME = "prd-sre-test-jti-create-test002";
const PREFIX = "verify_";
const dbx = db.getSiblingDB(DB_NAME);
const colUsers = dbx.getCollection(PREFIX + "users");
const colProducts = dbx.getCollection(PREFIX + "products");
const colOrders = dbx.getCollection(PREFIX + "orders");
const COUNTS = { users: 2000, products: 500, orders: 7000 };
const BATCH_SIZE = 500;
function randInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }
function pick(arr) { return arr[Math.floor(Math.random() * arr.length)]; }
function randomString(len) {
const chars = "abcdefghijklmnopqrstuvwxyz";
let s = ""; for (let i = 0; i < len; i++) s += chars.charAt(Math.floor(Math.random()*chars.length));
return s;
}
function randomTags() {
const tags = ["mochi","fruit","gift","dessert","coffee","kids","premium"];
const n = randInt(1,3);
return Array.from({length:n}, () => pick(tags));
}
colUsers.createIndex({ email: 1 }, { unique: true });
colProducts.createIndex({ sku: 1 }, { unique: true });
colOrders.createIndex({ userId: 1, createdAt: -1 });
print("== inserting users ==");
let users = [];
for (let i = 0; i < COUNTS.users; i++) {
users.push({
_id: new ObjectId(),
email: `user${i}@test.com`,
name: `User ${i}`,
age: randInt(18,60),
joinedAt: new Date(Date.now() - randInt(0,1000)*86400000),
city: pick(["Seoul","Suwon","Busan","Incheon"]),
tier: pick(["basic","silver","gold","vip"])
});
if (users.length >= BATCH_SIZE) { colUsers.insertMany(users, { ordered:false }); users=[]; }
}
if (users.length) colUsers.insertMany(users, { ordered:false });
print("== inserting products ==");
let products = [];
for (let i = 0; i < COUNTS.products; i++) {
products.push({
_id: new ObjectId(),
sku: `SKU-${String(i).padStart(5,"0")}`,
name: `Product ${i} ${randomString(randInt(3,12))}`,
price: Math.round((Math.random()*50000 + 5000))/100,
tags: randomTags(),
createdAt: new Date(Date.now() - randInt(0,365)*86400000)
});
if (products.length >= BATCH_SIZE) { colProducts.insertMany(products, { ordered:false }); products=[]; }
}
if (products.length) colProducts.insertMany(products, { ordered:false });
print("== inserting orders ==");
const userIds = colUsers.find({}, { _id:1 }).toArray().map(d=>d._id);
const skus = colProducts.find({}, { sku:1 }).toArray().map(d=>d.sku);
let orders = [];
for (let i = 0; i < COUNTS.orders; i++) {
const items = Array.from({length: randInt(1,3)}).map(()=>({
sku: pick(skus),
qty: randInt(1,5),
price: Math.round((Math.random()*10000 + 500))/100
}));
orders.push({
_id: new ObjectId(),
userId: pick(userIds),
items,
total: items.reduce((a,b)=>a + b.qty*b.price, 0),
status: pick(["new","paid","shipped","cancelled"]),
createdAt: new Date(Date.now() - randInt(0,120)*86400000)
});
if (orders.length >= BATCH_SIZE) { colOrders.insertMany(orders, { ordered:false }); orders=[]; }
}
if (orders.length) colOrders.insertMany(orders, { ordered:false });
print("✅ verify data inserted");
print("counts:", {
users: colUsers.estimatedDocumentCount(),
products: colProducts.estimatedDocumentCount(),
orders: colOrders.estimatedDocumentCount()
});
1. 업그레이드 “전” 스냅샷 (같은 DB 내 저장)
const DB_NAME = "prd-sre-test-jti-create-test002";
const SNAP_COLL = "__verify_snapshots";
const EXCLUDE_PREFIX = "__verify_";
const ALLOWLIST = [];
const dbx = db.getSiblingDB(DB_NAME);
const snaps = dbx.getCollection(SNAP_COLL);
function listCollections() {
return dbx.runCommand({ listCollections: 1, nameOnly: false }).cursor.firstBatch;
}
function isIncluded(name) {
if (name.startsWith(EXCLUDE_PREFIX)) return false;
if (ALLOWLIST.length === 0) return true;
return ALLOWLIST.includes(name);
}
function helloSafe() {
try { return db.adminCommand({ hello: 1 }); }
catch (e) { try { return db.isMaster(); } catch (e2) { return {}; } }
}
function isMongos(hello) {
return hello && (hello.msg === "isdbgrid");
}
function fcwSafe() {
try {
const r = db.adminCommand({ getParameter: 1, featureCompatibilityVersion: 1 });
if (r && r.featureCompatibilityVersion) return { source: "getParameter", value: r.featureCompatibilityVersion };
} catch (_) {}
try {
const doc = db.getSiblingDB("admin").system.version.findOne({ _id: "featureCompatibilityVersion" });
if (doc) return { source: "system.version", value: { version: doc.version, targetVersion: doc.targetVersion ?? null } };
} catch (_) {}
try { return { source: "fallback", value: { serverVersion: db.version() } }; } catch (_) { return { source: "fallback", value: null }; }
}
function collStatsSafe(n) { try { return dbx.runCommand({ collStats: n, scale: 1 }); } catch (e) { return { ok:0, errmsg: e.message }; } }
function indexesSafe(n) { try { return dbx.getCollection(n).getIndexes(); } catch (e) { return [{ _error: e.message }]; } }
function dbHashSafe(names) {
try { return dbx.runCommand({ dbHash: 1, collections: names, full: true }); }
catch (e) { return { ok: 0, errmsg: e.message }; }
}
function sampleChecksum(collName, opts = { limitPerSide: 1000, stride: 100 }) {
const { limitPerSide, stride } = opts;
const col = dbx.getCollection(collName);
function rollingHashOfCursor(cur) {
let h = 0; let i = 0;
cur.forEach(doc => {
if ((i++ % stride) !== 0) return;
const s = JSON.stringify(doc);
for (let k = 0; k < s.length; k++) h = ((h << 5) - h + s.charCodeAt(k)) | 0;
});
return h|0;
}
const fwd = col.find({}).sort({ _id: 1 }).limit(limitPerSide);
const rev = col.find({}).sort({ _id: -1 }).limit(limitPerSide);
const hf = rollingHashOfCursor(fwd);
const hr = rollingHashOfCursor(rev);
const combined = (((hf & 0xffff) << 16) ^ (hr & 0xffff)) | 0;
return { hash32: combined, forwardSample: limitPerSide, backwardSample: limitPerSide, stride };
}
const snapshotId = new ObjectId();
print(`[PRE] start for ${DB_NAME}, snapshotId=${snapshotId}`);
const hello = helloSafe();
const atMongos = isMongos(hello);
const fcv = fcwSafe();
const colMetaAll = listCollections().map(c => ({
name: c.name,
type: c.type,
options: {
validator: c.options?.validator ?? null,
timeseries: c.options?.timeseries ?? null,
clusteredIndex: c.options?.clusteredIndex ?? null
}
}));
const targetCols = colMetaAll.filter(c => c.type === "collection" && isIncluded(c.name)).map(c => c.name);
const stats = {}, indexes = {};
targetCols.forEach(n => { stats[n] = collStatsSafe(n); indexes[n] = indexesSafe(n); });
let hashMode = "";
let hashPayload = {};
if (!atMongos) {
const dh = dbHashSafe(targetCols);
if (dh && dh.ok === 1) {
hashMode = "dbHash";
hashPayload = dh;
} else {
hashMode = "sample32";
const samples = {};
targetCols.forEach(n => { samples[n] = sampleChecksum(n); });
hashPayload = { samples };
}
} else {
hashMode = "sample32";
const samples = {};
targetCols.forEach(n => { samples[n] = sampleChecksum(n); });
hashPayload = { samples };
}
snaps.insertOne({
_id: snapshotId,
role: "pre",
db: DB_NAME,
createdAt: new Date(),
hello,
fcv,
hashMode,
hashPayload,
collections: colMetaAll,
targetCols,
stats,
indexes
}, { writeConcern: { w: "majority" } });
print(`[PRE] saved (mode=${hashMode}).`);
2.업그레이드 “후” 비교 (같은 DB 내 저장)
(() => {
const DB_NAME = "prd-sre-test-jti-create-test002";
const SNAP_COLL = "__verify_snapshots";
const EXCLUDE_PREFIX = "__verify_";
const ALLOWLIST = [];
const dbx = db.getSiblingDB(DB_NAME);
const snaps = dbx.getCollection(SNAP_COLL);
function latestPre() { return snaps.find({ db: DB_NAME, role: "pre" }).sort({ createdAt: -1 }).limit(1).toArray()[0] || null; }
function listCollections() { return dbx.runCommand({ listCollections: 1, nameOnly: false }).cursor.firstBatch; }
function isIncluded(name) { if (name.startsWith(EXCLUDE_PREFIX)) return false; if (ALLOWLIST.length===0) return true; return ALLOWLIST.includes(name); }
function collStatsSafe(n){ try { return dbx.runCommand({ collStats: n, scale: 1 }); } catch(e){ return { ok:0, errmsg: e.message }; } }
function indexesSafe(n){ try { return dbx.getCollection(n).getIndexes(); } catch(e){ return [{ _error: e.message }]; } }
function normalizeIndex(ix){
const keep={}; ["name","key","unique","partialFilterExpression","sparse","expireAfterSeconds","weights","default_language","language_override","textIndexVersion","wildcardProjection","collation"]
.forEach(k=>{ if (ix[k] !== undefined) keep[k]=ix[k]; });
return keep;
}
function helloSafe(){ try { return db.adminCommand({ hello: 1 }); } catch(e){ try{ return db.isMaster(); } catch(e2){ return {}; } } }
function isMongos(hello){ return hello && (hello.msg === "isdbgrid"); }
function dbHashSafe(names){ try { return dbx.runCommand({ dbHash: 1, collections: names, full: true }); } catch(e){ return { ok:0, errmsg:e.message }; } }
function sampleChecksum(collName, opts = { limitPerSide: 1000, stride: 100 }) {
const { limitPerSide, stride } = opts;
const col = dbx.getCollection(collName);
function rollingHashOfCursor(cur){
let h=0, i=0; cur.forEach(doc=>{
if ((i++ % stride) !== 0) return;
const s = JSON.stringify(doc);
for (let k=0;k<s.length;k++) h = ((h<<5) - h + s.charCodeAt(k)) | 0;
}); return h|0;
}
const fwd = col.find({}).sort({ _id: 1 }).limit(limitPerSide);
const rev = col.find({}).sort({ _id: -1 }).limit(limitPerSide);
const hf = rollingHashOfCursor(fwd);
const hr = rollingHashOfCursor(rev);
const combined = (((hf & 0xffff) << 16) ^ (hr & 0xffff)) | 0;
return { hash32: combined, forwardSample: limitPerSide, backwardSample: limitPerSide, stride };
}
const pre = latestPre();
if (!pre) {
print("ERROR: No PRE snapshot found. 먼저 01_pre_snapshot_fixed.js를 실행해 주세요.");
return;
}
print(`[POST] comparing with PRE ${pre._id} @ ${pre.createdAt}, mode=${pre.hashMode}`);
const hello = helloSafe();
const atMongos = isMongos(hello);
const colMetaNowAll = listCollections().map(c => ({
name: c.name, type: c.type,
options: {
validator: c.options?.validator ?? null,
timeseries: c.options?.timeseries ?? null,
clusteredIndex: c.options?.clusteredIndex ?? null
}
}));
const targetColsNow = colMetaNowAll.filter(c => c.type==="collection" && isIncluded(c.name)).map(c => c.name);
const statsNow = {}, indexesNow = {};
targetColsNow.forEach(n => { statsNow[n] = collStatsSafe(n); indexesNow[n] = indexesSafe(n).map(normalizeIndex); });
function diffCounts(preStats, nowStats){
const out=[]; const names = new Set([...Object.keys(preStats||{}), ...Object.keys(nowStats||{})]);
names.forEach(n=>{
if (!isIncluded(n)) return;
const a = preStats?.[n], b = nowStats?.[n];
const ac = a?.count, bc = b?.count;
if (ac !== bc) out.push({ collection:n, count: `${ac} → ${bc}` });
}); return out;
}
function diffIndexes(preIx, nowIx){
const out=[]; const names = new Set([...Object.keys(preIx||{}), ...Object.keys(nowIx||{})]);
const sig = x => JSON.stringify(x);
names.forEach(n=>{
if (!isIncluded(n)) return;
const A = (preIx?.[n]||[]).map(normalizeIndex);
const B = (nowIx?.[n]||[]).map(normalizeIndex);
const SA = new Set(A.map(sig)), SB = new Set(B.map(sig));
const missing = A.filter(x=>!SB.has(sig(x)));
const added = B.filter(x=>!SA.has(sig(x)));
if (missing.length || added.length) out.push({ collection:n, missingFromPost:missing, addedInPost:added });
}); return out;
}
function diffOptions(preColsAll, nowColsAll){
const out=[]; const preMap=Object.fromEntries((preColsAll||[]).map(x=>[x.name,x])); const nowMap=Object.fromEntries((nowColsAll||[]).map(x=>[x.name,x]));
const names=new Set([...Object.keys(preMap), ...Object.keys(nowMap)]);
names.forEach(n=>{
if (!isIncluded(n)) return;
const a = preMap[n], b = nowMap[n];
if (!a && b) out.push({ collection:n, change:"new collection" });
else if (a && !b) out.push({ collection:n, change:"collection removed" });
else if (a && b) {
const diffs=[]; ["validator","timeseries","clusteredIndex"].forEach(k=>{
if (JSON.stringify(a.options?.[k]) !== JSON.stringify(b.options?.[k])) diffs.push({ field:k, pre:a.options?.[k], post:b.options?.[k] });
});
if (diffs.length) out.push({ collection:n, diffs });
}
}); return out;
}
let hashDiff = [];
if (pre.hashMode === "dbHash") {
const dh = dbHashSafe(targetColsNow);
if (dh && dh.ok === 1) {
const preMap = pre.hashPayload?.collections || {};
const nowMap = dh.collections || {};
const names = new Set([...Object.keys(preMap), ...Object.keys(nowMap)]);
names.forEach(n=>{
if (!isIncluded(n)) return;
const a = preMap[n], b = nowMap[n];
if (!a && b) hashDiff.push({ collection:n, change:"added(no PRE hash)" });
else if (a && !b) hashDiff.push({ collection:n, change:"removed(no POST hash)" });
else if (a.md5 !== b.md5) hashDiff.push({ collection:n, change:`hash changed ${a.md5} → ${b.md5}` });
});
} else {
const samplesNow = {};
targetColsNow.forEach(n => { samplesNow[n] = sampleChecksum(n); });
hashDiff.push({ note: "dbHash unavailable at POST; used sample32 fallback", samplesNow });
}
} else {
const preSamples = pre.hashPayload?.samples || {};
const diff = [];
targetColsNow.forEach(n=>{
if (!isIncluded(n)) return;
const nowS = sampleChecksum(n);
const preS = preSamples[n];
if (!preS) diff.push({ collection:n, change:"no PRE sample" });
else if (preS.hash32 !== nowS.hash32) diff.push({ collection:n, change:`sample hash ${preS.hash32} → ${nowS.hash32}`, pre:preS, post:nowS });
});
const preNames = new Set(Object.keys(preSamples));
const nowNames = new Set(targetColsNow);
[...nowNames].forEach(n=>{ if (!preNames.has(n)) diff.push({ collection:n, change:"added in POST" }); });
[...preNames].forEach(n=>{ if (!nowNames.has(n)) diff.push({ collection:n, change:"removed in POST" }); });
hashDiff = diff;
}
const statsDiff = diffCounts(pre.stats, statsNow);
const idxDiff = diffIndexes(pre.indexes, indexesNow);
const optDiff = diffOptions(pre.collections, colMetaNowAll);
print("\n=== HASH DIFF ==="); printjson(hashDiff);
print("\n=== COUNT/STATS DIFF ==="); printjson(statsDiff);
print("\n=== INDEX DIFF ==="); printjson(idxDiff);
print("\n=== COLLECTION OPTION DIFF ==="); printjson(optDiff);
snaps.insertOne({
role: "post",
db: DB_NAME,
createdAt: new Date(),
compareWith: pre._id,
postHello: hello,
hashModePre: pre.hashMode,
result: { hashDiff, statsDiff, idxDiff, optDiff },
statsNow, indexesNow, colMetaNowAll
}, { writeConcern: { w: "majority" } });
print("\n[POST] saved & compare finished (mongos/mongod auto).");
})();