refactored "sync" table to "entity_changes" - more changes

This commit is contained in:
zadam 2020-08-02 23:43:39 +02:00
parent 864271d5ef
commit 7900622f38
13 changed files with 93 additions and 92 deletions

View File

@ -66,7 +66,7 @@ class Note extends Entity {
* part of Note entity with it's own sync. Reasons behind this hybrid design has been:
*
* - content can be quite large and it's not necessary to load it / fill memory for any note access even if we don't need a content, especially for bulk operations like search
* - changes in the note metadata or title should not trigger note content sync (so we keep separate utcDateModified and sync rows)
* - changes in the note metadata or title should not trigger note content sync (so we keep separate utcDateModified and entity changes records)
* - but to the user note content and title changes are one and the same - single dateModified (so all changes must go through Note and content is not a separate entity)
*/
@ -154,7 +154,7 @@ class Note extends Entity {
sql.upsert("note_contents", "noteId", pojo);
entityChangesService.addNoteContentSync(this.noteId);
entityChangesService.addNoteContentEntityChange(this.noteId);
}
setJsonContent(content) {

View File

@ -126,7 +126,7 @@ class NoteRevision extends Entity {
sql.upsert("note_revision_contents", "noteRevisionId", pojo);
entityChangesService.addNoteRevisionContentSync(this.noteRevisionId);
entityChangesService.addNoteRevisionContentEntityChange(this.noteRevisionId);
}
beforeSaving() {

View File

@ -8,7 +8,7 @@ const TPL = `
<br/>
<br/>
<button id="fill-sync-rows-button" class="btn">Fill sync rows</button>
<button id="fill-entity-changes-button" class="btn">Fill entity changes records</button>
<br/>
<br/>
@ -41,7 +41,7 @@ export default class AdvancedOptions {
$("#options-advanced").html(TPL);
this.$forceFullSyncButton = $("#force-full-sync-button");
this.$fillEntityChangesButton = $("#fill-sync-rows-button");
this.$fillEntityChangesButton = $("#fill-entity-changes-button");
this.$anonymizeButton = $("#anonymize-button");
this.$backupDatabaseButton = $("#backup-database-button");
this.$vacuumDatabaseButton = $("#vacuum-database-button");
@ -54,7 +54,7 @@ export default class AdvancedOptions {
});
this.$fillEntityChangesButton.on('click', async () => {
await server.post('sync/fill-sync-rows');
await server.post('sync/fill-entity-changes');
toastService.showMessage("Sync rows filled successfully");
});

View File

@ -66,7 +66,7 @@ function moveBranchBeforeNote(req) {
sql.execute("UPDATE branches SET notePosition = notePosition + 10 WHERE parentNoteId = ? AND notePosition >= ? AND isDeleted = 0",
[beforeNote.parentNoteId, beforeNote.notePosition]);
entityChangesService.addNoteReorderingSync(beforeNote.parentNoteId);
entityChangesService.addNoteReorderingEntityChange(beforeNote.parentNoteId);
if (branchToMove.parentNoteId === beforeNote.parentNoteId) {
branchToMove.notePosition = beforeNote.notePosition;
@ -100,7 +100,7 @@ function moveBranchAfterNote(req) {
sql.execute("UPDATE branches SET notePosition = notePosition + 10 WHERE parentNoteId = ? AND notePosition > ? AND isDeleted = 0",
[afterNote.parentNoteId, afterNote.notePosition]);
entityChangesService.addNoteReorderingSync(afterNote.parentNoteId);
entityChangesService.addNoteReorderingEntityChange(afterNote.parentNoteId);
const movedNotePosition = afterNote.notePosition + 10;

View File

@ -82,32 +82,32 @@ function forceNoteSync(req) {
const now = dateUtils.utcNowDateTime();
sql.execute(`UPDATE notes SET utcDateModified = ? WHERE noteId = ?`, [now, noteId]);
entityChangesService.addNoteSync(noteId);
entityChangesService.addNoteEntityChange(noteId);
sql.execute(`UPDATE note_contents SET utcDateModified = ? WHERE noteId = ?`, [now, noteId]);
entityChangesService.addNoteContentSync(noteId);
entityChangesService.addNoteContentEntityChange(noteId);
for (const branchId of sql.getColumn("SELECT branchId FROM branches WHERE noteId = ?", [noteId])) {
sql.execute(`UPDATE branches SET utcDateModified = ? WHERE branchId = ?`, [now, branchId]);
entityChangesService.addBranchSync(branchId);
entityChangesService.addBranchEntityChange(branchId);
}
for (const attributeId of sql.getColumn("SELECT attributeId FROM attributes WHERE noteId = ?", [noteId])) {
sql.execute(`UPDATE attributes SET utcDateModified = ? WHERE attributeId = ?`, [now, attributeId]);
entityChangesService.addAttributeSync(attributeId);
entityChangesService.addAttributeEntityChange(attributeId);
}
for (const noteRevisionId of sql.getColumn("SELECT noteRevisionId FROM note_revisions WHERE noteId = ?", [noteId])) {
sql.execute(`UPDATE note_revisions SET utcDateModified = ? WHERE noteRevisionId = ?`, [now, noteRevisionId]);
entityChangesService.addNoteRevisionSync(noteRevisionId);
entityChangesService.addNoteRevisionEntityChange(noteRevisionId);
sql.execute(`UPDATE note_revision_contents SET utcDateModified = ? WHERE noteRevisionId = ?`, [now, noteRevisionId]);
entityChangesService.addNoteRevisionContentSync(noteRevisionId);
entityChangesService.addNoteRevisionContentEntityChange(noteRevisionId);
}
entityChangesService.addRecentNoteSync(noteId);
entityChangesService.addRecentNoteEntityChange(noteId);
log.info("Forcing note sync for " + noteId);
@ -123,12 +123,12 @@ function getChanged(req) {
const entityChanges = sql.getRows("SELECT * FROM entity_changes WHERE isSynced = 1 AND id > ? LIMIT 1000", [lastEntityChangeId]);
const ret = {
syncs: syncService.getEntityChangesRecords(entityChanges),
entityChanges: syncService.getEntityChangesRecords(entityChanges),
maxEntityChangeId: sql.getValue('SELECT COALESCE(MAX(id), 0) FROM entity_changes WHERE isSynced = 1')
};
if (ret.syncs.length > 0) {
log.info(`Returning ${ret.syncs.length} entity changes in ${Date.now() - startTime}ms`);
if (ret.entityChanges.length > 0) {
log.info(`Returning ${ret.entityChanges.length} entity changes in ${Date.now() - startTime}ms`);
}
return ret;

View File

@ -205,7 +205,7 @@ function register(app) {
apiRoute(POST, '/api/sync/test', syncApiRoute.testSync);
apiRoute(POST, '/api/sync/now', syncApiRoute.syncNow);
apiRoute(POST, '/api/sync/fill-sync-rows', syncApiRoute.fillEntityChanges);
apiRoute(POST, '/api/sync/fill-entity-changes', syncApiRoute.fillEntityChanges);
apiRoute(POST, '/api/sync/force-full-sync', syncApiRoute.forceFullSync);
apiRoute(POST, '/api/sync/force-note-sync/:noteId', syncApiRoute.forceNoteSync);
route(GET, '/api/sync/check', [auth.checkApiAuth], syncApiRoute.checkSync, apiResultHandler);

View File

@ -1,7 +1,7 @@
"use strict";
const sql = require('./sql');
const syncTable = require('./entity_changes.js');
const eventChangesService = require('./entity_changes.js');
const treeService = require('./tree');
const noteService = require('./notes');
const repository = require('./repository');
@ -90,7 +90,7 @@ function cloneNoteAfter(noteId, afterBranchId) {
sql.execute("UPDATE branches SET notePosition = notePosition + 10 WHERE parentNoteId = ? AND notePosition > ? AND isDeleted = 0",
[afterNote.parentNoteId, afterNote.notePosition]);
syncTable.addNoteReorderingSync(afterNote.parentNoteId);
eventChangesService.addNoteReorderingEntityChange(afterNote.parentNoteId);
const branch = new Branch({
noteId: noteId,

View File

@ -300,7 +300,7 @@ class ConsistencyChecks {
utcDateModified: dateUtils.utcNowDateTime()
});
entityChangesService.addNoteContentSync(noteId);
entityChangesService.addNoteContentEntityChange(noteId);
}
else {
// empty string might be wrong choice for some note types but it's a best guess
@ -343,14 +343,14 @@ class ConsistencyChecks {
// we always fix this issue because there does not seem to be a good way to prevent it.
// Scenario in which this can happen:
// 1. user on instance A deletes the note (sync for notes is created, but not for note_contents) and is later erased
// 2. instance B gets synced from instance A, note is updated because of sync row for notes,
// but note_contents is not because erasion does not create sync rows
// 2. instance B gets synced from instance A, note is updated because of entity change for notes,
// but note_contents is not because erasion does not create entity change rows
// 3. therefore note.isErased = true, but note_contents.content remains not updated and not erased.
//
// Considered solutions:
// - don't sync erased notes - this might prevent syncing also of the isDeleted flag and note would continue
// to exist on the other instance
// - create sync rows for erased event - this would be a problem for undeletion since erasion might happen
// - create entity changes for erased event - this would be a problem for undeletion since erasion might happen
// on one instance after undelete and thus would win even though there's no user action behind it
//
// So instead we just fix such cases afterwards here.
@ -555,22 +555,23 @@ class ConsistencyChecks {
});
}
runSyncRowChecks(entityName, key) {
runEntityChangeChecks(entityName, key) {
this.findAndFixIssues(`
SELECT
${key} as entityId
FROM
${entityName}
LEFT JOIN sync ON sync.entityName = '${entityName}' AND entityId = ${key}
LEFT JOIN entity_changes ON entity_changes.entityName = '${entityName}'
AND entity_changes.entityId = ${key}
WHERE
sync.id IS NULL AND ` + (entityName === 'options' ? 'options.isSynced = 1' : '1'),
entity_changes.id IS NULL AND ` + (entityName === 'options' ? 'options.isSynced = 1' : '1'),
({entityId}) => {
if (this.autoFix) {
entityChangesService.addEntityChange(entityName, entityId);
logFix(`Created missing sync record for entityName=${entityName}, entityId=${entityId}`);
logFix(`Created missing entity change for entityName=${entityName}, entityId=${entityId}`);
} else {
logError(`Missing sync record for entityName=${entityName}, entityId=${entityId}`);
logError(`Missing entity change for entityName=${entityName}, entityId=${entityId}`);
}
});
@ -578,31 +579,31 @@ class ConsistencyChecks {
SELECT
id, entityId
FROM
sync
entity_changes
LEFT JOIN ${entityName} ON entityId = ${key}
WHERE
sync.entityName = '${entityName}'
entity_changes.entityName = '${entityName}'
AND ${key} IS NULL`,
({id, entityId}) => {
if (this.autoFix) {
sql.execute("DELETE FROM entity_changes WHERE entityName = ? AND entityId = ?", [entityName, entityId]);
logFix(`Deleted extra sync record id=${id}, entityName=${entityName}, entityId=${entityId}`);
logFix(`Deleted extra entity change id=${id}, entityName=${entityName}, entityId=${entityId}`);
} else {
logError(`Unrecognized sync record id=${id}, entityName=${entityName}, entityId=${entityId}`);
logError(`Unrecognized entity change id=${id}, entityName=${entityName}, entityId=${entityId}`);
}
});
}
findSyncRowsIssues() {
this.runSyncRowChecks("notes", "noteId");
this.runSyncRowChecks("note_contents", "noteId");
this.runSyncRowChecks("note_revisions", "noteRevisionId");
this.runSyncRowChecks("branches", "branchId");
this.runSyncRowChecks("recent_notes", "noteId");
this.runSyncRowChecks("attributes", "attributeId");
this.runSyncRowChecks("api_tokens", "apiTokenId");
this.runSyncRowChecks("options", "name");
findEntityChangeIssues() {
this.runEntityChangeChecks("notes", "noteId");
this.runEntityChangeChecks("note_contents", "noteId");
this.runEntityChangeChecks("note_revisions", "noteRevisionId");
this.runEntityChangeChecks("branches", "branchId");
this.runEntityChangeChecks("recent_notes", "noteId");
this.runEntityChangeChecks("attributes", "attributeId");
this.runEntityChangeChecks("api_tokens", "apiTokenId");
this.runEntityChangeChecks("options", "name");
}
findWronglyNamedAttributes() {
@ -653,7 +654,7 @@ class ConsistencyChecks {
this.findLogicIssues();
this.findSyncRowsIssues();
this.findEntityChangeIssues();
this.findWronglyNamedAttributes();

View File

@ -7,7 +7,7 @@ const cls = require('./cls');
let maxEntityChangeId = 0;
function insertEntityChange(entityName, entityId, sourceId = null, isSynced = true) {
const sync = {
const entityChange = {
entityName: entityName,
entityId: entityId,
utcSyncDate: dateUtils.utcNowDateTime(),
@ -15,11 +15,11 @@ function insertEntityChange(entityName, entityId, sourceId = null, isSynced = tr
isSynced: isSynced ? 1 : 0
};
sync.id = sql.replace("sync", sync);
entityChange.id = sql.replace("entity_changes", entityChange);
maxEntityChangeId = Math.max(maxEntityChangeId, sync.id);
maxEntityChangeId = Math.max(maxEntityChangeId, entityChange.id);
return sync;
return entityChange;
}
function addEntityChange(entityName, entityId, sourceId, isSynced) {
@ -85,13 +85,13 @@ function fillEntityChanges(entityName, entityPrimaryKey, condition = '') {
}
if (createdCount > 0) {
log.info(`Created ${createdCount} missing sync records for ${entityName}.`);
log.info(`Created ${createdCount} missing entity changes for ${entityName}.`);
}
}
catch (e) {
// this is to fix migration from 0.30 to 0.32, can be removed later
// see https://github.com/zadam/trilium/issues/557
log.error(`Filling sync rows failed for ${entityName} ${entityPrimaryKey} with error "${e.message}", continuing`);
log.error(`Filling entity changes failed for ${entityName} ${entityPrimaryKey} with error "${e.message}", continuing`);
}
}
@ -112,16 +112,16 @@ function fillAllEntityChanges() {
}
module.exports = {
addNoteSync: (noteId, sourceId) => addEntityChange("notes", noteId, sourceId),
addNoteContentSync: (noteId, sourceId) => addEntityChange("note_contents", noteId, sourceId),
addBranchSync: (branchId, sourceId) => addEntityChange("branches", branchId, sourceId),
addNoteReorderingSync: (parentNoteId, sourceId) => addEntityChange("note_reordering", parentNoteId, sourceId),
addNoteRevisionSync: (noteRevisionId, sourceId) => addEntityChange("note_revisions", noteRevisionId, sourceId),
addNoteRevisionContentSync: (noteRevisionId, sourceId) => addEntityChange("note_revision_contents", noteRevisionId, sourceId),
addOptionsSync: (name, sourceId, isSynced) => addEntityChange("options", name, sourceId, isSynced),
addRecentNoteSync: (noteId, sourceId) => addEntityChange("recent_notes", noteId, sourceId),
addAttributeSync: (attributeId, sourceId) => addEntityChange("attributes", attributeId, sourceId),
addApiTokenSync: (apiTokenId, sourceId) => addEntityChange("api_tokens", apiTokenId, sourceId),
addNoteEntityChange: (noteId, sourceId) => addEntityChange("notes", noteId, sourceId),
addNoteContentEntityChange: (noteId, sourceId) => addEntityChange("note_contents", noteId, sourceId),
addBranchEntityChange: (branchId, sourceId) => addEntityChange("branches", branchId, sourceId),
addNoteReorderingEntityChange: (parentNoteId, sourceId) => addEntityChange("note_reordering", parentNoteId, sourceId),
addNoteRevisionEntityChange: (noteRevisionId, sourceId) => addEntityChange("note_revisions", noteRevisionId, sourceId),
addNoteRevisionContentEntityChange: (noteRevisionId, sourceId) => addEntityChange("note_revision_contents", noteRevisionId, sourceId),
addOptionEntityChange: (name, sourceId, isSynced) => addEntityChange("options", name, sourceId, isSynced),
addRecentNoteEntityChange: (noteId, sourceId) => addEntityChange("recent_notes", noteId, sourceId),
addAttributeEntityChange: (attributeId, sourceId) => addEntityChange("attributes", attributeId, sourceId),
addApiTokenEntityChange: (apiTokenId, sourceId) => addEntityChange("api_tokens", apiTokenId, sourceId),
addEntityChange,
fillAllEntityChanges,
addEntityChangesForSector,

View File

@ -159,7 +159,7 @@ function createNewNoteWithTarget(target, targetBranchId, params) {
const retObject = createNewNote(params);
entityChangesService.addNoteReorderingSync(params.parentNoteId);
entityChangesService.addNoteReorderingEntityChange(params.parentNoteId);
return retObject;
}

View File

@ -141,31 +141,31 @@ async function pullSync(syncContext) {
stats.outstandingPulls = 0;
}
const rows = resp.syncs;
const {entityChanges} = resp;
if (rows.length === 0) {
if (entityChanges.length === 0) {
break;
}
sql.transactional(() => {
for (const {sync, entity} of rows) {
if (!sourceIdService.isLocalSourceId(sync.sourceId)) {
if (!atLeastOnePullApplied && sync.entity !== 'recent_notes') { // send only for first
for (const {entityChange, entity} of entityChanges) {
if (!sourceIdService.isLocalSourceId(entityChange.sourceId)) {
if (!atLeastOnePullApplied && entityChange.entity !== 'recent_notes') { // send only for first
ws.syncPullInProgress();
atLeastOnePullApplied = true;
}
syncUpdateService.updateEntity(sync, entity, syncContext.sourceId);
syncUpdateService.updateEntity(entityChange, entity, syncContext.sourceId);
}
stats.outstandingPulls = resp.maxEntityChangeId - sync.id;
stats.outstandingPulls = resp.maxEntityChangeId - entityChange.id;
}
setLastSyncedPull(rows[rows.length - 1].sync.id);
setLastSyncedPull(entityChanges[entityChanges.length - 1].entityChange.id);
});
log.info(`Pulled ${rows.length} changes starting at entityChangeId=${lastSyncedPull} in ${pulledDate - startDate}ms and applied them in ${Date.now() - pulledDate}ms, ${stats.outstandingPulls} outstanding pulls`);
log.info(`Pulled ${entityChanges.length} changes starting at entityChangeId=${lastSyncedPull} in ${pulledDate - startDate}ms and applied them in ${Date.now() - pulledDate}ms, ${stats.outstandingPulls} outstanding pulls`);
}
if (atLeastOnePullApplied) {
@ -179,20 +179,20 @@ async function pushSync(syncContext) {
let lastSyncedPush = getLastSyncedPush();
while (true) {
const syncs = sql.getRows('SELECT * FROM entity_changes WHERE isSynced = 1 AND id > ? LIMIT 1000', [lastSyncedPush]);
const entityChanges = sql.getRows('SELECT * FROM entity_changes WHERE isSynced = 1 AND id > ? LIMIT 1000', [lastSyncedPush]);
if (syncs.length === 0) {
if (entityChanges.length === 0) {
log.info("Nothing to push");
break;
}
const filteredSyncs = syncs.filter(sync => {
if (sync.sourceId === syncContext.sourceId) {
const filteredEntityChanges = entityChanges.filter(entityChange => {
if (entityChange.sourceId === syncContext.sourceId) {
// this may set lastSyncedPush beyond what's actually sent (because of size limit)
// so this is applied to the database only if there's no actual update
// TODO: it would be better to simplify this somehow
lastSyncedPush = sync.id;
lastSyncedPush = entityChange.id;
return false;
}
@ -201,25 +201,25 @@ async function pushSync(syncContext) {
}
});
if (filteredSyncs.length === 0) {
// there still might be more syncs (because of batch limit), just all from current batch
if (filteredEntityChanges.length === 0) {
// there still might be more sync changes (because of batch limit), just all from current batch
// has been filtered out
setLastSyncedPush(lastSyncedPush);
continue;
}
const syncRecords = getEntityChangesRecords(filteredSyncs);
const entityChangesRecords = getEntityChangesRecords(filteredEntityChanges);
const startDate = new Date();
await syncRequest(syncContext, 'PUT', '/api/sync/update', {
sourceId: sourceIdService.getCurrentSourceId(),
entities: syncRecords
entities: entityChangesRecords
});
log.info(`Pushing ${syncRecords.length} syncs in ` + (Date.now() - startDate.getTime()) + "ms");
log.info(`Pushing ${entityChangesRecords.length} sync changes in ` + (Date.now() - startDate.getTime()) + "ms");
lastSyncedPush = syncRecords[syncRecords.length - 1].sync.id;
lastSyncedPush = entityChangesRecords[entityChangesRecords.length - 1].entityChange.id;
setLastSyncedPush(lastSyncedPush);
}

View File

@ -86,7 +86,7 @@ function updateNote(remoteEntity, sourceId) {
sql.transactional(() => {
sql.replace("notes", remoteEntity);
entityChangesService.addNoteSync(remoteEntity.noteId, sourceId);
entityChangesService.addNoteEntityChange(remoteEntity.noteId, sourceId);
});
return true;
@ -104,7 +104,7 @@ function updateNoteContent(remoteEntity, sourceId) {
sql.transactional(() => {
sql.replace("note_contents", remoteEntity);
entityChangesService.addNoteContentSync(remoteEntity.noteId, sourceId);
entityChangesService.addNoteContentEntityChange(remoteEntity.noteId, sourceId);
});
return true;
@ -126,7 +126,7 @@ function updateBranch(remoteEntity, sourceId) {
sql.replace('branches', remoteEntity);
entityChangesService.addBranchSync(remoteEntity.branchId, sourceId);
entityChangesService.addBranchEntityChange(remoteEntity.branchId, sourceId);
});
return true;
@ -142,7 +142,7 @@ function updateNoteRevision(remoteEntity, sourceId) {
if (shouldWeUpdateEntity(localEntity, remoteEntity)) {
sql.replace('note_revisions', remoteEntity);
entityChangesService.addNoteRevisionSync(remoteEntity.noteRevisionId, sourceId);
entityChangesService.addNoteRevisionEntityChange(remoteEntity.noteRevisionId, sourceId);
log.info("Update/sync note revision " + remoteEntity.noteRevisionId);
}
@ -158,7 +158,7 @@ function updateNoteRevisionContent(remoteEntity, sourceId) {
sql.replace('note_revision_contents', remoteEntity);
entityChangesService.addNoteRevisionContentSync(remoteEntity.noteRevisionId, sourceId);
entityChangesService.addNoteRevisionContentEntityChange(remoteEntity.noteRevisionId, sourceId);
});
return true;
@ -173,7 +173,7 @@ function updateNoteReordering(entityId, remote, sourceId) {
sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?", [remote[key], key]);
}
entityChangesService.addNoteReorderingSync(entityId, sourceId);
entityChangesService.addNoteReorderingEntityChange(entityId, sourceId);
});
return true;
@ -190,7 +190,7 @@ function updateOptions(remoteEntity, sourceId) {
sql.transactional(() => {
sql.replace('options', remoteEntity);
entityChangesService.addOptionsSync(remoteEntity.name, sourceId, true);
entityChangesService.addOptionEntityChange(remoteEntity.name, sourceId, true);
});
return true;
@ -206,7 +206,7 @@ function updateRecentNotes(remoteEntity, sourceId) {
sql.transactional(() => {
sql.replace('recent_notes', remoteEntity);
entityChangesService.addRecentNoteSync(remoteEntity.noteId, sourceId);
entityChangesService.addRecentNoteEntityChange(remoteEntity.noteId, sourceId);
});
return true;
@ -222,7 +222,7 @@ function updateAttribute(remoteEntity, sourceId) {
sql.transactional(() => {
sql.replace("attributes", remoteEntity);
entityChangesService.addAttributeSync(remoteEntity.attributeId, sourceId);
entityChangesService.addAttributeEntityChange(remoteEntity.attributeId, sourceId);
});
return true;
@ -238,7 +238,7 @@ function updateApiToken(entity, sourceId) {
sql.transactional(() => {
sql.replace("api_tokens", entity);
entityChangesService.addApiTokenSync(entity.apiTokenId, sourceId);
entityChangesService.addApiTokenEntityChange(entity.apiTokenId, sourceId);
});
return true;

View File

@ -138,7 +138,7 @@ function sortNotesAlphabetically(parentNoteId, directoriesFirst = false) {
position += 10;
}
entityChangesService.addNoteReorderingSync(parentNoteId);
entityChangesService.addNoteReorderingEntityChange(parentNoteId);
});
}