hidden subtree is now built as "predictable" to avoid duplicating it in sync

This commit is contained in:
zadam 2022-12-27 14:44:28 +01:00
parent ecc2ed7d73
commit 0758c82983
13 changed files with 169 additions and 69 deletions

View File

@ -0,0 +1,20 @@
module.exports = async () => {
const cls = require("../../src/services/cls");
const beccaLoader = require("../../src/becca/becca_loader");
const log = require("../../src/services/log");
const consistencyChecks = require("../../src/services/consistency_checks");
await cls.init(async () => {
beccaLoader.load();
try {
// precaution before running 211 which might produce unique constraint problems if the DB was not consistent
consistencyChecks.runOnDemandChecksWithoutExclusiveLock(true);
}
catch (e) {
// consistency checks might start failing in the future if there's some incompatible migration down the road
// we can optimistically assume the DB is consistent and still continue
log.error(`Consistency checks failed in migration 0210: ${e.message} ${e.stack}`);
}
});
};

View File

@ -0,0 +1,12 @@
-- case based on isDeleted is needed, otherwise 2 branches (1 deleted, 1 not) might get the same ID
UPDATE entity_changes SET entityId = (
SELECT
CASE isDeleted
WHEN 0 THEN parentNoteId || '_' || noteId
WHEN 1 THEN branchId
END
FROM branches WHERE branchId = entityId
)
WHERE entityName = 'branches';
UPDATE branches SET branchId = parentNoteId || '_' || noteId WHERE isDeleted = 0;

View File

@ -0,0 +1,21 @@
module.exports = () => {
const cls = require("../../src/services/cls");
const beccaLoader = require("../../src/becca/becca_loader");
const becca = require("../../src/becca/becca");
cls.init(() => {
beccaLoader.load();
const hidden = becca.getNote("_hidden");
for (const noteId of hidden.getSubtreeNoteIds({includeHidden: true})) {
if (noteId.startsWith("_")) { // is "named" note
const note = becca.getNote(noteId);
for (const attr of note.getOwnedAttributes()) {
attr.markAsDeleted("0212__delete_all_attributes_of_named_notes");
}
}
}
});
};

View File

@ -46,7 +46,10 @@ class AbstractEntity {
return this.utcDateModified || this.utcDateCreated; return this.utcDateModified || this.utcDateCreated;
} }
/** @protected */ /**
* @protected
* @returns {Becca}
*/
get becca() { get becca() {
if (!becca) { if (!becca) {
becca = require('../becca'); becca = require('../becca');
@ -75,7 +78,7 @@ class AbstractEntity {
/** /**
* Saves entity - executes SQL, but doesn't commit the transaction on its own * Saves entity - executes SQL, but doesn't commit the transaction on its own
* *
* @returns {AbstractEntity} * @returns {this}
*/ */
save() { save() {
const entityName = this.constructor.entityName; const entityName = this.constructor.entityName;

View File

@ -78,7 +78,7 @@ class Branch extends AbstractEntity {
childNote.parentBranches.push(this); childNote.parentBranches.push(this);
} }
if (this.branchId === 'root') { if (this.noteId === 'root') {
return; return;
} }
@ -165,8 +165,7 @@ class Branch extends AbstractEntity {
} }
} }
if (this.branchId === 'root' if (this.noteId === 'root'
|| this.noteId === 'root'
|| this.noteId === cls.getHoistedNoteId()) { || this.noteId === cls.getHoistedNoteId()) {
throw new Error("Can't delete root or hoisted branch/note"); throw new Error("Can't delete root or hoisted branch/note");
@ -209,11 +208,19 @@ class Branch extends AbstractEntity {
} }
beforeSaving() { beforeSaving() {
if (!this.noteId || !this.parentNoteId) {
throw new Error(`noteId and parentNoteId are mandatory properties for Branch`);
}
this.branchId = `${this.parentNoteId}_${this.noteId}`;
if (this.notePosition === undefined || this.notePosition === null) { if (this.notePosition === undefined || this.notePosition === null) {
let maxNotePos = 0; let maxNotePos = 0;
for (const childBranch of this.parentNote.getChildBranches()) { for (const childBranch of this.parentNote.getChildBranches()) {
if (maxNotePos < childBranch.notePosition && childBranch.noteId !== '_hidden') { if (maxNotePos < childBranch.notePosition
&& childBranch.noteId !== '_hidden' // hidden has very large notePosition to always stay last
) {
maxNotePos = childBranch.notePosition; maxNotePos = childBranch.notePosition;
} }
} }
@ -246,13 +253,20 @@ class Branch extends AbstractEntity {
} }
createClone(parentNoteId, notePosition) { createClone(parentNoteId, notePosition) {
return new Branch({ const existingBranch = this.becca.getBranchFromChildAndParent(this.noteId, parentNoteId);
noteId: this.noteId,
parentNoteId: parentNoteId, if (existingBranch) {
notePosition: notePosition, existingBranch.notePosition = notePosition;
prefix: this.prefix, return existingBranch;
isExpanded: this.isExpanded } else {
}); return new Branch({
noteId: this.noteId,
parentNoteId: parentNoteId,
notePosition: notePosition,
prefix: this.prefix,
isExpanded: this.isExpanded
});
}
} }
} }

View File

@ -945,13 +945,14 @@ class Note extends AbstractEntity {
}; };
} }
/** @returns {String[]} */ /** @returns {String[]} - includes the subtree node as well */
getSubtreeNoteIds({includeArchived = true, resolveSearch = false} = {}) { getSubtreeNoteIds({includeArchived = true, includeHidden = false, resolveSearch = false} = {}) {
return this.getSubtree({includeArchived, resolveSearch}) return this.getSubtree({includeArchived, includeHidden, resolveSearch})
.notes .notes
.map(note => note.noteId); .map(note => note.noteId);
} }
/** @deprecated use getSubtreeNoteIds() instead */
getDescendantNoteIds() { getDescendantNoteIds() {
return this.getSubtreeNoteIds(); return this.getSubtreeNoteIds();
} }
@ -1171,7 +1172,8 @@ class Note extends AbstractEntity {
* @param {string} type - attribute type (label / relation) * @param {string} type - attribute type (label / relation)
* @param {string} name - name of the attribute, not including the leading ~/# * @param {string} name - name of the attribute, not including the leading ~/#
* @param {string} [value] - value of the attribute - text for labels, target note ID for relations; optional. * @param {string} [value] - value of the attribute - text for labels, target note ID for relations; optional.
* * @param {boolean} [isInheritable=false]
* @param {int} [position]
* @return {Attribute} * @return {Attribute}
*/ */
addAttribute(type, name, value = "", isInheritable = false, position = 1000) { addAttribute(type, name, value = "", isInheritable = false, position = 1000) {
@ -1192,7 +1194,7 @@ class Note extends AbstractEntity {
* *
* @param {string} name - name of the label, not including the leading # * @param {string} name - name of the label, not including the leading #
* @param {string} [value] - text value of the label; optional * @param {string} [value] - text value of the label; optional
* * @param {boolean} [isInheritable=false]
* @return {Attribute} * @return {Attribute}
*/ */
addLabel(name, value = "", isInheritable = false) { addLabel(name, value = "", isInheritable = false) {
@ -1204,8 +1206,8 @@ class Note extends AbstractEntity {
* returned. * returned.
* *
* @param {string} name - name of the relation, not including the leading ~ * @param {string} name - name of the relation, not including the leading ~
* @param {string} value - ID of the target note of the relation * @param {string} targetNoteId
* * @param {boolean} [isInheritable=false]
* @return {Attribute} * @return {Attribute}
*/ */
addRelation(name, targetNoteId, isInheritable = false) { addRelation(name, targetNoteId, isInheritable = false) {

View File

@ -35,15 +35,14 @@ function register(router) {
existing.save(); existing.save();
return res.status(200).json(mappers.mapBranchToPojo(existing)); return res.status(200).json(mappers.mapBranchToPojo(existing));
} } else {
try {
const branch = new Branch(params).save();
try { res.status(201).json(mappers.mapBranchToPojo(branch));
const branch = new Branch(params).save(); } catch (e) {
throw new eu.EtapiError(400, eu.GENERIC_CODE, e.message);
res.status(201).json(mappers.mapBranchToPojo(branch)); }
}
catch (e) {
throw new eu.EtapiError(400, eu.GENERIC_CODE, e.message);
} }
}); });

View File

@ -172,7 +172,7 @@ function updateNoteAttributes(req) {
continue; continue;
} }
// no existing attribute has been matched so we need to create a new one // no existing attribute has been matched, so we need to create a new one
// type, name and isInheritable are immutable so even if there is an attribute with matching type & name, we need to create a new one and delete the former one // type, name and isInheritable are immutable so even if there is an attribute with matching type & name, we need to create a new one and delete the former one
note.addAttribute(incAttr.type, incAttr.name, incAttr.value, incAttr.isInheritable, position); note.addAttribute(incAttr.type, incAttr.name, incAttr.value, incAttr.isInheritable, position);

View File

@ -4,8 +4,8 @@ const build = require('./build');
const packageJson = require('../../package'); const packageJson = require('../../package');
const {TRILIUM_DATA_DIR} = require('./data_dir'); const {TRILIUM_DATA_DIR} = require('./data_dir');
const APP_DB_VERSION = 209; const APP_DB_VERSION = 212;
const SYNC_VERSION = 28; const SYNC_VERSION = 29;
const CLIPPER_PROTOCOL_VERSION = "1.0"; const CLIPPER_PROTOCOL_VERSION = "1.0";
module.exports = { module.exports = {

View File

@ -144,7 +144,7 @@ class ConsistencyChecks {
FROM branches FROM branches
LEFT JOIN notes ON notes.noteId = branches.parentNoteId LEFT JOIN notes ON notes.noteId = branches.parentNoteId
WHERE branches.isDeleted = 0 WHERE branches.isDeleted = 0
AND branches.branchId != 'root' AND branches.noteId != 'root'
AND notes.noteId IS NULL`, AND notes.noteId IS NULL`,
({branchId, parentNoteId}) => { ({branchId, parentNoteId}) => {
if (this.autoFix) { if (this.autoFix) {
@ -670,7 +670,7 @@ class ConsistencyChecks {
this.findSyncIssues(); this.findSyncIssues();
// root branch should always be expanded // root branch should always be expanded
sql.execute("UPDATE branches SET isExpanded = 1 WHERE branchId = 'root'"); sql.execute("UPDATE branches SET isExpanded = 1 WHERE noteId = 'root'");
if (!this.unrecoveredConsistencyErrors) { if (!this.unrecoveredConsistencyErrors) {
// we run this only if basic checks passed since this assumes basic data consistency // we run this only if basic checks passed since this assumes basic data consistency
@ -701,13 +701,7 @@ class ConsistencyChecks {
let elapsedTimeMs; let elapsedTimeMs;
await syncMutexService.doExclusively(() => { await syncMutexService.doExclusively(() => {
const startTimeMs = Date.now(); elapsedTimeMs = this.runChecksInner();
this.runDbDiagnostics();
this.runAllChecksAndFixers();
elapsedTimeMs = Date.now() - startTimeMs;
}); });
if (this.unrecoveredConsistencyErrors) { if (this.unrecoveredConsistencyErrors) {
@ -721,6 +715,16 @@ class ConsistencyChecks {
); );
} }
} }
runChecksInner() {
const startTimeMs = Date.now();
this.runDbDiagnostics();
this.runAllChecksAndFixers();
return Date.now() - startTimeMs;
}
} }
function getBlankContent(isProtected, type, mime) { function getBlankContent(isProtected, type, mime) {
@ -750,9 +754,14 @@ function runPeriodicChecks() {
consistencyChecks.runChecks(); consistencyChecks.runChecks();
} }
function runOnDemandChecks(autoFix) { async function runOnDemandChecks(autoFix) {
const consistencyChecks = new ConsistencyChecks(autoFix); const consistencyChecks = new ConsistencyChecks(autoFix);
consistencyChecks.runChecks(); await consistencyChecks.runChecks();
}
function runOnDemandChecksWithoutExclusiveLock(autoFix) {
const consistencyChecks = new ConsistencyChecks(autoFix);
consistencyChecks.runChecksInner();
} }
function runEntityChangesChecks() { function runEntityChangesChecks() {
@ -769,5 +778,6 @@ sqlInit.dbReady.then(() => {
module.exports = { module.exports = {
runOnDemandChecks, runOnDemandChecks,
runOnDemandChecksWithoutExclusiveLock,
runEntityChangesChecks runEntityChangesChecks
}; };

View File

@ -1,5 +1,6 @@
const becca = require("../becca/becca"); const becca = require("../becca/becca");
const noteService = require("./notes"); const noteService = require("./notes");
const Attribute = require("../becca/entities/attribute.js");
const LBTPL_ROOT = "_lbTplRoot"; const LBTPL_ROOT = "_lbTplRoot";
const LBTPL_BASE = "_lbTplBase"; const LBTPL_BASE = "_lbTplBase";
@ -10,6 +11,12 @@ const LBTPL_BUILTIN_WIDGET = "_lbTplBuiltinWidget";
const LBTPL_SPACER = "_lbTplSpacer"; const LBTPL_SPACER = "_lbTplSpacer";
const LBTPL_CUSTOM_WIDGET = "_lbTplCustomWidget"; const LBTPL_CUSTOM_WIDGET = "_lbTplCustomWidget";
/*
* Hidden subtree is generated as a "predictable structure" which means that it avoids generating random IDs to always
* produce same structure. This is needed because it is run on multiple instances in the sync cluster which might produce
* duplicate subtrees. This way, all instances will generate the same structure with same IDs.
*/
const HIDDEN_SUBTREE_DEFINITION = { const HIDDEN_SUBTREE_DEFINITION = {
id: '_hidden', id: '_hidden',
title: 'Hidden Notes', title: 'Hidden Notes',
@ -239,12 +246,6 @@ function checkHiddenSubtreeRecursively(parentNoteId, item) {
let note = becca.notes[item.id]; let note = becca.notes[item.id];
let branch = becca.branches[item.id]; let branch = becca.branches[item.id];
const attrs = [...(item.attributes || [])];
if (item.icon) {
attrs.push({ type: 'label', name: 'iconClass', value: `bx ${item.icon}` });
}
if (!note) { if (!note) {
({note, branch} = noteService.createNewNote({ ({note, branch} = noteService.createNewNote({
noteId: item.id, noteId: item.id,
@ -254,27 +255,33 @@ function checkHiddenSubtreeRecursively(parentNoteId, item) {
content: '', content: '',
ignoreForbiddenParents: true ignoreForbiddenParents: true
})); }));
}
if (item.type === 'launcher') { const attrs = [...(item.attributes || [])];
if (item.command) {
attrs.push({ type: 'relation', name: 'template', value: LBTPL_COMMAND });
attrs.push({ type: 'label', name: 'command', value: item.command });
} else if (item.builtinWidget) {
if (item.builtinWidget === 'spacer') {
attrs.push({ type: 'relation', name: 'template', value: LBTPL_SPACER });
attrs.push({ type: 'label', name: 'baseSize', value: item.baseSize });
attrs.push({ type: 'label', name: 'growthFactor', value: item.growthFactor });
} else {
attrs.push({ type: 'relation', name: 'template', value: LBTPL_BUILTIN_WIDGET });
}
attrs.push({ type: 'label', name: 'builtinWidget', value: item.builtinWidget }); if (item.icon) {
} else if (item.targetNoteId) { attrs.push({ type: 'label', name: 'iconClass', value: `bx ${item.icon}` });
attrs.push({ type: 'relation', name: 'template', value: LBTPL_NOTE_LAUNCHER }); }
attrs.push({ type: 'relation', name: 'target', value: item.targetNoteId });
if (item.type === 'launcher') {
if (item.command) {
attrs.push({ type: 'relation', name: 'template', value: LBTPL_COMMAND });
attrs.push({ type: 'label', name: 'command', value: item.command });
} else if (item.builtinWidget) {
if (item.builtinWidget === 'spacer') {
attrs.push({ type: 'relation', name: 'template', value: LBTPL_SPACER });
attrs.push({ type: 'label', name: 'baseSize', value: item.baseSize });
attrs.push({ type: 'label', name: 'growthFactor', value: item.growthFactor });
} else { } else {
throw new Error(`No action defined for launcher ${JSON.stringify(item)}`); attrs.push({ type: 'relation', name: 'template', value: LBTPL_BUILTIN_WIDGET });
} }
attrs.push({ type: 'label', name: 'builtinWidget', value: item.builtinWidget });
} else if (item.targetNoteId) {
attrs.push({ type: 'relation', name: 'template', value: LBTPL_NOTE_LAUNCHER });
attrs.push({ type: 'relation', name: 'target', value: item.targetNoteId });
} else {
throw new Error(`No action defined for launcher ${JSON.stringify(item)}`);
} }
} }
@ -299,8 +306,17 @@ function checkHiddenSubtreeRecursively(parentNoteId, item) {
} }
for (const attr of attrs) { for (const attr of attrs) {
if (!note.hasAttribute(attr.type, attr.name)) { const attrId = note.noteId + "_" + attr.type.charAt(0) + attr.name;
note.addAttribute(attr.type, attr.name, attr.value);
if (!note.getAttributes().find(attr => attr.attributeId === attrId)) {console.log("EEEEE");
new Attribute({
attributeId: attrId,
noteId: note.noteId,
type: attr.type,
name: attr.name,
value: attr.value,
isInheritable: false
}).save();
} }
} }

View File

@ -155,6 +155,9 @@ function createNewNote(params) {
cls.disableEntityEvents(); cls.disableEntityEvents();
} }
// TODO: think about what can happen if the note already exists with the forced ID
// I guess on DB it's going to be fine, but becca references between entities
// might get messed up (two Note instance for the same ID existing in the references)
note = new Note({ note = new Note({
noteId: params.noteId, // optionally can force specific noteId noteId: params.noteId, // optionally can force specific noteId
title: params.title, title: params.title,

View File

@ -51,9 +51,9 @@ function runNotesWithLabel(runAttrValue) {
} }
sqlInit.dbReady.then(() => { sqlInit.dbReady.then(() => {
if (!process.env.TRILIUM_SAFE_MODE) { cls.init(() => hiddenSubtreeService.checkHiddenSubtree());
cls.init(() => hiddenSubtreeService.checkHiddenSubtree());
if (!process.env.TRILIUM_SAFE_MODE) {
setTimeout(cls.wrap(() => runNotesWithLabel('backendStartup')), 10 * 1000); setTimeout(cls.wrap(() => runNotesWithLabel('backendStartup')), 10 * 1000);
setInterval(cls.wrap(() => runNotesWithLabel('hourly')), 3600 * 1000); setInterval(cls.wrap(() => runNotesWithLabel('hourly')), 3600 * 1000);