{"record":{"id":"93b03c4b3a4f8702","repo":"siyuan-note/siyuan","slug":"pending-edits-during-asset-scan-retry-the-request","errorCode":null,"errorMessage":"pending edits during asset scan; retry the request","messagePattern":"pending edits during asset scan; retry the request","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"kernel/model/asset_relink_batch.go","lineNumber":177,"sourceCode":"\t\tcase <-p.ctx.Done():\n\t\t\treturn p.ctx.Err()\n\t\tcase <-time.After(10 * time.Millisecond):\n\t\t}\n\t}\n\t// 扫描期间允许编辑；保存前在事务锁内校验文件与目录快照，新增引用也会触发重试。\n\tif err := p.scan(); err != nil {\n\t\treturn err\n\t}\n\tp.finishPreflight()\n\tif p.result.DryRun || p.lookup {\n\t\treturn nil\n\t}\n\tif err := lockAssetRelink(p.ctx, &flushLock); err != nil {\n\t\treturn err\n\t}\n\tdefer flushLock.Unlock()\n\tif txQueueSize() > 0 {\n\t\treturn errors.New(\"pending edits during asset scan; retry the request\")\n\t}\n\treturn p.apply()\n}\n\nfunc (p *assetRelinkPlan) initialize() {\n\tif p.ctx == nil {\n\t\tp.ctx = context.Background()\n\t}\n\tif p.inventory == nil {\n\t\tp.inventory = map[string]assetRelinkStamp{}\n\t}\n\tif p.items == nil {\n\t\trule := p.assetRelinker\n\t\tp.items = []*assetRelinkItem{{input: apicontract.AssetRelinkMapping{OldPath: rule.oldPath, NewPath: rule.newPath}, rule: rule}}\n\t\tp.assetRelinker = &assetRelinker{routes: map[string]*assetRelinker{rule.oldPath: rule}, collectOnly: true, result: rule.result}\n\t}\n\tif p.matcher == nil {\n\t\tpatterns := []string{}","sourceCodeStart":159,"sourceCodeEnd":195,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/asset_relink_batch.go#L159-L195","documentation":"Before applying a relink plan, the runner acquires the flush lock and checks the pending transaction queue (txQueueSize). If there are uncommitted document edits queued, applying renames now could race with the write queue and corrupt references, so the operation aborts and asks the client to retry once edits are flushed.","triggerScenarios":"Calling RelinkAssets/RelinkAssetWithContext while the user (or another client) is actively editing documents and the async transaction queue is non-empty (txQueueSize() > 0) at apply time.","commonSituations":"Triggering a batch relink via API immediately after heavy editing; automated scripts relinking while the editor is open and auto-saving; concurrent websocket clients producing ongoing transactions.","solutions":["Retry the relink after a short delay so the transaction queue drains","Ensure documents are saved/flushed (pause editing or commit pending edits) before running relink","Schedule relink jobs during idle periods or stop the editor/other clients first","Add client-side retry with backoff on this specific error message"],"exampleFix":"// before\n_, err := model.RelinkAssets(ctx, mappings, false)\n// after\nvar result apicontract.AssetReferencesData\nfor i := 0; i < 5; i++ {\n\tresult, err = model.RelinkAssets(ctx, mappings, false)\n\tif err == nil || !strings.Contains(fmt.Sprint(err), \"pending edits\") { break }\n\ttime.Sleep(2 * time.Second)\n}","handlingStrategy":"retry","validationCode":"if model.TxQueueSize() > 0 { waitUntilQueueEmpty(timeout) } // expose or check via API before calling","typeGuard":null,"tryCatchPattern":"for i := 0; i < maxRetries; i++ {\n\tresult, err := model.RelinkAssets(ctx, mappings, dryRun)\n\tif err == nil || !strings.Contains(fmt.Sprint(err), \"pending edits\") { return result, err }\n\ttime.Sleep(backoff(i))\n}","preventionTips":["Run relink when the workspace is idle (no active editing)","Add retry-with-backoff for transient queue-busy conditions","Pause autosave-heavy operations during large batch relinks"],"tags":["go","concurrency","retry","transactions"],"backgroundTag":"request-timeout","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}