{"record":{"id":"ba831f86f07c87a8","repo":"siyuan-note/siyuan","slug":"conf-language-239","errorCode":null,"errorMessage":"Conf.Language(239)","messagePattern":"Conf\\.Language\\(239\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/history.go","lineNumber":637,"sourceCode":"\tencrypted, err := isEncryptedHistoryBoxDir(filepath.Join(util.HistoryDir, parts[0], boxID))\n\tif err != nil {\n\t\tlogging.LogErrorf(\"inspect encrypted history path [%s] failed: %s\", absPath, err)\n\t\treturn true\n\t}\n\treturn encrypted\n}\n\nfunc RollbackNotebookHistory(historyPath string) (err error) {\n\thistoryPath, err = validateHistoryPath(historyPath)\n\tif err != nil {\n\t\treturn\n\t}\n\tboxID, err := validateNotebookHistoryPath(historyPath)\n\tif err != nil {\n\t\treturn\n\t}\n\tif _, loaded := boxLock.LoadOrStore(boxID, true); loaded {\n\t\treturn errors.New(Conf.Language(239))\n\t}\n\tdefer boxLock.Delete(boxID)\n\n\tfrom := historyPath\n\tto := filepath.Join(util.DataDir, boxID)\n\tif filelock.IsExist(to) {\n\t\treturn errors.New(Conf.Language(371))\n\t}\n\n\tif err = filelock.CopyNewtimes(from, to); err != nil {\n\t\tlogging.LogErrorf(\"copy file [%s] to [%s] failed: %s\", from, to, err)\n\t\treturn\n\t}\n\n\tIncSync()\n\tReloadFiletree()\n\tutil.PushMsg(Conf.Language(372), 3000)\n\treturn nil","sourceCodeStart":619,"sourceCodeEnd":655,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/model/history.go#L619-L655","documentation":"RollbackNotebookHistory takes a per-notebook lock (boxLock.LoadOrStore) to prevent two concurrent rollbacks of the same notebook. If the box is already locked — another rollback of that notebook is in flight — the call fails with the localized message Conf.Language(239) instead of corrupting data with overlapping restores.","triggerScenarios":"Two simultaneous calls to rollbackNotebookHistory (API /api/history/rollbackNotebookHistory) for the same notebook ID, e.g. double-clicking the rollback button or triggering rollback from two clients while the first copy operation is still running.","commonSituations":"Impatient users clicking rollback repeatedly; automation scripts firing overlapping requests; multi-window/multi-device sessions issuing the same rollback; slow copy of a large notebook holding the lock for a long time.","solutions":["Wait for the in-flight rollback to finish; the lock is removed (boxLock.Delete) when the first call completes","Disable the rollback button while a request is pending (single-flight on the client)","Serialize rollback requests per notebook in automation scripts (await previous before next)","Retry the request after a short delay if it was rejected due to the concurrent guard"],"exampleFix":"// before: fire-and-forget, allows double submit\nrollbackNotebookHistory(path); rollbackNotebookHistory(path);\n// after: guard against re-entry\nlet busy = false;\nasync function rollbackOnce(p) {\n  if (busy) return;\n  busy = true;\n  try { await rollbackNotebookHistory(p); } finally { busy = false; }\n}","handlingStrategy":"try-catch","validationCode":"const inFlight = new Set();\nfunction canStartNotebookRollback(boxID) { return !inFlight.has(boxID); }","typeGuard":"null","tryCatchPattern":"try { await rollbackNotebookHistory(p); } catch (e) { if (isI18nBusyMessage(e)) { await sleep(1000); /* retry once */ } else { throw e; } }","preventionTips":["Disable the rollback button while a request is pending","Serialize rollbacks per notebook in scripts and plugins","Avoid triggering the same rollback from multiple windows/devices at once"],"tags":["concurrency","history","lock","rollback"],"backgroundTag":"concurrent-operation-conflict","analyzedSha":"8641553a1f07374001902d3ce773285db1292b2d","analyzedAt":"2026-09-11T16:08:28.414Z","contentChangedAt":"2026-09-11T16:08:28.414Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}