GopeedLab/gopeed · warning
blob source closed
Error message
blob source closed
What it means
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).
Source
Thrown at internal/blob/registry.go:34
"strings"
"sync"
"time"
)
const urlPathPrefix = "/__blob/"
const rangeSourceFailureLimit = 2
// unclaimedSourceTTL bounds how long a session-backed source may keep its
// engine alive without ever being claimed by a download task.
var unclaimedSourceTTL = 10 * time.Minute
var (
ErrInvalidURL = errors.New("invalid blob url")
ErrInvalidOptions = errors.New("invalid blob options")
ErrSourceNotFound = errors.New("blob source not found")
ErrSourceRevoked = errors.New("blob source revoked")
ErrSourceClosed = errors.New("blob source closed")
ErrRangeNotAllowed = errors.New("blob range not allowed")
)
type SessionRef interface {
Retain()
Release()
}
type OpenRequest struct {
Offset int64
End int64
}
type OpenFunc func(ctx context.Context, req OpenRequest) (io.ReadCloser, error)
type CreateOptions struct {
ContentType string
Size int64View on GitHub (pinned to 7b7327ffb3)
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
Example fix
// before
registry.Acquire(url)
defer registry.Release(url)
// ... later on error path:
registry.Release(url) // second release -> ErrSourceClosed
// after
if err := registry.Acquire(url); err != nil { return err }
var releaseOnce sync.Once
release := func() { releaseOnce.Do(func() { _ = registry.Release(url) }) }
defer release() Defensive patterns
Strategy: validation
Validate before calling
owned := false
acquire := func() error {
if err := registry.Acquire(blobURL); err != nil { return err }
owned = true
return nil
}
release := func() error {
if !owned { return nil } // prevents over-release
owned = false
return registry.Release(blobURL)
} Try / catch
if err := registry.Release(blobURL); err != nil && errors.Is(err, blob.ErrSourceClosed) {
// over-release bug: log it and fix the Acquire/Release pairing; do not retry
log.Printf("blob over-release for %s", blobURL)
} Prevention
- 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)
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- blob source not found
- blob source revoked
- invalid blob url
- invalid blob options
- cannot change ed2k target path after transfer started
AI-assisted analysis of GopeedLab/gopeed@7b7327ffb3 (2026-08-16).
Data as JSON: /api/errors/31d0e4a09a3f5f43.
Report an issue: GitHub.