juicedata/juicefs · warning

closed

Error message

closed

What it means

parallelDownloader's reader sets its persistent error to "closed" when Close is called; subsequent reads after close return this error. doCopySingle triggers it when it closes the downloader while (or before) a read is still in flight.

Solutions

  1. Ensure all reads complete before calling Close, or stop reading after Close returns
  2. Use io.Copy semantics that stop reading once EOF/err is seen and only close once
  3. Add synchronization so Close happens only after the reader goroutine finishes
  4. If intentional shutdown, ignore/compare the "closed" error instead of treating it as failure

Example fix

// before
io.Copy(dst, reader)
reader.Close()
// after
_, err := io.Copy(dst, reader)
if err != nil && !strings.Contains(err.Error(), "closed") {
	return err
}
reader.Close()
Defensive patterns

Strategy: try-catch

Type guard

func isClosedErr(err error) bool { return err != nil && err.Error() == "closed" }

Try / catch

n, err := io.Copy(dst, reader)
if isClosedErr(err) {
	return nil // expected after shutdown
} else if err != nil {
	return err
}

Prevention

When it happens

Trigger: Reading from a downloader reader after Close was invoked; concurrent copy logic closing the reader while another goroutine still reads from it.

Common situations: Sync copy cancellation or early exit where one path closes the reader but a deferred/buffered read follows; races between the copy loop and error handling.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of juicedata/juicefs@c9a67b23e8 (2026-09-06). Data as JSON: /api/errors/278fe203a5ecccbd. Report an issue: GitHub.

Appendix: source

Thrown at pkg/sync/download.go:151

		delete(r.buffers, off)
		r.Unlock()
		<-r.concurrent
	}
	if copiedBytes != nil {
		copiedBytes.IncrInt64(int64(n))
	}
	return n, nil
}

func (r *parallelDownloader) Close() {
	r.Lock()
	defer r.Unlock()
	for _, p := range r.buffers {
		downloadBufPool.Put(p)
	}
	r.buffers = nil
	if r.err == nil {
		r.err = errors.New("closed")
	}
}

func newParallelDownloader(store object.ObjectStorage, key string, size int64, bSize int64, concurrent chan int) *parallelDownloader {
	if bSize < 1 {
		panic("concurrent and blockSize must be positive integer")
	}
	down := &parallelDownloader{
		src:        store,
		key:        key,
		fsize:      size,
		blockSize:  bSize,
		concurrent: concurrent,
		buffers:    make(map[int64]*[]byte),
	}
	down.notify = sync.NewCond(down)
	go down.download()
	return down

View on GitHub (pinned to c9a67b23e8)