server-ts: Port consistency_checks

This commit is contained in:
Elian Doran 2024-04-03 20:47:41 +03:00
parent ed47c23e23
commit aa233b8adb
No known key found for this signature in database
7 changed files with 111 additions and 49 deletions

View File

@ -26,10 +26,10 @@ app.use(helmet({
crossOriginEmbedderPolicy: false crossOriginEmbedderPolicy: false
})); }));
app.use(express.text({limit: '500mb'})); app.use(express.text({ limit: '500mb' }));
app.use(express.json({limit: '500mb'})); app.use(express.json({ limit: '500mb' }));
app.use(express.raw({limit: '500mb'})); app.use(express.raw({ limit: '500mb' }));
app.use(express.urlencoded({extended: false})); app.use(express.urlencoded({ extended: false }));
app.use(cookieParser()); app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public/root'))); app.use(express.static(path.join(__dirname, 'public/root')));
app.use(`/manifest.webmanifest`, express.static(path.join(__dirname, 'public/manifest.webmanifest'))); app.use(`/manifest.webmanifest`, express.static(path.join(__dirname, 'public/manifest.webmanifest')));
@ -49,7 +49,7 @@ require('./services/sync');
require('./services/backup'); require('./services/backup');
// trigger consistency checks timer // trigger consistency checks timer
require('./services/consistency_checks.js'); require('./services/consistency_checks');
require('./services/scheduler.js'); require('./services/scheduler.js');

View File

@ -4,7 +4,7 @@ const sql = require('../../services/sql');
const log = require('../../services/log'); const log = require('../../services/log');
const backupService = require('../../services/backup'); const backupService = require('../../services/backup');
const anonymizationService = require('../../services/anonymization'); const anonymizationService = require('../../services/anonymization');
const consistencyChecksService = require('../../services/consistency_checks.js'); const consistencyChecksService = require('../../services/consistency_checks');
function getExistingBackups() { function getExistingBackups() {
return backupService.getExistingBackups(); return backupService.getExistingBackups();

View File

@ -132,7 +132,7 @@ function getChanged(req) {
const partialRequests = {}; const partialRequests = {};
function update(req) { function update(req) {
let {body} = req; let { body } = req;
const pageCount = parseInt(req.get('pageCount')); const pageCount = parseInt(req.get('pageCount'));
const pageIndex = parseInt(req.get('pageIndex')); const pageIndex = parseInt(req.get('pageIndex'));
@ -164,7 +164,7 @@ function update(req) {
} }
} }
const {entities, instanceId} = body; const { entities, instanceId } = body;
sql.transactional(() => syncUpdateService.updateEntities(entities, instanceId)); sql.transactional(() => syncUpdateService.updateEntities(entities, instanceId));
} }
@ -193,7 +193,7 @@ function queueSector(req) {
} }
function checkEntityChanges() { function checkEntityChanges() {
require('../../services/consistency_checks.js').runEntityChangesChecks(); require('../../services/consistency_checks').runEntityChangesChecks();
} }
module.exports = { module.exports = {

View File

@ -1,33 +1,42 @@
"use strict"; "use strict";
const sql = require('./sql'); import sql = require('./sql');
const sqlInit = require('./sql_init'); import sqlInit = require('./sql_init');
const log = require('./log'); import log = require('./log');
const ws = require('./ws'); import ws = require('./ws');
const syncMutexService = require('./sync_mutex'); import syncMutexService = require('./sync_mutex');
const cls = require('./cls'); import cls = require('./cls');
const entityChangesService = require('./entity_changes'); import entityChangesService = require('./entity_changes');
const optionsService = require('./options'); import optionsService = require('./options');
const BBranch = require('../becca/entities/bbranch'); import BBranch = require('../becca/entities/bbranch');
const revisionService = require('./revisions'); import revisionService = require('./revisions');
const becca = require('../becca/becca'); import becca = require('../becca/becca');
const utils = require('../services/utils'); import utils = require('../services/utils');
const eraseService = require('../services/erase'); import eraseService = require('../services/erase');
const {sanitizeAttributeName} = require('./sanitize_attribute_name'); import sanitizeAttributeName = require('./sanitize_attribute_name');
const noteTypes = require('../services/note_types').getNoteTypeNames(); import noteTypesService = require('../services/note_types');
import { BranchRow, NoteRow } from '../becca/entities/rows';
import { EntityChange, EntityRow } from './entity_changes_interface';
const noteTypes = noteTypesService.getNoteTypeNames();
class ConsistencyChecks { class ConsistencyChecks {
private autoFix: boolean;
private unrecoveredConsistencyErrors: boolean;
private fixedIssues: boolean;
private reloadNeeded: boolean;
/** /**
* @param autoFix - automatically fix all encountered problems. False is only for debugging during development (fail fast) * @param autoFix - automatically fix all encountered problems. False is only for debugging during development (fail fast)
*/ */
constructor(autoFix) { constructor(autoFix: boolean) {
this.autoFix = autoFix; this.autoFix = autoFix;
this.unrecoveredConsistencyErrors = false; this.unrecoveredConsistencyErrors = false;
this.fixedIssues = false; this.fixedIssues = false;
this.reloadNeeded = false; this.reloadNeeded = false;
} }
findAndFixIssues(query, fixerCb) { findAndFixIssues(query: string, fixerCb: (res: any) => void) {
const results = sql.getRows(query); const results = sql.getRows(query);
for (const res of results) { for (const res of results) {
@ -39,7 +48,7 @@ class ConsistencyChecks {
} else { } else {
this.unrecoveredConsistencyErrors = true; this.unrecoveredConsistencyErrors = true;
} }
} catch (e) { } catch (e: any) {
logError(`Fixer failed with ${e.message} ${e.stack}`); logError(`Fixer failed with ${e.message} ${e.stack}`);
this.unrecoveredConsistencyErrors = true; this.unrecoveredConsistencyErrors = true;
} }
@ -49,8 +58,8 @@ class ConsistencyChecks {
} }
checkTreeCycles() { checkTreeCycles() {
const childToParents = {}; const childToParents: Record<string, string[]> = {};
const rows = sql.getRows("SELECT noteId, parentNoteId FROM branches WHERE isDeleted = 0"); const rows = sql.getRows<BranchRow>("SELECT noteId, parentNoteId FROM branches WHERE isDeleted = 0");
for (const row of rows) { for (const row of rows) {
const childNoteId = row.noteId; const childNoteId = row.noteId;
@ -61,7 +70,7 @@ class ConsistencyChecks {
} }
/** @returns {boolean} true if cycle was found and we should try again */ /** @returns {boolean} true if cycle was found and we should try again */
const checkTreeCycle = (noteId, path) => { const checkTreeCycle = (noteId: string, path: string[]) => {
if (noteId === 'root') { if (noteId === 'root') {
return false; return false;
} }
@ -70,8 +79,10 @@ class ConsistencyChecks {
if (path.includes(parentNoteId)) { if (path.includes(parentNoteId)) {
if (this.autoFix) { if (this.autoFix) {
const branch = becca.getBranchFromChildAndParent(noteId, parentNoteId); const branch = becca.getBranchFromChildAndParent(noteId, parentNoteId);
branch.markAsDeleted('cycle-autofix'); if (branch) {
logFix(`Branch '${branch.branchId}' between child '${noteId}' and parent '${parentNoteId}' has been deleted since it was causing a tree cycle.`); branch.markAsDeleted('cycle-autofix');
logFix(`Branch '${branch.branchId}' between child '${noteId}' and parent '${parentNoteId}' has been deleted since it was causing a tree cycle.`);
}
return true; return true;
} }
@ -133,6 +144,9 @@ class ConsistencyChecks {
({branchId, noteId}) => { ({branchId, noteId}) => {
if (this.autoFix) { if (this.autoFix) {
const branch = becca.getBranch(branchId); const branch = becca.getBranch(branchId);
if (!branch) {
return;
}
branch.markAsDeleted(); branch.markAsDeleted();
this.reloadNeeded = true; this.reloadNeeded = true;
@ -154,12 +168,21 @@ class ConsistencyChecks {
if (this.autoFix) { if (this.autoFix) {
// Delete the old branch and recreate it with root as parent. // Delete the old branch and recreate it with root as parent.
const oldBranch = becca.getBranch(branchId); const oldBranch = becca.getBranch(branchId);
if (!oldBranch) {
return;
}
const noteId = oldBranch.noteId; const noteId = oldBranch.noteId;
oldBranch.markAsDeleted("missing-parent"); oldBranch.markAsDeleted("missing-parent");
let message = `Branch '${branchId}' was missing parent note '${parentNoteId}', so it was deleted. `; let message = `Branch '${branchId}' was missing parent note '${parentNoteId}', so it was deleted. `;
if (becca.getNote(noteId).getParentBranches().length === 0) { const note = becca.getNote(noteId);
if (!note) {
return;
}
if (note.getParentBranches().length === 0) {
const newBranch = new BBranch({ const newBranch = new BBranch({
parentNoteId: 'root', parentNoteId: 'root',
noteId: noteId, noteId: noteId,
@ -188,6 +211,9 @@ class ConsistencyChecks {
({attributeId, noteId}) => { ({attributeId, noteId}) => {
if (this.autoFix) { if (this.autoFix) {
const attribute = becca.getAttribute(attributeId); const attribute = becca.getAttribute(attributeId);
if (!attribute) {
return;
}
attribute.markAsDeleted(); attribute.markAsDeleted();
this.reloadNeeded = true; this.reloadNeeded = true;
@ -208,6 +234,9 @@ class ConsistencyChecks {
({attributeId, noteId}) => { ({attributeId, noteId}) => {
if (this.autoFix) { if (this.autoFix) {
const attribute = becca.getAttribute(attributeId); const attribute = becca.getAttribute(attributeId);
if (!attribute) {
return;
}
attribute.markAsDeleted(); attribute.markAsDeleted();
this.reloadNeeded = true; this.reloadNeeded = true;
@ -230,6 +259,9 @@ class ConsistencyChecks {
({attachmentId, ownerId}) => { ({attachmentId, ownerId}) => {
if (this.autoFix) { if (this.autoFix) {
const attachment = becca.getAttachment(attachmentId); const attachment = becca.getAttachment(attachmentId);
if (!attachment) {
return;
}
attachment.markAsDeleted(); attachment.markAsDeleted();
this.reloadNeeded = false; this.reloadNeeded = false;
@ -258,6 +290,7 @@ class ConsistencyChecks {
({branchId, noteId}) => { ({branchId, noteId}) => {
if (this.autoFix) { if (this.autoFix) {
const branch = becca.getBranch(branchId); const branch = becca.getBranch(branchId);
if (!branch) return;
branch.markAsDeleted(); branch.markAsDeleted();
this.reloadNeeded = true; this.reloadNeeded = true;
@ -278,6 +311,9 @@ class ConsistencyChecks {
`, ({branchId, parentNoteId}) => { `, ({branchId, parentNoteId}) => {
if (this.autoFix) { if (this.autoFix) {
const branch = becca.getBranch(branchId); const branch = becca.getBranch(branchId);
if (!branch) {
return;
}
branch.markAsDeleted(); branch.markAsDeleted();
this.reloadNeeded = true; this.reloadNeeded = true;
@ -321,7 +357,7 @@ class ConsistencyChecks {
HAVING COUNT(1) > 1`, HAVING COUNT(1) > 1`,
({noteId, parentNoteId}) => { ({noteId, parentNoteId}) => {
if (this.autoFix) { if (this.autoFix) {
const branchIds = sql.getColumn( const branchIds = sql.getColumn<string>(
`SELECT branchId `SELECT branchId
FROM branches FROM branches
WHERE noteId = ? WHERE noteId = ?
@ -333,9 +369,17 @@ class ConsistencyChecks {
// it's not necessarily "original" branch, it's just the only one which will survive // it's not necessarily "original" branch, it's just the only one which will survive
const origBranch = branches[0]; const origBranch = branches[0];
if (!origBranch) {
logError(`Unable to find original branch.`);
return;
}
// delete all but the first branch // delete all but the first branch
for (const branch of branches.slice(1)) { for (const branch of branches.slice(1)) {
if (!branch) {
continue;
}
branch.markAsDeleted(); branch.markAsDeleted();
logFix(`Removing branch '${branch.branchId}' since it's a parent-child duplicate of branch '${origBranch.branchId}'`); logFix(`Removing branch '${branch.branchId}' since it's a parent-child duplicate of branch '${origBranch.branchId}'`);
@ -357,6 +401,7 @@ class ConsistencyChecks {
({attachmentId, noteId}) => { ({attachmentId, noteId}) => {
if (this.autoFix) { if (this.autoFix) {
const attachment = becca.getAttachment(attachmentId); const attachment = becca.getAttachment(attachmentId);
if (!attachment) return;
attachment.markAsDeleted(); attachment.markAsDeleted();
this.reloadNeeded = false; this.reloadNeeded = false;
@ -379,6 +424,7 @@ class ConsistencyChecks {
({noteId, type}) => { ({noteId, type}) => {
if (this.autoFix) { if (this.autoFix) {
const note = becca.getNote(noteId); const note = becca.getNote(noteId);
if (!note) return;
note.type = 'file'; // file is a safe option to recover notes if the type is not known note.type = 'file'; // file is a safe option to recover notes if the type is not known
note.save(); note.save();
@ -404,6 +450,10 @@ class ConsistencyChecks {
const fakeDate = "2000-01-01 00:00:00Z"; const fakeDate = "2000-01-01 00:00:00Z";
const blankContent = getBlankContent(isProtected, type, mime); const blankContent = getBlankContent(isProtected, type, mime);
if (!blankContent) {
logError(`Unable to recover note ${noteId} since it's content could not be retrieved (might be protected note).`);
return;
}
const blobId = utils.hashedBlobId(blankContent); const blobId = utils.hashedBlobId(blankContent);
const blobAlreadyExists = !!sql.getValue("SELECT 1 FROM blobs WHERE blobId = ?", [blobId]); const blobAlreadyExists = !!sql.getValue("SELECT 1 FROM blobs WHERE blobId = ?", [blobId]);
@ -452,7 +502,11 @@ class ConsistencyChecks {
if (this.autoFix) { if (this.autoFix) {
const note = becca.getNote(noteId); const note = becca.getNote(noteId);
const blankContent = getBlankContent(false, type, mime); const blankContent = getBlankContent(false, type, mime);
note.setContent(blankContent); if (!note) return;
if (blankContent) {
note.setContent(blankContent);
}
this.reloadNeeded = true; this.reloadNeeded = true;
@ -506,7 +560,7 @@ class ConsistencyChecks {
AND branches.isDeleted = 0`, AND branches.isDeleted = 0`,
({parentNoteId}) => { ({parentNoteId}) => {
if (this.autoFix) { if (this.autoFix) {
const branchIds = sql.getColumn(` const branchIds = sql.getColumn<string>(`
SELECT branchId SELECT branchId
FROM branches FROM branches
WHERE isDeleted = 0 WHERE isDeleted = 0
@ -515,6 +569,8 @@ class ConsistencyChecks {
const branches = branchIds.map(branchId => becca.getBranch(branchId)); const branches = branchIds.map(branchId => becca.getBranch(branchId));
for (const branch of branches) { for (const branch of branches) {
if (!branch) continue;
// delete the old wrong branch // delete the old wrong branch
branch.markAsDeleted("parent-is-search"); branch.markAsDeleted("parent-is-search");
@ -543,6 +599,7 @@ class ConsistencyChecks {
({attributeId}) => { ({attributeId}) => {
if (this.autoFix) { if (this.autoFix) {
const relation = becca.getAttribute(attributeId); const relation = becca.getAttribute(attributeId);
if (!relation) return;
relation.markAsDeleted(); relation.markAsDeleted();
this.reloadNeeded = true; this.reloadNeeded = true;
@ -563,6 +620,7 @@ class ConsistencyChecks {
({attributeId, type}) => { ({attributeId, type}) => {
if (this.autoFix) { if (this.autoFix) {
const attribute = becca.getAttribute(attributeId); const attribute = becca.getAttribute(attributeId);
if (!attribute) return;
attribute.type = 'label'; attribute.type = 'label';
attribute.save(); attribute.save();
@ -584,6 +642,7 @@ class ConsistencyChecks {
({attributeId, noteId}) => { ({attributeId, noteId}) => {
if (this.autoFix) { if (this.autoFix) {
const attribute = becca.getAttribute(attributeId); const attribute = becca.getAttribute(attributeId);
if (!attribute) return;
attribute.markAsDeleted(); attribute.markAsDeleted();
this.reloadNeeded = true; this.reloadNeeded = true;
@ -605,6 +664,7 @@ class ConsistencyChecks {
({attributeId, targetNoteId}) => { ({attributeId, targetNoteId}) => {
if (this.autoFix) { if (this.autoFix) {
const attribute = becca.getAttribute(attributeId); const attribute = becca.getAttribute(attributeId);
if (!attribute) return;
attribute.markAsDeleted(); attribute.markAsDeleted();
this.reloadNeeded = true; this.reloadNeeded = true;
@ -616,14 +676,14 @@ class ConsistencyChecks {
}); });
} }
runEntityChangeChecks(entityName, key) { runEntityChangeChecks(entityName: string, key: string) {
this.findAndFixIssues(` this.findAndFixIssues(`
SELECT ${key} as entityId SELECT ${key} as entityId
FROM ${entityName} FROM ${entityName}
LEFT JOIN entity_changes ec ON ec.entityName = '${entityName}' AND ec.entityId = ${entityName}.${key} LEFT JOIN entity_changes ec ON ec.entityName = '${entityName}' AND ec.entityId = ${entityName}.${key}
WHERE ec.id IS NULL`, WHERE ec.id IS NULL`,
({entityId}) => { ({entityId}) => {
const entityRow = sql.getRow(`SELECT * FROM ${entityName} WHERE ${key} = ?`, [entityId]); const entityRow = sql.getRow<EntityChange>(`SELECT * FROM ${entityName} WHERE ${key} = ?`, [entityId]);
if (this.autoFix) { if (this.autoFix) {
entityChangesService.putEntityChange({ entityChangesService.putEntityChange({
@ -691,10 +751,10 @@ class ConsistencyChecks {
} }
findWronglyNamedAttributes() { findWronglyNamedAttributes() {
const attrNames = sql.getColumn(`SELECT DISTINCT name FROM attributes`); const attrNames = sql.getColumn<string>(`SELECT DISTINCT name FROM attributes`);
for (const origName of attrNames) { for (const origName of attrNames) {
const fixedName = sanitizeAttributeName(origName); const fixedName = sanitizeAttributeName.sanitizeAttributeName(origName);
if (fixedName !== origName) { if (fixedName !== origName) {
if (this.autoFix) { if (this.autoFix) {
@ -721,7 +781,7 @@ class ConsistencyChecks {
findSyncIssues() { findSyncIssues() {
const lastSyncedPush = parseInt(sql.getValue("SELECT value FROM options WHERE name = 'lastSyncedPush'")); const lastSyncedPush = parseInt(sql.getValue("SELECT value FROM options WHERE name = 'lastSyncedPush'"));
const maxEntityChangeId = sql.getValue("SELECT MAX(id) FROM entity_changes"); const maxEntityChangeId = sql.getValue<number>("SELECT MAX(id) FROM entity_changes");
if (lastSyncedPush > maxEntityChangeId) { if (lastSyncedPush > maxEntityChangeId) {
if (this.autoFix) { if (this.autoFix) {
@ -773,8 +833,8 @@ class ConsistencyChecks {
} }
runDbDiagnostics() { runDbDiagnostics() {
function getTableRowCount(tableName) { function getTableRowCount(tableName: string) {
const count = sql.getValue(`SELECT COUNT(1) FROM ${tableName}`); const count = sql.getValue<number>(`SELECT COUNT(1) FROM ${tableName}`);
return `${tableName}: ${count}`; return `${tableName}: ${count}`;
} }
@ -810,7 +870,7 @@ class ConsistencyChecks {
} }
} }
function getBlankContent(isProtected, type, mime) { function getBlankContent(isProtected: boolean, type: string, mime: string) {
if (isProtected) { if (isProtected) {
return null; // this is wrong for protected non-erased notes, but we cannot create a valid value without a password return null; // this is wrong for protected non-erased notes, but we cannot create a valid value without a password
} }
@ -822,11 +882,11 @@ function getBlankContent(isProtected, type, mime) {
return ''; // empty string might be a wrong choice for some note types, but it's the best guess return ''; // empty string might be a wrong choice for some note types, but it's the best guess
} }
function logFix(message) { function logFix(message: string) {
log.info(`Consistency issue fixed: ${message}`); log.info(`Consistency issue fixed: ${message}`);
} }
function logError(message) { function logError(message: string) {
log.info(`Consistency error: ${message}`); log.info(`Consistency error: ${message}`);
} }
@ -837,7 +897,7 @@ function runPeriodicChecks() {
consistencyChecks.runChecks(); consistencyChecks.runChecks();
} }
async function runOnDemandChecks(autoFix) { async function runOnDemandChecks(autoFix: boolean) {
const consistencyChecks = new ConsistencyChecks(autoFix); const consistencyChecks = new ConsistencyChecks(autoFix);
await consistencyChecks.runChecks(); await consistencyChecks.runChecks();
} }

View File

@ -7,6 +7,8 @@ export interface EntityChange {
positions?: Record<string, number>; positions?: Record<string, number>;
hash: string; hash: string;
utcDateChanged?: string; utcDateChanged?: string;
utcDateModified?: string;
utcDateCreated?: string;
isSynced: boolean | 1 | 0; isSynced: boolean | 1 | 0;
isErased: boolean | 1 | 0; isErased: boolean | 1 | 0;
componentId?: string | null; componentId?: string | null;

View File

@ -179,7 +179,7 @@ dbReady.then(() => {
}); });
function getDbSize() { function getDbSize() {
return sql.getValue("SELECT page_count * page_size / 1000 as size FROM pragma_page_count(), pragma_page_size()"); return sql.getValue<number>("SELECT page_count * page_size / 1000 as size FROM pragma_page_count(), pragma_page_size()");
} }
log.info(`DB size: ${getDbSize()} KB`); log.info(`DB size: ${getDbSize()} KB`);

View File

@ -282,7 +282,7 @@ async function checkContentHash(syncContext: SyncContext) {
if (failedChecks.length > 0) { if (failedChecks.length > 0) {
// before re-queuing sectors, make sure the entity changes are correct // before re-queuing sectors, make sure the entity changes are correct
const consistencyChecks = require('./consistency_checks.js'); const consistencyChecks = require('./consistency_checks');
consistencyChecks.runEntityChangesChecks(); consistencyChecks.runEntityChangesChecks();
await syncRequest(syncContext, 'POST', `/api/sync/check-entity-changes`); await syncRequest(syncContext, 'POST', `/api/sync/check-entity-changes`);