Re-sync Improvements
When table data needs resync (ie, when checksum fails), Instead of requesting the entire data, following procedure is done - row-group (based on id column) hashes is requested from master - hashes are checked on the DB - request chunks that have failed hash check
This commit is contained in:
parent
d30603134f
commit
12e90abe8b
@ -288,6 +288,9 @@ function startBackupTransmitter(server) {
|
|||||||
case "BACKUP_SYNC":
|
case "BACKUP_SYNC":
|
||||||
sync.sendBackupData(request.last_time, request.checksum, ws);
|
sync.sendBackupData(request.last_time, request.checksum, ws);
|
||||||
break;
|
break;
|
||||||
|
case "HASH_SYNC":
|
||||||
|
sync.sendTableHash(request.tables, ws);
|
||||||
|
break;
|
||||||
case "RE_SYNC":
|
case "RE_SYNC":
|
||||||
sync.sendTableData(request.tables, ws);
|
sync.sendTableData(request.tables, ws);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@ -3,7 +3,8 @@
|
|||||||
const WAIT_TIME = 10 * 60 * 1000,
|
const WAIT_TIME = 10 * 60 * 1000,
|
||||||
BACKUP_INTERVAL = 1 * 60 * 1000,
|
BACKUP_INTERVAL = 1 * 60 * 1000,
|
||||||
CHECKSUM_INTERVAL = 15, //times of BACKUP_INTERVAL
|
CHECKSUM_INTERVAL = 15, //times of BACKUP_INTERVAL
|
||||||
SINK_KEY_INDICATOR = '$$$';
|
SINK_KEY_INDICATOR = '$$$',
|
||||||
|
HASH_ROW_COUNT = 100;
|
||||||
|
|
||||||
var DB; //Container for Database connection
|
var DB; //Container for Database connection
|
||||||
var masterWS = null; //Container for Master websocket connection
|
var masterWS = null; //Container for Master websocket connection
|
||||||
@ -219,6 +220,14 @@ function processBackupData(response) {
|
|||||||
case "SYNC_CHECKSUM":
|
case "SYNC_CHECKSUM":
|
||||||
self.checksum = response.checksum;
|
self.checksum = response.checksum;
|
||||||
break;
|
break;
|
||||||
|
case "SYNC_HASH":
|
||||||
|
verifyHash(response.hashes)
|
||||||
|
.then(mismatch => requestTableChunks(mismatch))
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error);
|
||||||
|
self.close();
|
||||||
|
});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -337,12 +346,11 @@ function verifyChecksum(checksum_ref) {
|
|||||||
mismatch.push(table);
|
mismatch.push(table);
|
||||||
console.debug("Mismatch:", mismatch);
|
console.debug("Mismatch:", mismatch);
|
||||||
if (!mismatch.length) //Checksum of every table is verified.
|
if (!mismatch.length) //Checksum of every table is verified.
|
||||||
return resolve(true);
|
resolve(true);
|
||||||
else //If one or more tables checksum is not correct, re-request the table data
|
else { //If one or more tables checksum is not correct, re-request the table data
|
||||||
Promise.allSettled(mismatch.map(t => DB.query("TRUNCATE " + t))).then(_ => {
|
requestHash(mismatch);
|
||||||
requestReSync(mismatch);
|
resolve(false);
|
||||||
resolve(false);
|
}
|
||||||
})
|
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
reject(false);
|
reject(false);
|
||||||
@ -350,12 +358,13 @@ function verifyChecksum(checksum_ref) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function requestReSync(tables) {
|
function requestHash(tables) {
|
||||||
|
//TODO: resync only necessary data (instead of entire table)
|
||||||
let self = requestInstance;
|
let self = requestInstance;
|
||||||
let request = {
|
let request = {
|
||||||
floID: global.myFloID,
|
floID: global.myFloID,
|
||||||
pubKey: global.myPubKey,
|
pubKey: global.myPubKey,
|
||||||
type: "RE_SYNC",
|
type: "HASH_SYNC",
|
||||||
tables: tables,
|
tables: tables,
|
||||||
req_time: Date.now()
|
req_time: Date.now()
|
||||||
};
|
};
|
||||||
@ -368,6 +377,60 @@ function requestReSync(tables) {
|
|||||||
self.add_count = self.delete_count = 0;
|
self.add_count = self.delete_count = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function verifyHash(hashes) {
|
||||||
|
const getHash = table => new Promise((res, rej) => {
|
||||||
|
DB.query("SHOW COLUMNS FROM " + table).then(result => {
|
||||||
|
let columns = result.map(r => r["Field"]).sort();
|
||||||
|
DB.query(`SELECT CEIL(id/${HASH_ROW_COUNT}) as group_id, MD5(GROUP_CONCAT(${columns.map(c => `IFNULL(${c}, "NULL")`).join()})) as hash FROM ${table} GROUP BY group_id ORDER BY group_id`)
|
||||||
|
.then(result => res(Object.fromEntries(result.map(r => [r.group_id, r.hash]))))
|
||||||
|
.catch(error => rej(error))
|
||||||
|
}).catch(error => rej(error))
|
||||||
|
});
|
||||||
|
const convertIntArray = obj => Object.keys(obj).map(i => parseInt(i));
|
||||||
|
const checkHash = (table, hash_ref) => new Promise((res, rej) => {
|
||||||
|
getHash(table).then(hash_cur => {
|
||||||
|
console.debug(hash_ref, hash_cur)
|
||||||
|
for (let i in hash_ref)
|
||||||
|
if (hash_ref[i] === hash_cur[i]) {
|
||||||
|
delete hash_ref[i];
|
||||||
|
delete hash_cur[i];
|
||||||
|
}
|
||||||
|
console.debug("HashDiff:", hash_ref, hash_cur);
|
||||||
|
res([convertIntArray(hash_ref), convertIntArray(hash_cur)]);
|
||||||
|
}).catch(error => rej(error))
|
||||||
|
})
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let tables = Object.keys(hashes);
|
||||||
|
Promise.allSettled(tables.map(t => checkHash(t, hashes[t]))).then(result => {
|
||||||
|
let mismatch = {};
|
||||||
|
for (let t in tables)
|
||||||
|
if (result[t].status === "fulfilled") {
|
||||||
|
mismatch[tables[t]] = result[t].value; //Data that are incorrect/missing/deleted
|
||||||
|
//Data to be deleted (incorrect data will be added by resync)
|
||||||
|
let id_end = result[t].value[1].map(i => i * HASH_ROW_COUNT); //eg if i=2 AND H_R_C = 5 then id_end = 2 * 5 = 10 (ie, range 6-10)
|
||||||
|
Promise.allSettled(id_end.map(i =>
|
||||||
|
DB.query(`DELETE FROM ${tables[t]} WHERE id BETWEEN ${i - HASH_ROW_COUNT + 1} AND ${i}`))) //eg, i - HASH_ROW_COUNT + 1 = 10 - 5 + 1 = 6
|
||||||
|
.then(_ => null);
|
||||||
|
} else
|
||||||
|
console.error(result[t].reason);
|
||||||
|
console.debug("mismatch", mismatch);
|
||||||
|
resolve(mismatch);
|
||||||
|
}).catch(error => reject(error))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestTableChunks(tables, ws) {
|
||||||
|
let request = {
|
||||||
|
floID: global.myFloID,
|
||||||
|
pubKey: global.myPubKey,
|
||||||
|
type: "RE_SYNC",
|
||||||
|
tables: tables,
|
||||||
|
req_time: Date.now()
|
||||||
|
};
|
||||||
|
request.sign = floCrypto.signData(request.type + "|" + request.req_time, global.myPrivKey);
|
||||||
|
ws.send(JSON.stringify(tables));
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
SINK_KEY_INDICATOR,
|
SINK_KEY_INDICATOR,
|
||||||
set DB(db) {
|
set DB(db) {
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
var DB; //Container for database
|
var DB; //Container for database
|
||||||
|
const HASH_ROW_COUNT = 100;
|
||||||
|
|
||||||
//Backup Transfer
|
//Backup Transfer
|
||||||
function sendBackupData(timestamp, checksum, ws) {
|
function sendBackupData(timestamp, checksum, ws) {
|
||||||
@ -110,11 +111,34 @@ function backupSync_checksum(ws) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sendTableHash(tables, ws) {
|
||||||
|
const getHash = table => new Promise((res, rej) => {
|
||||||
|
DB.query("SHOW COLUMNS FROM " + table).then(result => {
|
||||||
|
let columns = result.map(r => r["Field"]).sort();
|
||||||
|
DB.query(`SELECT CEIL(id/${HASH_ROW_COUNT}) as group_id, MD5(GROUP_CONCAT(${columns.map(c => `IFNULL(${c}, "NULL")`).join()})) as hash FROM ${table} GROUP BY group_id ORDER BY group_id`)
|
||||||
|
.then(result => res(Object.fromEntries(result.map(r => [r.group_id, r.hash]))))
|
||||||
|
.catch(error => rej(error))
|
||||||
|
}).catch(error => rej(error))
|
||||||
|
});
|
||||||
|
Promise.allSettled(tables.map(t => getHash(t))).then(result => {
|
||||||
|
let hashes = {};
|
||||||
|
for (let i in tables)
|
||||||
|
if (result[i].status === "fulfilled")
|
||||||
|
hashes[tables[i]] = result[i].value;
|
||||||
|
else
|
||||||
|
console.error(result[i].reason);
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
command: "SYNC_HASH",
|
||||||
|
hashes: hashes
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function sendTableData(tables, ws) {
|
function sendTableData(tables, ws) {
|
||||||
let promises = [
|
let promises = [
|
||||||
tableSync_data(tables, ws),
|
tableSync_data(tables, ws),
|
||||||
tableSync_delete(tables, ws),
|
tableSync_delete(tables, ws),
|
||||||
tableSync_checksum(tables, ws)
|
tableSync_checksum(Object.keys(tables), ws)
|
||||||
];
|
];
|
||||||
Promise.allSettled(promises).then(result => {
|
Promise.allSettled(promises).then(result => {
|
||||||
let failedSync = [];
|
let failedSync = [];
|
||||||
@ -137,54 +161,79 @@ function sendTableData(tables, ws) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function tableSync_delete(tables, ws) {
|
function tableSync_delete(tables, ws) {
|
||||||
|
let getDelete = (table, group_id) => new Promise((res, rej) => {
|
||||||
|
let id_end = group_id * HASH_ROW_COUNT,
|
||||||
|
id_start = id_end - HASH_ROW_COUNT + 1;
|
||||||
|
DB.query("SELCT * FROM _backup WHERE t_name=? AND mode is NULL AND id BETWEEN ? AND ?", [table, id_start, id_end])
|
||||||
|
.then(result => res(result))
|
||||||
|
.catch(error => rej(error))
|
||||||
|
})
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
DB.query(`SELECT * FROM _backup WHERE mode is NULL AND t_name IN (${Array(tables.length).fill("?").join()})`, tables).then(result => {
|
let promises = [];
|
||||||
|
for (let t in tables)
|
||||||
|
for (let g_id in tables[t][1]) //tables[t] is [convertIntArray(hash_ref), convertIntArray(hash_cur)]
|
||||||
|
promises.push(getDelete(t, g_id));
|
||||||
|
Promise.allSettled(promises).then(results => {
|
||||||
|
let delete_sync = results.filter(r => r.status === "fulfilled").map(r => r.value); //Filtered results
|
||||||
|
delete_sync = [].concat(...delete_sync); //Convert 2d array into 1d
|
||||||
ws.send(JSON.stringify({
|
ws.send(JSON.stringify({
|
||||||
command: "SYNC_DELETE",
|
command: "SYNC_DELETE",
|
||||||
delete_data: result
|
delete_data: delete_sync
|
||||||
}));
|
}));
|
||||||
resolve("deleteSync");
|
resolve("deleteSync");
|
||||||
}).catch(error => {
|
|
||||||
console.error(error);
|
|
||||||
reject("deleteSync");
|
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function tableSync_data(tables, ws) {
|
function tableSync_data(tables, ws) {
|
||||||
const sendTable = table => new Promise((res, rej) => {
|
const sendTable = (table, group_id) => new Promise((res, rej) => {
|
||||||
DB.query(`SELECT * FROM ${table}`)
|
let id_end = group_id * HASH_ROW_COUNT,
|
||||||
.then(data => {
|
id_start = id_end - HASH_ROW_COUNT + 1;
|
||||||
ws.send(JSON.stringify({
|
DB.query(`SELECT * FROM ${table} WHERE id BETWEEN ? AND ?`, [id_start, id_end]).then(data => {
|
||||||
table,
|
ws.send(JSON.stringify({
|
||||||
command: "SYNC_UPDATE",
|
table,
|
||||||
data
|
command: "SYNC_UPDATE",
|
||||||
}));
|
data
|
||||||
res(table);
|
}));
|
||||||
}).catch(error => {
|
res(table);
|
||||||
console.error(error);
|
}).catch(error => {
|
||||||
rej(table);
|
console.error(error);
|
||||||
});
|
rej(table);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
const getUpdate = (table, group_id) => new Promise((res, rej) => {
|
||||||
|
let id_end = group_id * HASH_ROW_COUNT,
|
||||||
|
id_start = id_end - HASH_ROW_COUNT + 1;
|
||||||
|
DB.query("SELCT * FROM _backup WHERE t_name=? AND mode=TRUE AND id BETWEEN ? AND ?", [table, id_start, id_end])
|
||||||
|
.then(result => res(result))
|
||||||
|
.catch(error => rej(error))
|
||||||
|
})
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
DB.query(`SELECT * FROM _backup WHERE mode=TRUE AND t_name IN (${Array(tables.length).fill("?").join()})`, tables).then(result => {
|
let promises = [];
|
||||||
|
for (let t in tables)
|
||||||
|
for (let g_id in tables[t][0]) //tables[t] is [convertIntArray(hash_ref), convertIntArray(hash_cur)]
|
||||||
|
promises.push(getUpdate(t, g_id));
|
||||||
|
Promise.allSettled(promises).then(results => {
|
||||||
|
let update_sync = results.filter(r => r.status === "fulfilled").map(r => r.value); //Filtered results
|
||||||
|
update_sync = [].concat(...update_sync); //Convert 2d array into 1d
|
||||||
ws.send(JSON.stringify({
|
ws.send(JSON.stringify({
|
||||||
command: "SYNC_HEADER",
|
command: "SYNC_HEADER",
|
||||||
add_data: result
|
add_data: update_sync
|
||||||
}));
|
}));
|
||||||
Promise.allSettled(tables.map(t => sendTable(t))).then(result => {
|
let promises = [];
|
||||||
|
for (let t in tables)
|
||||||
|
for (let g_id in tables[t][0]) //tables[t] is [convertIntArray(hash_ref), convertIntArray(hash_cur)]
|
||||||
|
promises.push(sendTable(t, g_id));
|
||||||
|
Promise.allSettled(promises).then(result => {
|
||||||
let failedTables = [];
|
let failedTables = [];
|
||||||
result.forEach(r => r.status === "rejected" ? failedTables.push(r.reason) : null);
|
result.forEach(r => r.status === "rejected" ? failedTables.push(r.reason) : null);
|
||||||
if (failedTables.length)
|
if (failedTables.length)
|
||||||
reject(["dataSync", failedTables]);
|
reject(["dataSync", [...new Set(failedTables)]]);
|
||||||
else
|
else
|
||||||
resolve("dataSync");
|
resolve("dataSync");
|
||||||
});
|
});
|
||||||
}).catch(error => {
|
|
||||||
console.error(error);
|
|
||||||
reject("dataSync");
|
|
||||||
});
|
});
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function tableSync_checksum(tables, ws) {
|
function tableSync_checksum(tables, ws) {
|
||||||
@ -206,6 +255,7 @@ function tableSync_checksum(tables, ws) {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
sendBackupData,
|
sendBackupData,
|
||||||
|
sendTableHash,
|
||||||
sendTableData,
|
sendTableData,
|
||||||
set DB(db) {
|
set DB(db) {
|
||||||
DB = db;
|
DB = db;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user