{"record":{"id":"d346661a67d88720","repo":"vxcontrol/pentagi","slug":"failed-to-commit-blob-s-w","errorCode":null,"errorMessage":"failed to commit blob %s: %w","messagePattern":"failed to commit blob (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":500,"severity":"error","filePath":"backend/pkg/resources/resources.go","lineNumber":318,"sourceCode":"// CommitBlob atomically moves tmpPath to the .blob destination for hash.  If\n// the blob already exists (race with concurrent upload of identical file) the\n// tmp file is removed and no error is returned.\nfunc CommitBlob(dataDir, hash, tmpPath string) error {\n\tif err := validateBlobHash(hash); err != nil {\n\t\treturn err\n\t}\n\tif err := EnsureResourcesDir(dataDir); err != nil {\n\t\treturn err\n\t}\n\n\tdest := BlobPath(dataDir, hash)\n\tif _, err := os.Lstat(dest); err == nil {\n\t\t// Already exists — remove tmp and consider success.\n\t\tos.Remove(tmpPath)\n\t\treturn nil\n\t}\n\tif err := os.Rename(tmpPath, dest); err != nil {\n\t\treturn fmt.Errorf(\"failed to commit blob %s: %w\", hash, err)\n\t}\n\treturn nil\n}\n\n// ZipResources writes a ZIP archive to w containing all entries in files.\n// Each ZipEntry maps a .blob file on disk to a path inside the archive.\nfunc ZipResources(w io.Writer, entries []ZipEntry) (err error) {\n\t// The streaming HTTP caller commits its 200 status on the first byte written,\n\t// so a missing blob must be caught before then, or the client gets a truncated\n\t// archive under 200. Stat all blobs up front; don't fold into the write loop.\n\tfor _, e := range entries {\n\t\tif _, statErr := os.Lstat(e.BlobPath); statErr != nil {\n\t\t\treturn fmt.Errorf(\"failed to stat blob %s: %w\", e.BlobPath, statErr)\n\t\t}\n\t}\n\n\tzw := zip.NewWriter(w)\n\tdefer func() {","sourceCodeStart":300,"sourceCodeEnd":336,"githubUrl":"https://github.com/vxcontrol/pentagi/blob/ea665308baaff015b226f308438a68d929d0f29b/backend/pkg/resources/resources.go#L300-L336","documentation":"CommitBlob atomically moves the temp file to its content-addressed .blob destination with os.Rename; this error wraps a rename failure for the given hash. Renames fail across filesystem boundaries (EXDEV) or on permission/existence conflicts.","triggerScenarios":"Calling CommitBlob (via UploadResources or promoteToResources) where temp dir and blob dir are on different devices (rename returns 'invalid cross-device link'); destination parent missing or unwritable; concurrent cleanup removing the destination directory.","commonSituations":"Temp dir configured on tmpfs while blobs live on a mounted volume — the classic EXDEV case; container volume remounted read-only; blob directory deleted by a cleanup job mid-upload.","solutions":["Ensure the temp dir and blob dir reside on the same filesystem/mount so os.Rename works","If cross-device is unavoidable, fall back to copy-then-delete instead of rename","Check write permission on the blob destination directory for the service user","Check for concurrent jobs removing the blob directory during uploads"],"exampleFix":"// before\n# temp on tmpfs, blobs on volume → EXDEV\nvolumes: [\"/tmp/pentagi-tmp:/tmp:rw\", \"blobdata:/data/blobs\"]\n// after\n# both on the same volume\nvolumes: [\"blobdata:/data\"]  # tmp = /data/tmp, blobs = /data/blobs","handlingStrategy":"try-catch","validationCode":"func sameFilesystem(a, b string) (bool, error) {\n\tvar sa, sb syscall.Stat_t\n\tif err := syscall.Stat(a, &sa); err != nil {\n\t\treturn false, err\n\t}\n\tif err := syscall.Stat(b, &sb); err != nil {\n\t\treturn false, err\n\t}\n\treturn sa.Dev == sb.Dev, nil\n}","typeGuard":null,"tryCatchPattern":"err := resources.CommitBlob(tmpPath, hash)\nif err != nil {\n\tvar linkErr *os.LinkError\n\tif errors.As(err, &linkErr) && errors.Is(linkErr.Err, syscall.EXDEV) {\n\t\t// fallback: copy then remove\n\t\tdest := filepath.Join(blobRoot, hash)\n\t\tif cerr := copyFile(tmpPath, dest); cerr == nil {\n\t\t\tos.Remove(tmpPath)\n\t\t\treturn nil\n\t\t}\n\t}\n\treturn err\n}","preventionTips":["Keep the temp dir and blob store on the same mount/volume so os.Rename stays atomic","Check rename errors specifically for EXDEV and implement a copy fallback","Ensure the blob destination directory exists and is writable before committing","Avoid cleanup jobs racing with blob commits on the same directory"],"tags":["filesystem","atomic-rename","cross-device","storage"],"backgroundTag":"cross-device-rename","analyzedSha":"ea665308baaff015b226f308438a68d929d0f29b","analyzedAt":"2026-09-01T14:16:31.421Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}