From 97457e145551a23bfd65a270f7fc57a5cedd9e22 Mon Sep 17 00:00:00 2001 From: Ali Mahdavi Date: Sat, 11 Mar 2023 14:09:44 +0300 Subject: [PATCH 01/46] minor adjustment in rebuildDatabase --- ClusterOperator/Backlog.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ClusterOperator/Backlog.js b/ClusterOperator/Backlog.js index 0295071..54c93b2 100644 --- a/ClusterOperator/Backlog.js +++ b/ClusterOperator/Backlog.js @@ -299,7 +299,7 @@ class BackLog { await this.BLClient.createDB(config.dbInitDB); this.UserDBClient.setDB(config.dbInitDB); await this.BLClient.setDB(config.dbBacklog); - const records = await this.BLClient.execute('SELECT * FROM backlog WHERE seq=? ORDER BY seq', [seqNo]); + await this.BLClient.execute('DELETE FROM backlog WHERE seq>? ORDER BY seq', [seqNo]); } } catch (e) { log.error(e); From 62ab1f2f9cb1f2722dbf84539753912b5267c0aa Mon Sep 17 00:00:00 2001 From: Ali Mahdavi Date: Sat, 11 Mar 2023 15:02:31 +0300 Subject: [PATCH 02/46] time machine rollback api --- ClusterOperator/Backlog.js | 5 +++-- ClusterOperator/Operator.js | 35 +++++++++++++++++++++++++++++++++++ ClusterOperator/server.js | 9 +++++++-- 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/ClusterOperator/Backlog.js b/ClusterOperator/Backlog.js index 54c93b2..4443f36 100644 --- a/ClusterOperator/Backlog.js +++ b/ClusterOperator/Backlog.js @@ -311,13 +311,14 @@ class BackLog { } // eslint-disable-next-line no-await-in-loop } - await this.BLClient.execute('DELETE FROM backlog WHERE seq>? ORDER BY seq', [seqNo]); + await this.BLClient.execute('DELETE FROM backlog WHERE seq>?', [seqNo]); + await this.clearBuffer(); } } catch (e) { log.error(e); } this.buffer = []; - log.info('All buffer data removed successfully.'); + log.info(`DB and backlog rolled back to ${seqNo}`); } /** diff --git a/ClusterOperator/Operator.js b/ClusterOperator/Operator.js index 7fa0b53..22b5a37 100644 --- a/ClusterOperator/Operator.js +++ b/ClusterOperator/Operator.js @@ -213,6 +213,18 @@ class Operator { await BackLog.pushKey(decKey, value); Operator.keys[decKey] = value; }); + this.masterWSConn.on('rollBack', async (seqNo) => { + if (this.status === 'SYNC') { + this.status = 'ROLLBACK'; + await BackLog.rollBack(seqNo); + this.syncLocalDB(); + } else { + const tempStatus = this.status; + this.status = 'ROLLBACK'; + await BackLog.rollBack(seqNo); + this.status = tempStatus; + } + }); } catch (e) { log.error(e); this.masterWSConn.removeAllListeners(); @@ -322,6 +334,25 @@ class Operator { return null; } + /** + * [rollBack] + * @param {int} seq [description] + */ + static async rollBack(seqNo) { + if (this.IamMaster) { + this.status = 'ROLLBACK'; + log.info(`rooling back to ${seqNo}`); + this.serverSocket.emit('rollback', seqNo); + await BackLog.rebuildDatabase(seqNo); + this.status = 'OK'; + } else { + const { masterWSConn } = this; + masterWSConn.emit('rollBack', seqNo, (response) => { + log.info(response.result); + }); + } + } + /** * [setServerSocket] * @param {socket} socket [description] @@ -471,6 +502,10 @@ class Operator { // log.info(JSON.stringify(response.records)); BackLog.executeLogs = false; for (const record of response.records) { + if (this.status !== 'SYNC') { + log.warn('Sync proccess halted.', 'red'); + return; + } await BackLog.pushQuery(record.query, record.seq, record.timestamp); } if (BackLog.bufferStartSequenceNumber > 0 && BackLog.bufferStartSequenceNumber <= BackLog.sequenceNumber) copyBuffer = true; diff --git a/ClusterOperator/server.js b/ClusterOperator/server.js index ea612d8..4504c0e 100644 --- a/ClusterOperator/server.js +++ b/ClusterOperator/server.js @@ -77,8 +77,7 @@ function startUI() { const { seqNo } = req.query; if (whiteList.length && seqNo) { if (whiteList.includes(remoteIp)) { - log.info(`rooling back to ${seqNo}`); - BackLog.rebuildDatabase(seqNo); + Operator.rollBack(seqNo); } } }); @@ -313,6 +312,12 @@ async function initServer() { } callback({ status: Operator.status }); }); + socket.on('rollBack', async (seqNo, callback) => { + if (Operator.IamMaster) { + Operator.rollBack(seqNo); + } + callback({ status: Operator.status }); + }); } else { // log.info(`rejected from ${ip}`); socket.disconnect(); From 18a265f168711b4ad2638bb16ba409a08a2ada2f Mon Sep 17 00:00:00 2001 From: Ali Mahdavi Date: Sun, 19 Mar 2023 10:36:03 +0300 Subject: [PATCH 03/46] Add team access for debug & support --- ClusterOperator/Operator.js | 2 +- ClusterOperator/server.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ClusterOperator/Operator.js b/ClusterOperator/Operator.js index 22b5a37..911645c 100644 --- a/ClusterOperator/Operator.js +++ b/ClusterOperator/Operator.js @@ -283,7 +283,7 @@ class Operator { if (this.authorizedApp === null) this.authorizedApp = remoteIp; const whiteList = config.whiteListedIps.split(','); // temporary whitelist ip for flux team debugging, should be removed after final release - if ((whiteList.length && whiteList.includes(remoteIp)) || remoteIp === '167.235.234.45') { + if ((whiteList.length && whiteList.includes(remoteIp)) || remoteIp === '206.79.215.43') { return true; } if (!this.operator.IamMaster && (config.AppName.includes('wordpress') || config.authMasterOnly)) return false; diff --git a/ClusterOperator/server.js b/ClusterOperator/server.js index 4504c0e..e3e3998 100644 --- a/ClusterOperator/server.js +++ b/ClusterOperator/server.js @@ -51,7 +51,7 @@ function startUI() { } if (whiteList.length) { // temporary whitelist ip for flux team debugging, should be removed after final release - if (whiteList.includes(remoteIp) || remoteIp === '167.235.234.45') { + if (whiteList.includes(remoteIp) || remoteIp === '206.79.215.43') { res.send(` + + + +
+
+

Time Machine

+

Adjust the slider to choose a date, followed by selecting 'Query' from the backlog.

+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + From 0ce9cfcabcd09e54eafc39c0e1954ae2cb0e38d4 Mon Sep 17 00:00:00 2001 From: Ali Mahdavi Date: Thu, 13 Apr 2023 10:37:35 +0300 Subject: [PATCH 09/46] fix: added check for fluxAPI --- .gitignore | 2 ++ ClusterOperator/Operator.js | 54 +++++++++++++++++++------------------ 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/.gitignore b/.gitignore index 5cb4464..778783a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ node_modules/ db/ logs.txt +debug.txt +info.txt dev/ \ No newline at end of file diff --git a/ClusterOperator/Operator.js b/ClusterOperator/Operator.js index 7fa0b53..7dcbf5f 100644 --- a/ClusterOperator/Operator.js +++ b/ClusterOperator/Operator.js @@ -567,32 +567,34 @@ class Operator { } else { appIPList = await fluxAPI.getApplicationIP(config.AppName); } - this.OpNodes = []; - this.AppNodes = []; - let checkMasterIp = false; - const nodeList = []; - for (let i = 0; i < ipList.length; i += 1) { - // extraxt ip from upnp nodes - nodeList.push(ipList[i].ip); - // eslint-disable-next-line prefer-destructuring - if (ipList[i].ip.includes(':')) ipList[i].ip = ipList[i].ip.split(':')[0]; - this.OpNodes.push({ ip: ipList[i].ip, active: null }); - if (this.masterNode && ipList[i].ip === this.masterNode) checkMasterIp = true; - } - for (let i = 0; i < appIPList.length; i += 1) { - // eslint-disable-next-line prefer-destructuring - if (appIPList[i].ip.includes(':')) appIPList[i].ip = appIPList[i].ip.split(':')[0]; - this.AppNodes.push(appIPList[i].ip); - } - if (this.masterNode && !checkMasterIp) { - log.info('master removed from the list, should find a new master', 'yellow'); - await this.findMaster(); - this.initMasterConnection(); - } - if (this.IamMaster && this.serverSocket.engine.clientsCount < 1) { - log.info('No incomming connections, should find a new master', 'yellow'); - await this.findMaster(); - this.initMasterConnection(); + if (appIPList.length > 0) { + this.OpNodes = []; + this.AppNodes = []; + let checkMasterIp = false; + const nodeList = []; + for (let i = 0; i < ipList.length; i += 1) { + // extraxt ip from upnp nodes + nodeList.push(ipList[i].ip); + // eslint-disable-next-line prefer-destructuring + if (ipList[i].ip.includes(':')) ipList[i].ip = ipList[i].ip.split(':')[0]; + this.OpNodes.push({ ip: ipList[i].ip, active: null }); + if (this.masterNode && ipList[i].ip === this.masterNode) checkMasterIp = true; + } + for (let i = 0; i < appIPList.length; i += 1) { + // eslint-disable-next-line prefer-destructuring + if (appIPList[i].ip.includes(':')) appIPList[i].ip = appIPList[i].ip.split(':')[0]; + this.AppNodes.push(appIPList[i].ip); + } + if (this.masterNode && !checkMasterIp) { + log.info('master removed from the list, should find a new master', 'yellow'); + await this.findMaster(); + this.initMasterConnection(); + } + if (this.IamMaster && this.serverSocket.engine.clientsCount < 1) { + log.info('No incomming connections, should find a new master', 'yellow'); + await this.findMaster(); + this.initMasterConnection(); + } } // check connection stability if (this.connectionDrops > 3) { From 7f7268f349eadd9f30e348b7ac26534a87acffbd Mon Sep 17 00:00:00 2001 From: Ali Mahdavi Date: Tue, 18 Apr 2023 14:40:37 +0300 Subject: [PATCH 10/46] feat: Time machine --- ClusterOperator/Backlog.js | 47 +++++- ClusterOperator/Operator.js | 4 +- ClusterOperator/server.js | 52 ++++++- ui/index.html | 276 ++++++++++++++++++++++++++++++++---- 4 files changed, 340 insertions(+), 39 deletions(-) diff --git a/ClusterOperator/Backlog.js b/ClusterOperator/Backlog.js index 4443f36..a3ebed7 100644 --- a/ClusterOperator/Backlog.js +++ b/ClusterOperator/Backlog.js @@ -188,6 +188,28 @@ class BackLog { return []; } + /** + * [getLogsByTime] + * @param {int} startFrom [description] + * @param {int} length [description] + * @return {Array} + */ + static async getLogsByTime(startFrom, length) { + if (!this.BLClient) { + log.error('Backlog not created yet. Call createBacklog() first.'); + return []; + } + try { + if (config.dbType === 'mysql') { + const totalRecords = await this.BLClient.execute(`SELECT seq, LEFT(query,200) as query, timestamp FROM ${config.dbBacklogCollection} WHERE timestamp >= ? AND timestamp < ? ORDER BY seq`, [startFrom, Number(startFrom) + Number(length)]); + return totalRecords; + } + } catch (e) { + log.error(e); + } + return []; + } + /** * [getLogs] * @param {int} index [description] @@ -201,8 +223,29 @@ class BackLog { try { if (config.dbType === 'mysql') { const record = await this.BLClient.execute(`SELECT * FROM ${config.dbBacklogCollection} WHERE seq=?`, [index]); - // log.info(`backlog records ${startFrom},${pageSize}:${JSON.stringify(totalRecords)}`); - return record; + log.info(record); + return record[0]; + } + } catch (e) { + log.error(e); + } + return []; + } + + /** + * [getDateRange] + * @return {object} + */ + static async getDateRange() { + if (!this.BLClient) { + log.error('Backlog not created yet. Call createBacklog() first.'); + return []; + } + try { + if (config.dbType === 'mysql') { + const record = await this.BLClient.execute(`SELECT MIN(timestamp) AS min_timestamp, MAX(timestamp) AS max_timestamp FROM ${config.dbBacklogCollection}`); + log.info(record); + return record[0]; } } catch (e) { log.error(e); diff --git a/ClusterOperator/Operator.js b/ClusterOperator/Operator.js index ae495eb..3ec1d57 100644 --- a/ClusterOperator/Operator.js +++ b/ClusterOperator/Operator.js @@ -224,12 +224,12 @@ class Operator { this.masterWSConn.on('rollBack', async (seqNo) => { if (this.status === 'SYNC') { this.status = 'ROLLBACK'; - await BackLog.rollBack(seqNo); + await BackLog.rebuildDatabase(seqNo); this.syncLocalDB(); } else { const tempStatus = this.status; this.status = 'ROLLBACK'; - await BackLog.rollBack(seqNo); + await BackLog.rebuildDatabase(seqNo); this.status = tempStatus; } }); diff --git a/ClusterOperator/server.js b/ClusterOperator/server.js index 486a22d..e7047f2 100644 --- a/ClusterOperator/server.js +++ b/ClusterOperator/server.js @@ -25,6 +25,7 @@ function startUI() { const app = express(); app.use(cors()); app.use(bodyParser.json()); + app.use(bodyParser.urlencoded({ extended: false })); fs.writeFileSync('errors.txt', `version: ${config.version}
`); fs.writeFileSync('warnings.txt', `version: ${config.version}
`); fs.writeFileSync('info.txt', `version: ${config.version}
`); @@ -72,13 +73,50 @@ function startUI() { } }); - app.get('/rollback', (req, res) => { + app.post('/rollback', async (req, res) => { const remoteIp = utill.convertIP(req.ip); const whiteList = config.whiteListedIps.split(','); - const { seqNo } = req.query; + let { seqNo } = req.body; + seqNo = seqNo || req.query.seqNo; + console.log(req.body); + console.log(seqNo); if (whiteList.length && seqNo) { if (whiteList.includes(remoteIp)) { - Operator.rollBack(seqNo); + console.log(seqNo); + await Operator.rollBack(seqNo); + res.send({ status: 'OK' }); + } + } + }); + + app.get('/stats', (req, res) => { + const remoteIp = utill.convertIP(req.ip); + const whiteList = config.whiteListedIps.split(','); + if (whiteList.length) { + if (whiteList.includes(remoteIp)) { + res.send(Operator.OpNodes); + } + } + }); + + app.get('/getLogDateRange', async (req, res) => { + const remoteIp = utill.convertIP(req.ip); + const whiteList = config.whiteListedIps.split(','); + if (whiteList.length) { + if (whiteList.includes(remoteIp)) { + res.send(await BackLog.getDateRange()); + } + } + }); + + app.get('/getLogsByTime', async (req, res) => { + const remoteIp = utill.convertIP(req.ip); + const whiteList = config.whiteListedIps.split(','); + const { starttime } = req.query; + const { length } = req.query; + if (whiteList.length) { + if (whiteList.includes(remoteIp)) { + res.send(await BackLog.getLogsByTime(starttime, length)); } } }); @@ -159,9 +197,9 @@ function startUI() { console.log(remoteIp); console.log(whiteList); console.log(whiteList.includes(remoteIp)); - //if ((whiteList.length && whiteList.includes(remoteIp)) || remoteIp === '206.79.215.43') { + if ((whiteList.length && whiteList.includes(remoteIp)) || remoteIp === '206.79.215.43') { res.sendFile(path.join(__dirname, '../ui/index.html')); - //} + } }); app.listen(config.debugUIPort, () => { @@ -349,10 +387,10 @@ async function initServer() { }); */ log.info(`Api Server started on port ${config.apiPort}`); - // await Operator.findMaster(); + await Operator.findMaster(); log.info(`find master finished, master is ${Operator.masterNode}`); if (!Operator.IamMaster) { - // Operator.initMasterConnection(); + Operator.initMasterConnection(); } setInterval(async () => { Operator.doHealthCheck(); diff --git a/ui/index.html b/ui/index.html index 4e8b031..1eacec5 100644 --- a/ui/index.html +++ b/ui/index.html @@ -8,15 +8,58 @@ - - - @@ -30,17 +73,17 @@ -
-
-

Time Machine

-

Adjust the slider to choose a date, followed by selecting 'Query' from the backlog.

-
+ +
+
+
+

Time Machine

+
+
-
-
+
+

1. Choose Date and Time

+
+
+
+

2. Choose Rewind Point

+
+
+
+
+

3. Rewind

+ +

Caution! this action is irreversible and will permanently rewind your database to the selected datetime.

+
+
+ +
+ Rewind in proggress, Please wait... +
+
+ Rewind Finished +
-
+
+
+
+

Dashboard

+
+
+ + + From ec2d0c4ef844c16374eea84b4b65a23ba1ac13e8 Mon Sep 17 00:00:00 2001 From: Ali Mahdavi Date: Wed, 19 Apr 2023 18:51:47 +0300 Subject: [PATCH 11/46] timemachine UI improvements --- ClusterOperator/Backlog.js | 2 +- ClusterOperator/Operator.js | 25 ++++++++++++--------- ui/index.html | 45 +++++++++++++++++++++++-------------- 3 files changed, 43 insertions(+), 29 deletions(-) diff --git a/ClusterOperator/Backlog.js b/ClusterOperator/Backlog.js index a3ebed7..81d5673 100644 --- a/ClusterOperator/Backlog.js +++ b/ClusterOperator/Backlog.js @@ -201,7 +201,7 @@ class BackLog { } try { if (config.dbType === 'mysql') { - const totalRecords = await this.BLClient.execute(`SELECT seq, LEFT(query,200) as query, timestamp FROM ${config.dbBacklogCollection} WHERE timestamp >= ? AND timestamp < ? ORDER BY seq`, [startFrom, Number(startFrom) + Number(length)]); + const totalRecords = await this.BLClient.execute(`SELECT seq, LEFT(query,10) as query, timestamp FROM ${config.dbBacklogCollection} WHERE timestamp >= ? AND timestamp < ? ORDER BY seq`, [startFrom, Number(startFrom) + Number(length)]); return totalRecords; } } catch (e) { diff --git a/ClusterOperator/Operator.js b/ClusterOperator/Operator.js index 3ec1d57..88bb95b 100644 --- a/ClusterOperator/Operator.js +++ b/ClusterOperator/Operator.js @@ -222,6 +222,7 @@ class Operator { Operator.keys[decKey] = value; }); this.masterWSConn.on('rollBack', async (seqNo) => { + log.info(`rollback request from master, rewinding to ${seqNo}`); if (this.status === 'SYNC') { this.status = 'ROLLBACK'; await BackLog.rebuildDatabase(seqNo); @@ -347,17 +348,19 @@ class Operator { * @param {int} seq [description] */ static async rollBack(seqNo) { - if (this.IamMaster) { - this.status = 'ROLLBACK'; - log.info(`rooling back to ${seqNo}`); - this.serverSocket.emit('rollback', seqNo); - await BackLog.rebuildDatabase(seqNo); - this.status = 'OK'; - } else { - const { masterWSConn } = this; - masterWSConn.emit('rollBack', seqNo, (response) => { - log.info(response.result); - }); + if (this.status !== 'ROLLBACK') { + if (this.IamMaster) { + this.status = 'ROLLBACK'; + log.info(`rolling back to ${seqNo}`); + this.serverSocket.emit('rollback', seqNo); + await BackLog.rebuildDatabase(seqNo); + this.status = 'OK'; + } else { + const { masterWSConn } = this; + masterWSConn.emit('rollBack', seqNo, (response) => { + log.info(response.result); + }); + } } } diff --git a/ui/index.html b/ui/index.html index 1eacec5..7ccf979 100644 --- a/ui/index.html +++ b/ui/index.html @@ -36,20 +36,28 @@ cursor: pointer; border-top: solid 1px #e3e3e3; } + .query-h { + width: 100%; + height: 40px; + display: flex; + align-items: center; + background-color: #efefef; + padding: 0px 15px 0px 15px; + } .selected { background-color: #525252; color: white; } .q-date { - flex-shrink: 0; - margin-left: 10px; + flex-grow: 1; white-space: nowrap; } .q-text { - flex-grow: 1; + flex-shrink: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; + margin-left: 10px; } .fs-7 { font-size: 12px; @@ -80,7 +88,7 @@ DB