2017-12-15 11:16:26 +08:00
"use strict" ;
const sql = require ( './sql' ) ;
2018-04-03 09:25:20 +08:00
const sqlInit = require ( './sql_init' ) ;
2017-12-15 11:16:26 +08:00
const log = require ( './log' ) ;
2019-08-27 02:21:43 +08:00
const ws = require ( './ws.js' ) ;
2018-04-02 09:27:46 +08:00
const syncMutexService = require ( './sync_mutex' ) ;
2019-02-02 05:48:51 +08:00
const repository = require ( './repository' ) ;
2018-03-29 11:41:22 +08:00
const cls = require ( './cls' ) ;
2019-02-02 05:48:51 +08:00
const syncTableService = require ( './sync_table' ) ;
const Branch = require ( '../entities/branch' ) ;
2017-12-15 11:16:26 +08:00
2019-02-02 19:41:20 +08:00
let unrecoverableConsistencyErrors = false ;
2019-02-02 16:26:57 +08:00
let fixedIssues = false ;
2017-12-15 11:16:26 +08:00
2019-02-02 05:48:51 +08:00
async function findIssues ( query , errorCb ) {
const results = await sql . getRows ( query ) ;
2017-12-24 02:16:18 +08:00
2019-02-02 05:48:51 +08:00
for ( const res of results ) {
2019-02-02 17:38:33 +08:00
logError ( errorCb ( res ) ) ;
2017-12-15 11:16:26 +08:00
2019-02-02 19:41:20 +08:00
unrecoverableConsistencyErrors = true ;
2017-12-15 11:16:26 +08:00
}
2019-02-02 05:48:51 +08:00
return results ;
2017-12-15 11:16:26 +08:00
}
2019-02-02 05:48:51 +08:00
async function findAndFixIssues ( query , fixerCb ) {
const results = await sql . getRows ( query ) ;
for ( const res of results ) {
await fixerCb ( res ) ;
2019-02-02 16:26:57 +08:00
fixedIssues = true ;
2019-02-02 05:48:51 +08:00
}
return results ;
}
async function checkTreeCycles ( ) {
2018-01-02 08:41:22 +08:00
const childToParents = { } ;
2018-03-25 09:39:15 +08:00
const rows = await sql . getRows ( "SELECT noteId, parentNoteId FROM branches WHERE isDeleted = 0" ) ;
2018-01-02 08:41:22 +08:00
for ( const row of rows ) {
2018-01-29 08:30:14 +08:00
const childNoteId = row . noteId ;
const parentNoteId = row . parentNoteId ;
2018-01-02 08:41:22 +08:00
2018-10-22 03:37:34 +08:00
childToParents [ childNoteId ] = childToParents [ childNoteId ] || [ ] ;
2018-01-02 08:41:22 +08:00
childToParents [ childNoteId ] . push ( parentNoteId ) ;
}
2019-02-02 05:48:51 +08:00
function checkTreeCycle ( noteId , path ) {
2018-01-02 08:41:22 +08:00
if ( noteId === 'root' ) {
return ;
}
2018-10-22 04:42:20 +08:00
if ( ! childToParents [ noteId ] || childToParents [ noteId ] . length === 0 ) {
2019-02-02 16:26:57 +08:00
logError ( ` No parents found for note ${ noteId } ` ) ;
2019-02-02 05:48:51 +08:00
2019-02-02 19:41:20 +08:00
unrecoverableConsistencyErrors = true ;
2018-10-22 04:42:20 +08:00
return ;
}
2018-01-02 08:41:22 +08:00
for ( const parentNoteId of childToParents [ noteId ] ) {
if ( path . includes ( parentNoteId ) ) {
2019-02-02 16:26:57 +08:00
logError ( ` Tree cycle detected at parent-child relationship: ${ parentNoteId } - ${ noteId } , whole path: ${ path } ` ) ;
2019-02-02 05:48:51 +08:00
2019-02-02 19:41:20 +08:00
unrecoverableConsistencyErrors = true ;
2018-01-02 08:41:22 +08:00
}
else {
const newPath = path . slice ( ) ;
newPath . push ( noteId ) ;
2019-02-02 05:48:51 +08:00
checkTreeCycle ( parentNoteId , newPath ) ;
2018-01-02 08:41:22 +08:00
}
}
}
const noteIds = Object . keys ( childToParents ) ;
for ( const noteId of noteIds ) {
2019-02-02 05:48:51 +08:00
checkTreeCycle ( noteId , [ ] ) ;
2018-01-02 08:41:22 +08:00
}
2018-10-22 03:37:34 +08:00
if ( childToParents [ 'root' ] . length !== 1 || childToParents [ 'root' ] [ 0 ] !== 'none' ) {
2019-02-02 16:26:57 +08:00
logError ( 'Incorrect root parent: ' + JSON . stringify ( childToParents [ 'root' ] ) ) ;
2019-02-02 19:41:20 +08:00
unrecoverableConsistencyErrors = true ;
2018-10-22 03:37:34 +08:00
}
2018-01-02 08:41:22 +08:00
}
2019-02-02 17:38:33 +08:00
async function findBrokenReferenceIssues ( ) {
await findIssues ( `
SELECT branchId , branches . noteId
FROM branches LEFT JOIN notes USING ( noteId )
2019-02-03 23:27:26 +08:00
WHERE branches . isDeleted = 0 AND notes . noteId IS NULL ` ,
2019-02-02 17:38:33 +08:00
( { branchId , noteId } ) => ` Branch ${ branchId } references missing note ${ noteId } ` ) ;
2018-11-13 06:34:22 +08:00
2019-02-02 17:38:33 +08:00
await findIssues ( `
SELECT branchId , branches . noteId AS parentNoteId
FROM branches LEFT JOIN notes ON notes . noteId = branches . parentNoteId
2019-02-03 23:27:26 +08:00
WHERE branches . isDeleted = 0 AND branches . branchId != 'root' AND notes . noteId IS NULL ` ,
2019-02-02 17:38:33 +08:00
( { branchId , noteId } ) => ` Branch ${ branchId } references missing parent note ${ noteId } ` ) ;
2019-01-22 05:51:49 +08:00
2019-02-02 17:38:33 +08:00
await findIssues ( `
SELECT attributeId , attributes . noteId
FROM attributes LEFT JOIN notes USING ( noteId )
2019-02-03 23:27:26 +08:00
WHERE attributes . isDeleted = 0 AND notes . noteId IS NULL ` ,
2019-02-02 17:38:33 +08:00
( { attributeId , noteId } ) => ` Attribute ${ attributeId } references missing source note ${ noteId } ` ) ;
2019-01-22 05:46:27 +08:00
2019-02-02 17:38:33 +08:00
// empty targetNoteId for relations is a special fixable case so not covered here
await findIssues ( `
2019-02-03 23:22:45 +08:00
SELECT attributeId , attributes . value AS noteId
2019-02-02 17:38:33 +08:00
FROM attributes LEFT JOIN notes ON notes . noteId = attributes . value
2019-02-03 23:27:26 +08:00
WHERE attributes . isDeleted = 0 AND attributes . type = 'relation'
AND attributes . value != '' AND notes . noteId IS NULL ` ,
2019-02-02 17:38:33 +08:00
( { attributeId , noteId } ) => ` Relation ${ attributeId } references missing note ${ noteId } ` ) ;
2019-01-19 02:32:59 +08:00
2019-02-02 17:38:33 +08:00
await findIssues ( `
SELECT noteRevisionId , note _revisions . noteId
FROM note _revisions LEFT JOIN notes USING ( noteId )
WHERE notes . noteId IS NULL ` ,
( { noteRevisionId , noteId } ) => ` Note revision ${ noteRevisionId } references missing note ${ noteId } ` ) ;
}
2019-01-19 02:32:59 +08:00
2019-02-02 18:26:27 +08:00
async function findExistencyIssues ( ) {
2019-08-20 02:19:42 +08:00
// principle for fixing inconsistencies is that if the note itself is deleted (isDeleted=true) then all related entities should be also deleted (branches, attributes)
2019-02-02 17:38:33 +08:00
// but if note is not deleted, then at least one branch should exist.
2019-02-02 05:48:51 +08:00
2019-02-02 19:41:20 +08:00
// the order here is important - first we might need to delete inconsistent branches and after that
// another check might create missing branch
await findAndFixIssues ( `
SELECT
branchId , noteId
FROM
branches
JOIN notes USING ( noteId )
WHERE
notes . isDeleted = 1
AND branches . isDeleted = 0 ` ,
async ( { branchId , noteId } ) => {
const branch = await repository . getBranch ( branchId ) ;
branch . isDeleted = true ;
await branch . save ( ) ;
logFix ( ` Branch ${ branchId } has been deleted since associated note ${ noteId } is deleted. ` ) ;
} ) ;
2019-02-02 05:48:51 +08:00
await findAndFixIssues ( `
SELECT
2019-02-02 19:41:20 +08:00
branchId , parentNoteId
FROM
branches
JOIN notes AS parentNote ON parentNote . noteId = branches . parentNoteId
WHERE
parentNote . isDeleted = 1
AND branches . isDeleted = 0
` , async ({branchId, parentNoteId}) => {
const branch = await repository . getBranch ( branchId ) ;
branch . isDeleted = true ;
await branch . save ( ) ;
logFix ( ` Branch ${ branchId } has been deleted since associated parent note ${ parentNoteId } is deleted. ` ) ;
} ) ;
await findAndFixIssues ( `
SELECT
DISTINCT notes . noteId
2019-02-02 05:48:51 +08:00
FROM
notes
2019-02-02 19:41:20 +08:00
LEFT JOIN branches ON notes . noteId = branches . noteId AND branches . isDeleted = 0
2019-02-02 05:48:51 +08:00
WHERE
2019-02-02 19:41:20 +08:00
notes . isDeleted = 0
AND branches . branchId IS NULL
2019-02-02 05:48:51 +08:00
` , async ({noteId}) => {
const branch = await new Branch ( {
parentNoteId : 'root' ,
noteId : noteId ,
prefix : 'recovered'
} ) . save ( ) ;
2019-02-02 16:26:57 +08:00
logFix ( ` Created missing branch ${ branch . branchId } for note ${ noteId } ` ) ;
2019-02-02 05:48:51 +08:00
} ) ;
2019-01-19 16:57:51 +08:00
2019-02-02 19:41:20 +08:00
// there should be a unique relationship between note and its parent
2019-02-02 17:38:33 +08:00
await findAndFixIssues ( `
2019-02-02 19:41:20 +08:00
SELECT
noteId , parentNoteId
FROM
branches
WHERE
branches . isDeleted = 0
GROUP BY
branches . parentNoteId ,
branches . noteId
HAVING
COUNT ( * ) > 1 ` ,
async ( { noteId , parentNoteId } ) => {
const branches = await repository . getEntities ( ` SELECT * FROM branches WHERE noteId = ? and parentNoteId = ? and isDeleted = 1 ` , [ noteId , parentNoteId ] ) ;
// it's not necessarily "original" branch, it's just the only one which will survive
2019-10-19 18:36:16 +08:00
const origBranch = branches [ 0 ] ;
2019-02-02 19:41:20 +08:00
// delete all but the first branch
for ( const branch of branches . slice ( 1 ) ) {
2019-02-02 17:38:33 +08:00
branch . isDeleted = true ;
await branch . save ( ) ;
2019-02-02 19:41:20 +08:00
logFix ( ` Removing branch ${ branch . branchId } since it's parent-child duplicate of branch ${ origBranch . branchId } ` ) ;
}
} ) ;
2019-02-02 17:38:33 +08:00
}
2019-02-02 18:26:27 +08:00
async function findLogicIssues ( ) {
2019-02-02 05:48:51 +08:00
await findIssues ( `
2019-02-02 17:38:33 +08:00
SELECT noteId , type
FROM notes
2019-02-02 19:41:20 +08:00
WHERE
isDeleted = 0
2019-10-02 03:11:11 +08:00
AND type NOT IN ( 'text' , 'code' , 'render' , 'file' , 'image' , 'search' , 'relation-map' , 'book' ) ` ,
2019-02-02 16:26:57 +08:00
( { noteId , type } ) => ` Note ${ noteId } has invalid type= ${ type } ` ) ;
2018-01-21 10:56:03 +08:00
2019-02-02 05:48:51 +08:00
await findIssues ( `
2019-02-02 17:38:33 +08:00
SELECT noteId
FROM notes
2019-02-07 03:19:25 +08:00
JOIN note _contents USING ( noteId )
2018-11-20 06:11:36 +08:00
WHERE
isDeleted = 0
AND content IS NULL ` ,
2019-02-02 16:26:57 +08:00
( { noteId } ) => ` Note ${ noteId } content is null even though it is not deleted ` ) ;
2018-11-20 06:11:36 +08:00
2019-02-02 05:48:51 +08:00
await findIssues ( `
2019-02-02 17:38:33 +08:00
SELECT parentNoteId
2018-03-14 07:18:52 +08:00
FROM
2018-03-25 09:39:15 +08:00
branches
JOIN notes ON notes . noteId = branches . parentNoteId
2019-02-02 19:41:20 +08:00
WHERE
notes . isDeleted = 0
AND notes . type == 'search'
AND branches . isDeleted = 0 ` ,
2019-02-02 16:26:57 +08:00
( { parentNoteId } ) => ` Search note ${ parentNoteId } has children ` ) ;
2018-03-14 07:18:52 +08:00
2019-02-02 17:38:33 +08:00
await findAndFixIssues ( `
SELECT attributeId
FROM attributes
WHERE
isDeleted = 0
AND type = 'relation'
AND value = '' ` ,
2019-02-02 18:26:27 +08:00
async ( { attributeId } ) => {
const relation = await repository . getAttribute ( attributeId ) ;
relation . isDeleted = true ;
await relation . save ( ) ;
2019-02-02 17:38:33 +08:00
2019-02-02 18:26:27 +08:00
logFix ( ` Removed relation ${ relation . attributeId } of name " ${ relation . name } with empty target. ` ) ;
} ) ;
2018-11-15 20:58:14 +08:00
2019-02-02 05:48:51 +08:00
await findIssues ( `
2018-11-15 20:58:14 +08:00
SELECT
2019-02-02 05:48:51 +08:00
attributeId ,
type
2019-02-02 17:38:33 +08:00
FROM attributes
2018-11-15 20:58:14 +08:00
WHERE
2019-02-02 19:41:20 +08:00
isDeleted = 0
AND type != 'label'
2018-11-15 20:58:14 +08:00
AND type != 'label-definition'
AND type != 'relation'
AND type != 'relation-definition' ` ,
2019-02-02 16:26:57 +08:00
( { attributeId , type } ) => ` Attribute ${ attributeId } has invalid type ' ${ type } ' ` ) ;
2018-11-15 20:58:14 +08:00
2019-02-02 16:26:57 +08:00
await findAndFixIssues ( `
SELECT
attributeId ,
attributes . noteId
FROM
attributes
JOIN notes ON attributes . noteId = notes . noteId
WHERE
attributes . isDeleted = 0
AND notes . isDeleted = 1 ` ,
async ( { attributeId , noteId } ) => {
const attribute = await repository . getAttribute ( attributeId ) ;
attribute . isDeleted = true ;
await attribute . save ( ) ;
logFix ( ` Removed attribute ${ attributeId } because owning note ${ noteId } is also deleted. ` ) ;
} ) ;
2018-11-15 20:58:14 +08:00
2019-02-02 17:38:33 +08:00
await findAndFixIssues ( `
2018-11-15 20:58:14 +08:00
SELECT
2019-02-02 05:48:51 +08:00
attributeId ,
2019-02-02 17:38:33 +08:00
attributes . value AS targetNoteId
2018-11-15 20:58:14 +08:00
FROM
attributes
2019-02-02 17:38:33 +08:00
JOIN notes ON attributes . value = notes . noteId
2018-11-15 20:58:14 +08:00
WHERE
attributes . type = 'relation'
2019-02-02 17:38:33 +08:00
AND attributes . isDeleted = 0
AND notes . isDeleted = 1 ` ,
async ( { attributeId , targetNoteId } ) => {
const attribute = await repository . getAttribute ( attributeId ) ;
attribute . isDeleted = true ;
await attribute . save ( ) ;
logFix ( ` Removed attribute ${ attributeId } because target note ${ targetNoteId } is also deleted. ` ) ;
2019-02-02 18:26:27 +08:00
} ) ;
}
2019-02-02 05:48:51 +08:00
2019-02-02 18:26:27 +08:00
async function runSyncRowChecks ( entityName , key ) {
await findAndFixIssues ( `
SELECT
$ { key } as entityId
FROM
$ { entityName }
LEFT JOIN sync ON sync . entityName = '${entityName}' AND entityId = $ { key }
WHERE
sync . id IS NULL AND ` + (entityName === 'options' ? 'isSynced = 1' : '1'),
async ( { entityId } ) => {
await syncTableService . addEntitySync ( entityName , entityId ) ;
logFix ( ` Created missing sync record entityName= ${ entityName } , entityId= ${ entityId } ` ) ;
} ) ;
await findAndFixIssues ( `
SELECT
id , entityId
FROM
sync
LEFT JOIN $ { entityName } ON entityId = $ { key }
WHERE
sync . entityName = '${entityName}'
AND $ { key } IS NULL ` ,
async ( { id , entityId } ) => {
await sql . execute ( "DELETE FROM sync WHERE entityName = ? AND entityId = ?" , [ entityName , entityId ] ) ;
logFix ( ` Deleted extra sync record id= ${ id } , entityName= ${ entityName } , entityId= ${ entityId } ` ) ;
} ) ;
}
async function findSyncRowsIssues ( ) {
2019-02-02 05:48:51 +08:00
await runSyncRowChecks ( "notes" , "noteId" ) ;
2019-04-13 16:13:47 +08:00
await runSyncRowChecks ( "note_contents" , "noteId" ) ;
2019-02-02 05:48:51 +08:00
await runSyncRowChecks ( "note_revisions" , "noteRevisionId" ) ;
await runSyncRowChecks ( "branches" , "branchId" ) ;
2019-05-22 03:47:28 +08:00
await runSyncRowChecks ( "recent_notes" , "noteId" ) ;
2019-02-02 05:48:51 +08:00
await runSyncRowChecks ( "attributes" , "attributeId" ) ;
await runSyncRowChecks ( "api_tokens" , "apiTokenId" ) ;
await runSyncRowChecks ( "options" , "name" ) ;
2019-02-02 18:26:27 +08:00
}
async function runAllChecks ( ) {
2019-02-02 19:41:20 +08:00
unrecoverableConsistencyErrors = false ;
fixedIssues = false ;
2019-02-02 18:26:27 +08:00
await findBrokenReferenceIssues ( ) ;
await findExistencyIssues ( ) ;
await findLogicIssues ( ) ;
await findSyncRowsIssues ( ) ;
2019-02-02 05:48:51 +08:00
2019-02-02 19:41:20 +08:00
if ( unrecoverableConsistencyErrors ) {
2018-01-02 08:41:22 +08:00
// we run this only if basic checks passed since this assumes basic data consistency
2019-02-02 05:48:51 +08:00
await checkTreeCycles ( ) ;
2018-01-02 08:41:22 +08:00
}
2019-02-02 19:41:20 +08:00
return ! unrecoverableConsistencyErrors ;
2018-01-05 10:37:36 +08:00
}
async function runChecks ( ) {
let elapsedTimeMs ;
2018-04-02 09:27:46 +08:00
await syncMutexService . doExclusively ( async ( ) => {
2018-01-05 10:37:36 +08:00
const startTime = new Date ( ) ;
2019-02-02 19:41:20 +08:00
await runAllChecks ( ) ;
2018-01-05 10:37:36 +08:00
2019-02-10 23:36:25 +08:00
elapsedTimeMs = Date . now ( ) - startTime . getTime ( ) ;
2018-01-14 11:51:39 +08:00
} ) ;
2018-01-02 08:47:50 +08:00
2019-02-02 16:26:57 +08:00
if ( fixedIssues ) {
2019-08-27 02:21:43 +08:00
ws . refreshTree ( ) ;
2019-02-02 16:26:57 +08:00
}
2019-02-02 19:41:20 +08:00
if ( unrecoverableConsistencyErrors ) {
2019-02-02 05:48:51 +08:00
log . info ( ` Consistency checks failed (took ${ elapsedTimeMs } ms) ` ) ;
2018-01-02 08:41:22 +08:00
2019-08-27 02:21:43 +08:00
ws . sendMessageToAllClients ( { type : 'consistency-checks-failed' } ) ;
2017-12-15 12:21:03 +08:00
}
2017-12-15 12:30:38 +08:00
else {
2018-01-02 08:47:50 +08:00
log . info ( ` All consistency checks passed (took ${ elapsedTimeMs } ms) ` ) ;
2017-12-15 12:30:38 +08:00
}
2017-12-15 11:16:26 +08:00
}
2019-02-02 16:26:57 +08:00
function logFix ( message ) {
log . info ( "Consistency issue fixed: " + message ) ;
}
function logError ( message ) {
log . info ( "Consistency error: " + message ) ;
}
2018-04-03 09:25:20 +08:00
sqlInit . dbReady . then ( ( ) => {
2018-03-29 11:41:22 +08:00
setInterval ( cls . wrap ( runChecks ) , 60 * 60 * 1000 ) ;
2017-12-15 11:16:26 +08:00
2019-02-02 19:41:20 +08:00
// kickoff checks soon after startup (to not block the initial load)
2019-02-03 06:51:00 +08:00
setTimeout ( cls . wrap ( runChecks ) , 10 * 1000 ) ;
2017-12-15 11:16:26 +08:00
} ) ;
2019-02-02 05:48:51 +08:00
module . exports = { } ;