{"record":{"id":"4b38d4ca7e03a669","repo":"cloudreve/cloudreve","slug":"failed-to-unlink-entity-v","errorCode":null,"errorMessage":"failed to unlink entity: %v","messagePattern":"failed to unlink entity: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"inventory/file.go","lineNumber":887,"sourceCode":"\t\tversionCount++\n\t\tif versionCount > max {\n\t\t\t// By default, eager-loaded entity is sorted by ID in descending order.\n\t\t\t// So we can just unlink the entity and it will be the older version.\n\t\t\tnewDiff, err := f.UnlinkEntity(ctx, e, file, owner)\n\t\t\tif err != nil {\n\t\t\t\treturn diff, fmt.Errorf(\"failed to cap file entities: %v\", err)\n\t\t\t}\n\n\t\t\tdiff.Merge(newDiff)\n\t\t}\n\t}\n\n\treturn diff, nil\n}\n\nfunc (f *fileClient) UnlinkEntity(ctx context.Context, entity *ent.Entity, file *ent.File, owner *ent.User) (StorageDiff, error) {\n\tif err := f.client.Entity.UpdateOne(entity).RemoveFile(file).AddReferenceCount(-1).Exec(ctx); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to unlink entity: %v\", err)\n\t}\n\n\treturn map[int]int64{owner.ID: entity.Size * int64(-1)}, nil\n}\n\nfunc (f *fileClient) IsStoragePolicyUsedByEntities(ctx context.Context, policyID int) (bool, error) {\n\tres, err := f.client.Entity.Query().Where(entity.StoragePolicyEntities(policyID)).Limit(1).All(ctx)\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"failed to check if storage policy is used by entities: %v\", err)\n\t}\n\n\tif len(res) > 0 {\n\t\treturn true, nil\n\t}\n\n\treturn false, nil\n}\n","sourceCodeStart":869,"sourceCodeEnd":905,"githubUrl":"https://github.com/cloudreve/cloudreve/blob/20c95ad73f3a8bcb72887fea91ff31ab24fa1011/inventory/file.go#L869-L905","documentation":"UnlinkEntity removes the file from the entity's m2m file edge and decrements reference count in one Entity.UpdateOne(...).Exec. Failure means the database rejected that update: the entity row no longer exists, a join-table problem, or connectivity. The storage diff is only returned on success.","triggerScenarios":"Two workers unlinking the same entity concurrently; the entity deleted before the unlink executes; reference_count constrained below the decremented value (if a check constraint exists); database unavailable.","commonSituations":"Retention/cleanup jobs racing with upload completion or failure handlers; multi-worker deployments processing the same files.","solutions":["Treat ent not-found as already-unlinked (benign) and return an empty diff instead of failing","Make the unlink idempotent and safe to retry","Check database connectivity and inspect the wrapped %v for the driver-specific cause"],"exampleFix":"// before\nif err := f.client.Entity.UpdateOne(entity).RemoveFile(file).AddReferenceCount(-1).Exec(ctx); err != nil {\n\treturn nil, fmt.Errorf(\"failed to unlink entity: %v\", err)\n}\n// after\nif err := f.client.Entity.UpdateOne(entity).RemoveFile(file).AddReferenceCount(-1).Exec(ctx); err != nil {\n\tif ent.IsNotFound(err) {\n\t\treturn map[int]int64{}, nil\n\t}\n\treturn nil, fmt.Errorf(\"failed to unlink entity: %v\", err)\n}","handlingStrategy":"try-catch","validationCode":"if _, err := f.client.Entity.Get(ctx, entity.ID); err != nil {\n\tif ent.IsNotFound(err) {\n\t\treturn map[int]int64{}, nil // already gone; nothing to unlink\n\t}\n\treturn nil, err\n}","typeGuard":null,"tryCatchPattern":"diff, err := f.UnlinkEntity(ctx, entity, file, owner)\nif err != nil {\n\tif ent.IsNotFound(err) {\n\t\tdiff = map[int]int64{} // benign: entity already removed\n\t} else {\n\t\treturn diff, err\n\t}\n}","preventionTips":["Treat not-found during unlink as success (idempotent semantics)","Avoid concurrent unlink of the same entity from multiple workers","Log reference count changes for auditability during retention runs"],"tags":["golang","ent","cloudreve","database","concurrency"],"backgroundTag":null,"analyzedSha":"20c95ad73f3a8bcb72887fea91ff31ab24fa1011","analyzedAt":"2026-08-16T01:42:55.403Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}