{"record":{"id":"31d0e4a09a3f5f43","repo":"GopeedLab/gopeed","slug":"blob-source-closed","errorCode":null,"errorMessage":"blob source closed","messagePattern":"blob source closed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"internal/blob/registry.go","lineNumber":34,"sourceCode":"\t\"strings\"\n\t\"sync\"\n\t\"time\"\n)\n\nconst urlPathPrefix = \"/__blob/\"\n\nconst rangeSourceFailureLimit = 2\n\n// unclaimedSourceTTL bounds how long a session-backed source may keep its\n// engine alive without ever being claimed by a download task.\nvar unclaimedSourceTTL = 10 * time.Minute\n\nvar (\n\tErrInvalidURL      = errors.New(\"invalid blob url\")\n\tErrInvalidOptions  = errors.New(\"invalid blob options\")\n\tErrSourceNotFound  = errors.New(\"blob source not found\")\n\tErrSourceRevoked   = errors.New(\"blob source revoked\")\n\tErrSourceClosed    = errors.New(\"blob source closed\")\n\tErrRangeNotAllowed = errors.New(\"blob range not allowed\")\n)\n\ntype SessionRef interface {\n\tRetain()\n\tRelease()\n}\n\ntype OpenRequest struct {\n\tOffset int64\n\tEnd    int64\n}\n\ntype OpenFunc func(ctx context.Context, req OpenRequest) (io.ReadCloser, error)\n\ntype CreateOptions struct {\n\tContentType string\n\tSize        int64","sourceCodeStart":16,"sourceCodeEnd":52,"githubUrl":"https://github.com/GopeedLab/gopeed/blob/7b7327ffb30816273a74b142cccc0bc10c5a4c67/internal/blob/registry.go#L16-L52","documentation":"Returned by Source.releaseTask when taskRefs is already zero (registry.go:423-425) — i.e. Release(raw) was called more times than Acquire(raw) succeeded, or without any prior Acquire. It is a reference-counting imbalance detector. The same sentinel also surfaces (wrapped, 'blob source closed: opener returned a nil reader') from ServeHTTP when an OpenFunc returns nil, nil (registry.go:355-356).","triggerScenarios":"Calling registry.Release for a task that never called Acquire (or whose Acquire failed); double-Release from a cleanup path plus a defer; releasing from both an error path and a normal path; an OpenFunc implementation returning (nil, nil) so HTTP GETs on the source fail with 410 Gone and SourceError reports this wrapped error.","commonSituations":"defer registry.Release(url) combined with an explicit Release on error; retries that re-run cleanup code; two goroutines (task + extension) both owning the release; custom openers that return nil reader on an internal edge case instead of an error.","solutions":["Audit Acquire/Release pairing: Acquire once when a task takes ownership, Release exactly once, guarded by a flag or done-channel","Use errors.Is(err, blob.ErrSourceClosed) to detect over-release in logs — it is a bug in caller lifecycle logic, not a transient condition","If the wrapped 'nil reader' variant appears, fix the OpenFunc to return a real error instead of (nil, nil)","Consider owning the release with sync.Once or a single deferred release at the function that acquired"],"exampleFix":"// before\nregistry.Acquire(url)\ndefer registry.Release(url)\n// ... later on error path:\nregistry.Release(url) // second release -> ErrSourceClosed\n\n// after\nif err := registry.Acquire(url); err != nil { return err }\nvar releaseOnce sync.Once\nrelease := func() { releaseOnce.Do(func() { _ = registry.Release(url) }) }\ndefer release()","handlingStrategy":"validation","validationCode":"owned := false\nacquire := func() error {\n    if err := registry.Acquire(blobURL); err != nil { return err }\n    owned = true\n    return nil\n}\nrelease := func() error {\n    if !owned { return nil } // prevents over-release\n    owned = false\n    return registry.Release(blobURL)\n}","typeGuard":null,"tryCatchPattern":"if err := registry.Release(blobURL); err != nil && errors.Is(err, blob.ErrSourceClosed) {\n    // over-release bug: log it and fix the Acquire/Release pairing; do not retry\n    log.Printf(\"blob over-release for %s\", blobURL)\n}","preventionTips":["Own releases with sync.Once or a single owner goroutine","Never release in both an error branch and a deferred cleanup of the same acquire","Openers must return (nil, err) on failure, never (nil, nil)"],"tags":["blob","lifecycle","reference-counting","bug"],"backgroundTag":null,"analyzedSha":"7b7327ffb30816273a74b142cccc0bc10c5a4c67","analyzedAt":"2026-08-16T02:51:03.250Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}