{"record":{"id":"97d3324cae8d4f60","repo":"siyuan-note/siyuan","slug":"source-changed-during-scan-s","errorCode":null,"errorMessage":"source changed during scan: %s","messagePattern":"source changed during scan: (.+?)","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/asset_relink_batch.go","lineNumber":457,"sourceCode":"\tif err = p.checkContext(); err != nil {\n\t\treturn err\n\t}\n\thistoryDir, err := newAssetRelinkHistoryDir()\n\tif err != nil {\n\t\treturn err\n\t}\n\tp.result.HistoryPath = filepath.ToSlash(historyDir)\n\t// 所有候选源先备份；备份失败属于请求级错误，此时尚未修改任何引用。\n\tfor _, file := range files {\n\t\tif err = p.checkContext(); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif file.before == nil {\n\t\t\tcontinue\n\t\t}\n\t\tcurrent, readErr := filelock.ReadFile(file.path)\n\t\tif readErr != nil || !bytes.Equal(current, file.before) {\n\t\t\treturn fmt.Errorf(\"source changed during scan: %s\", file.path)\n\t\t}\n\t\trel, relErr := filepath.Rel(util.DataDir, file.path)\n\t\tif relErr != nil || strings.HasPrefix(rel, \"..\") {\n\t\t\treturn errors.New(\"invalid history source path\")\n\t\t}\n\t\tdest := filepath.Join(historyDir, rel)\n\t\tif err = os.MkdirAll(filepath.Dir(dest), 0755); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err = gulu.File.WriteFileSafer(dest, file.before, 0644); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tindexHistoryDir(filepath.Base(historyDir), util.NewLute())\n\tchangedViews, reload := map[string]bool{}, map[string]bool{}\n\tdefer func() {\n\t\tfor _, tree := range p.trees {\n\t\t\tviewChanged := false","sourceCodeStart":439,"sourceCodeEnd":475,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/asset_relink_batch.go#L439-L475","documentation":"To support undo/history, apply() re-reads each source file it scanned and compares it byte-for-byte with the snapshot taken during scanning. If the file cannot be read or its content differs, apply aborts with this error before writing any rewrite, ensuring no relink is applied over silently changed content.","triggerScenarios":"A document file (.sy) covered by the plan is edited/saved between scan and apply; the file becomes unreadable (permissions, lock); external tools rewrite the file during the scan window.","commonSituations":"User typing in the editor while a relink batch runs; sync applying remote changes mid-scan; two concurrent relink/find operations overlapping on the same documents.","solutions":["Retry the whole relink request after the modifications settle so a fresh scan snapshot is taken","Ensure no editor clients or sync jobs write to the workspace during the operation","Serialize relink requests (don't run find and relink concurrently on the same paths)","Check file permissions/locks if readErr (not content change) is the recurring cause"],"exampleFix":"// before\nerrCh := make(chan error, 2)\ngo func() { _, err := model.FindAssetReferencesBatch(ctx, paths); errCh <- err }()\ngo func() { _, err := model.RelinkAssets(ctx, mappings, false); errCh <- err }() // races with scan\n// after\n_, err := model.FindAssetReferencesBatch(ctx, paths)\n_, err = model.RelinkAssets(ctx, mappings, false) // sequential; retry on 'source changed'","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"_, err := model.RelinkAssets(ctx, mappings, dryRun)\nif err != nil && strings.Contains(fmt.Sprint(err), \"source changed during scan\") {\n\ttime.Sleep(backoff)\n\t_, err = model.RelinkAssets(ctx, mappings, dryRun) // rescan and reapply\n}","preventionTips":["Do not edit documents while a relink runs","Serialize find/relink operations; never run them concurrently","Pause sync/external writers for the duration of the batch"],"tags":["go","concurrency","race-condition","file-write"],"backgroundTag":"checksum-mismatch","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"}