{"record":{"id":"8be01e71d9122e92","repo":"siyuan-note/siyuan","slug":"conf-language-239-8be01e","errorCode":null,"errorMessage":"Conf.language(239)","messagePattern":"Conf\\.language\\(239\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"kernel/model/mount.go","lineNumber":213,"sourceCode":"\nfunc collectBoxDeletedAttributeViewBlocks(boxID string) (ret map[string]map[string]struct{}, err error) {\n\trootIDs := treenode.GetRootBlockIDsByBoxID(boxID)\n\tif 1 > len(rootIDs) {\n\t\treturn map[string]map[string]struct{}{}, nil\n\t}\n\tboundAVIDs, err := sql.QueryBoundBlockAVIDsInBox(nil, rootIDs, boxID)\n\tif nil != err {\n\t\treturn nil, err\n\t}\n\treturn groupDeletedAttributeViewBlocks(boundAVIDs), nil\n}\n\nfunc RemoveBox(boxID string) (err error) {\n\tif !ast.IsNodeIDPattern(boxID) {\n\t\treturn errors.New(\"invalid notebook ID\")\n\t}\n\tif _, loaded := boxLock.LoadOrStore(boxID, true); loaded {\n\t\terr = errors.New(Conf.language(239))\n\t\treturn\n\t}\n\tdefer boxLock.Delete(boxID)\n\n\tif util.IsReservedFilename(boxID) {\n\t\treturn fmt.Errorf(\"can not remove [%s] caused by it is a reserved file\", boxID)\n\t}\n\n\tFlushTxQueue()\n\tsql.FlushQueue()\n\t// 索引和笔记本目录删除后无法再读取 custom-avs，需提前收集；实际删除成功后再清理绑定行。\n\tdeletedAttrViewBlockIDs, err := collectBoxDeletedAttributeViewBlocks(boxID)\n\tif nil != err {\n\t\treturn fmt.Errorf(\"query database-bound blocks in notebook [%s] failed: %w\", boxID, err)\n\t}\n\tisUserGuide := IsUserGuide(boxID)\n\tlocalPath := filepath.Join(util.DataDir, boxID)\n\tif !filelock.IsExist(localPath) {","sourceCodeStart":195,"sourceCodeEnd":231,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/model/mount.go#L195-L231","documentation":"RemoveBox refuses to run when the notebook is already locked by another remove/mount operation. boxLock.LoadOrStore(boxID, true) fails because the box ID is in the concurrent-operation map, so the localized i18n message (key 239, 'the operation is in progress, please wait') is returned. This is a concurrency guard against racing notebook removal and mounting.","triggerScenarios":"Calling /api/notebook/removeNotebook (RemoveBox) while the same box ID is locked by a concurrent Mount/mountBox, UnlockAndMountBox, or a second RemoveBox that has not yet released boxLock via defer boxLock.Delete.","commonSituations":"Double-clicking the notebook delete button firing two API calls; a UI auto-mount racing a manual removal; scripted API calls removing and opening the same notebook in quick succession.","solutions":["Wait for the in-flight operation to finish, then retry the removal once","Ensure only one remove/mount call per notebook is in flight; debounce UI triggers","Check server logs for the overlapping operation that held the lock"],"exampleFix":"// before: parallel calls\nfetchPost('/api/notebook/removeNotebook', {notebook: id});\nfetchPost('/api/notebook/openNotebook', {notebook: id});\n// after: sequential\nawait fetchPost('/api/notebook/removeNotebook', {notebook: id});\nawait fetchPost('/api/notebook/openNotebook', {notebook: id});","handlingStrategy":"retry","validationCode":"// no public lock check; serialize calls yourself\nlet removing = false;\nasync function safeRemove(id){ if (removing) return; removing = true; try { await removeNotebook(id); } finally { removing = false; } }","typeGuard":null,"tryCatchPattern":"try { await removeNotebook(id); } catch (e) { if (isOperationInProgress(e)) await waitForIdleThenRetry(id); }","preventionTips":["Serialize all notebook mutations per notebook ID","Debounce rapid UI delete/open actions","Avoid concurrent scripts touching the same notebook"],"tags":["concurrency","notebook","locked-resource"],"backgroundTag":"invalid-state-transition","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"}