{"record":{"id":"d79e047a064892c4","repo":"siyuan-note/siyuan","slug":"ocr-metadata-changed-during-asset-relink","errorCode":null,"errorMessage":"OCR metadata changed during asset relink","messagePattern":"OCR metadata changed during asset relink","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"kernel/util/ocr_relink_batch.go","lineNumber":144,"sourceCode":"\t\t\treturn nil, errors.New(\"invalid OCR mapping index\")\n\t\t}\n\t\tresult := p.Results[i]\n\t\tif result.Reason != \"\" {\n\t\t\treturn nil, errors.New(result.Reason)\n\t\t}\n\t\tif !result.Changed {\n\t\t\tcontinue\n\t\t}\n\t\ttexts[(&url.URL{Path: p.mappings[i].NewPath}).EscapedPath()] = p.values[i]\n\t\tchanged = append(changed, i)\n\t}\n\tif len(changed) == 0 {\n\t\treturn nil, nil\n\t}\n\tassetsTextsLock.Lock()\n\tdefer assetsTextsLock.Unlock()\n\tif !maps.Equal(p.memory, assetsTexts) {\n\t\treturn nil, errors.New(\"OCR metadata changed during asset relink\")\n\t}\n\tbackup, err := json.MarshalIndent(p.texts, \"\", \"  \")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\thistory := filepath.Join(historyDir, \"assets\", \"ocr-texts.json\")\n\tif err = os.MkdirAll(filepath.Dir(history), 0755); err != nil {\n\t\treturn nil, err\n\t}\n\tif err = gulu.File.WriteFileSafer(history, backup, 0644); err != nil {\n\t\treturn nil, err\n\t}\n\tdata, err := json.MarshalIndent(texts, \"\", \"  \")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err = WriteFileIfUnchanged(p.path, p.original, data); err != nil {\n\t\treturn nil, err","sourceCodeStart":126,"sourceCodeEnd":162,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/util/ocr_relink_batch.go#L126-L162","documentation":"Save captures a snapshot of the in-memory OCR metadata (assetsTexts) when the plan was prepared, then re-acquires the lock before writing. If the in-memory map changed between PrepareAssetTextRelinks and Save (maps.Equal fails), the plan is stale and Save aborts with \"OCR metadata changed during asset relink\" to avoid clobbering concurrent updates with data computed from an outdated snapshot.","triggerScenarios":"Another goroutine modified the in-memory OCR metadata (e.g. via OCR recognition of new images, asset deletion cleanup, or a concurrent relink Save that replaced assetsTexts) between calling PrepareAssetTextRelinks and calling plan.Save on the same plan.","commonSituations":"Running asset rename/relink while background OCR indexing is still processing images; two concurrent relink operations; an OCR re-scan triggered by the user while a rename dialog's save was in flight.","solutions":["Discard the stale plan and re-run PrepareAssetTextRelinks, then call Save promptly on the fresh plan.","Retry the whole prepare→save sequence with a small backoff; it succeeds once concurrent OCR writes settle.","Avoid invoking asset relink while OCR recognition is running (wait for indexing to finish).","Serialize relink operations through a single caller so two relinks cannot race."],"exampleFix":"// before: hold a plan for a long time then save\nplan, _ := util.PrepareAssetTextRelinks(mappings)\n// ... long UI wait, OCR runs meanwhile ...\nplan.Save(historyDir, selected) // stale snapshot\n// after: re-prepare right before saving\nplan, err := util.PrepareAssetTextRelinks(mappings)\nif err == nil {\n    plan.Save(historyDir, selected)\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"for attempt := 0; attempt < 3; attempt++ {\n    plan, err := util.PrepareAssetTextRelinks(mappings)\n    if err != nil {\n        return err\n    }\n    if _, err := plan.Save(historyDir, selected); err == nil || err.Error() != \"OCR metadata changed during asset relink\" {\n        return err\n    }\n    time.Sleep(time.Duration(attempt+1) * 100 * time.Millisecond)\n}","preventionTips":["Minimize the window between PrepareAssetTextRelinks and Save","Wait for background OCR indexing to finish before relinking assets","Serialize relink/save operations so concurrent relinks cannot race","Treat this error as retryable: it is a stale-snapshot guard, not data corruption"],"tags":["concurrency","race","ocr","stale-state"],"backgroundTag":"invalid-state-transition","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"}