{"record":{"id":"12fd0c83eef35c21","repo":"siyuan-note/siyuan","slug":"source-changed-during-asset-relink-s","errorCode":null,"errorMessage":"source changed during asset relink: %s","messagePattern":"source changed during asset relink: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/util/compare_write.go","lineNumber":28,"sourceCode":"\t\"os\"\n\n\t\"github.com/88250/gulu\"\n\t\"github.com/siyuan-note/filelock\"\n)\n\n// WriteFileIfUnchanged 在同一文件锁内比对扫描源并原子写入；original 为 nil 时要求目标不存在。\nfunc WriteFileIfUnchanged(path string, original, data []byte) error {\n\tfilelock.Lock(path)\n\tdefer filelock.Unlock(path)\n\tcurrent, err := os.ReadFile(path)\n\tif original == nil {\n\t\tif !os.IsNotExist(err) {\n\t\t\treturn fmt.Errorf(\"target already exists or is unreadable: %s\", path)\n\t\t}\n\t} else if err != nil {\n\t\treturn err\n\t} else if !bytes.Equal(current, original) {\n\t\treturn fmt.Errorf(\"source changed during asset relink: %s\", path)\n\t}\n\treturn gulu.File.WriteFileSafer(path, data, 0644)\n}\n","sourceCodeStart":10,"sourceCodeEnd":32,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/util/compare_write.go#L10-L32","documentation":"WriteFileIfUnchanged implements optimistic concurrency: the caller passes the content it read (original) and the function re-reads the file under a lock, refusing to write if the bytes changed in between. This error means the file on disk no longer matches the snapshot the caller saw, so writing would silently clobber an intermediate modification (typically during asset relink).","triggerScenarios":"Calling WriteFileIfUnchanged(path, original, data) where another writer modified path after original was captured; thrown when bytes.Equal(current, original) fails in the relink flow (apply, Save, saveAttributeView, WriteTreeIfUnchanged).","commonSituations":"Concurrent sync or relink operations touching the same asset/tree; user edits the file while an index/relink job runs; stale snapshot held across a long operation; multiple kernel instances sharing a workspace directory.","solutions":["Re-read the current file content, recompute the operation on the fresh data, and retry the compare-write","Retry with backoff if concurrent relink/sync writers are expected to settle","Serialize the operation so the read-snapshot and the write happen in one step (or under the same higher-level lock)","Investigate what else writes the path; if clobbering is acceptable, write directly without the compare wrapper"],"exampleFix":"// before\nerr := util.WriteFileIfUnchanged(path, staleOriginal, data)\n// after\nfor i := 0; i < 3; i++ {\n    current, _ := os.ReadFile(path)\n    data = recompute(current)\n    err = util.WriteFileIfUnchanged(path, current, data)\n    if err == nil { break }\n    time.Sleep(time.Millisecond * 100)\n}","handlingStrategy":"retry","validationCode":"current, err := os.ReadFile(path)\nif err != nil { return err }\n// use `current` as original right before writing","typeGuard":null,"tryCatchPattern":"for attempt := 0; attempt < 3; attempt++ {\n    snapshot, _ := os.ReadFile(path)\n    newData := recompute(snapshot)\n    err := util.WriteFileIfUnchanged(path, snapshot, newData)\n    if err == nil { break }\n    time.Sleep(100 * time.Millisecond)\n}","preventionTips":["Capture original and write in the tightest possible window","Retry the read-modify-write loop on conflict rather than failing the whole operation","Serialize concurrent relink/sync jobs touching the same paths"],"tags":["filesystem","concurrency","optimistic-locking","lost-update"],"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"}