pool: return an error instead of panicking when an RW is used after Close

*pool.RW implements io.Closer, so when one is used directly as an
http.Request body the transport closes it after each attempt. A retry
which then seeks and re-reads the RW would index the freed pages and
panic with "index out of range" in readPage.

Read, Write, ReadFrom, WriteTo and Seek now return ErrClosed (which
wraps io/fs.ErrClosed) after Close, and Close is safe to call more
than once.
This commit is contained in:
Nick Craig-Wood
2026-08-28 20:45:12 +01:00
parent e5e1ee3e96
commit 55fa9b6ab2
2 changed files with 94 additions and 22 deletions
+51 -22
View File
@@ -3,7 +3,9 @@ package pool
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"io" "io"
"io/fs"
"sync" "sync"
"time" "time"
) )
@@ -31,6 +33,7 @@ type RW struct {
size int // size written size int // size written
lastOffset int // size in last page lastOffset int // size in last page
written chan struct{} // signalled when a write happens written chan struct{} // signalled when a write happens
closed bool // set after Close and the pages are returned to the pool
// Read side Variables // Read side Variables
out int // offset we are reading from out int // offset we are reading from
@@ -40,6 +43,9 @@ type RW struct {
} }
var ( var (
// ErrClosed is returned from all operations on an RW after Close.
// It wraps fs.ErrClosed so errors.Is(err, fs.ErrClosed) is true.
ErrClosed = fmt.Errorf("pool.RW: %w", fs.ErrClosed)
errInvalidWhence = errors.New("pool.RW Seek: invalid whence") errInvalidWhence = errors.New("pool.RW Seek: invalid whence")
errNegativeSeek = errors.New("pool.RW Seek: negative position") errNegativeSeek = errors.New("pool.RW Seek: negative position")
errSeekPastEnd = errors.New("pool.RW Seek: attempt to seek past end of data") errSeekPastEnd = errors.New("pool.RW Seek: attempt to seek past end of data")
@@ -117,10 +123,17 @@ func (rw *RW) DelayAccounting(i int) {
// Returns the page and offset of i for reading. // Returns the page and offset of i for reading.
// //
// Ensure there are pages before calling this. // Returns ErrClosed if the RW has been closed or io.EOF if i is at or
func (rw *RW) readPage(i int) (page []byte) { // beyond the end of the data written.
func (rw *RW) readPage(i int) (page []byte, err error) {
rw.mu.Lock() rw.mu.Lock()
defer rw.mu.Unlock() defer rw.mu.Unlock()
if rw.closed {
return nil, ErrClosed
}
if i >= rw.size {
return nil, io.EOF
}
// Count a read of the data if we read the first page // Count a read of the data if we read the first page
if i == 0 { if i == 0 {
rw.reads++ rw.reads++
@@ -132,7 +145,7 @@ func (rw *RW) readPage(i int) (page []byte) {
if pageNumber == len(rw.pages)-1 { if pageNumber == len(rw.pages)-1 {
page = page[:rw.lastOffset] page = page[:rw.lastOffset]
} }
return page[offset:] return page[offset:], nil
} }
// account for n bytes being read // account for n bytes being read
@@ -150,13 +163,6 @@ func (rw *RW) accountRead(n int) error {
return nil return nil
} }
// Returns true if we have read to EOF
func (rw *RW) eof() bool {
rw.mu.Lock()
defer rw.mu.Unlock()
return rw.out >= rw.size
}
// Read reads up to len(p) bytes into p. It returns the number of // Read reads up to len(p) bytes into p. It returns the number of
// bytes read (0 <= n <= len(p)) and any error encountered. If some // bytes read (0 <= n <= len(p)) and any error encountered. If some
// data is available but not len(p) bytes, Read returns what is // data is available but not len(p) bytes, Read returns what is
@@ -167,10 +173,10 @@ func (rw *RW) Read(p []byte) (n int, err error) {
page []byte page []byte
) )
for len(p) > 0 { for len(p) > 0 {
if rw.eof() { page, err = rw.readPage(rw.out)
return n, io.EOF if err != nil {
return n, err
} }
page = rw.readPage(rw.out)
nn = copy(p, page) nn = copy(p, page)
p = p[nn:] p = p[nn:]
n += nn n += nn
@@ -194,8 +200,13 @@ func (rw *RW) WriteTo(w io.Writer) (n int64, err error) {
nn int nn int
page []byte page []byte
) )
for !rw.eof() { for {
page = rw.readPage(rw.out) page, err = rw.readPage(rw.out)
if err == io.EOF {
return n, nil
} else if err != nil {
return n, err
}
nn, err = w.Write(page) nn, err = w.Write(page)
n += int64(nn) n += int64(nn)
rw.out += nn rw.out += nn
@@ -207,15 +218,19 @@ func (rw *RW) WriteTo(w io.Writer) (n int64, err error) {
return n, err return n, err
} }
} }
return n, nil
} }
// Get the page we are writing to // Get the page we are writing to
func (rw *RW) writePage() (page []byte) { //
// Returns ErrClosed if the RW has been closed.
func (rw *RW) writePage() (page []byte, err error) {
rw.mu.Lock() rw.mu.Lock()
defer rw.mu.Unlock() defer rw.mu.Unlock()
if rw.closed {
return nil, ErrClosed
}
if len(rw.pages) > 0 && rw.lastOffset < rw.pool.bufferSize { if len(rw.pages) > 0 && rw.lastOffset < rw.pool.bufferSize {
return rw.pages[len(rw.pages)-1][rw.lastOffset:] return rw.pages[len(rw.pages)-1][rw.lastOffset:], nil
} }
if len(rw.reserved) > 0 { if len(rw.reserved) > 0 {
// Get reserved pages if available // Get reserved pages if available
@@ -228,7 +243,7 @@ func (rw *RW) writePage() (page []byte) {
} }
rw.pages = append(rw.pages, page) rw.pages = append(rw.pages, page)
rw.lastOffset = 0 rw.lastOffset = 0
return page return page, nil
} }
// Write writes len(p) bytes from p to the underlying data stream. It returns // Write writes len(p) bytes from p to the underlying data stream. It returns
@@ -239,7 +254,10 @@ func (rw *RW) Write(p []byte) (n int, err error) {
page []byte page []byte
) )
for len(p) > 0 { for len(p) > 0 {
page = rw.writePage() page, err = rw.writePage()
if err != nil {
return n, err
}
nn = copy(page, p) nn = copy(page, p)
p = p[nn:] p = p[nn:]
n += nn n += nn
@@ -264,7 +282,10 @@ func (rw *RW) ReadFrom(r io.Reader) (n int64, err error) {
page []byte page []byte
) )
for err == nil { for err == nil {
page = rw.writePage() page, err = rw.writePage()
if err != nil {
return n, err
}
nn, err = r.Read(page) nn, err = r.Read(page)
n += int64(nn) n += int64(nn)
rw.mu.Lock() rw.mu.Lock()
@@ -316,7 +337,11 @@ func (rw *RW) Seek(offset int64, whence int) (int64, error) {
var abs int64 var abs int64
rw.mu.Lock() rw.mu.Lock()
size := int64(rw.size) size := int64(rw.size)
closed := rw.closed
rw.mu.Unlock() rw.mu.Unlock()
if closed {
return 0, ErrClosed
}
switch whence { switch whence {
case io.SeekStart: case io.SeekStart:
abs = offset abs = offset
@@ -337,10 +362,14 @@ func (rw *RW) Seek(offset int64, whence int) (int64, error) {
return abs, nil return abs, nil
} }
// Close the buffer returning memory to the pool // Close the buffer returning memory to the pool.
//
// It is safe to call more than once. All other operations return
// ErrClosed afterwards.
func (rw *RW) Close() error { func (rw *RW) Close() error {
rw.mu.Lock() rw.mu.Lock()
defer rw.mu.Unlock() defer rw.mu.Unlock()
rw.closed = true
rw.signalWrite() // signal more data available rw.signalWrite() // signal more data available
rw.pool.PutN(rw.pages) rw.pool.PutN(rw.pages)
clear(rw.pages) clear(rw.pages)
+43
View File
@@ -5,6 +5,7 @@ import (
"context" "context"
"errors" "errors"
"io" "io"
"io/fs"
"sync" "sync"
"testing" "testing"
"time" "time"
@@ -607,3 +608,45 @@ func TestRWConcurrency(t *testing.T) {
} }
} }
// Check that using an RW after Close returns an ErrClosed
func TestRWUseAfterClose(t *testing.T) {
rw := NewRW(rwPool)
testData := []byte("Goodness!!")
n, err := rw.Write(testData)
require.NoError(t, err)
assert.Equal(t, len(testData), n)
require.NoError(t, rw.Close())
// Rewind and read as an HTTP retry would
_, err = rw.Seek(0, io.SeekStart)
assert.ErrorIs(t, err, ErrClosed)
buf := make([]byte, 16)
_, err = rw.Read(buf)
assert.ErrorIs(t, err, ErrClosed)
_, err = rw.WriteTo(io.Discard)
assert.ErrorIs(t, err, ErrClosed)
_, err = rw.Write(testData)
assert.ErrorIs(t, err, ErrClosed)
_, err = rw.ReadFrom(bytes.NewReader(testData))
assert.ErrorIs(t, err, ErrClosed)
// Check it matches the standard library sentinel too
assert.ErrorIs(t, err, fs.ErrClosed)
// Closing twice is fine
assert.NoError(t, rw.Close())
// Check a fully read RW still errors after Close rather than
// looking like an empty body
rw = NewRW(rwPool)
_, err = rw.Write(testData)
require.NoError(t, err)
_, err = rw.WriteTo(io.Discard)
require.NoError(t, err)
require.NoError(t, rw.Close())
_, err = rw.WriteTo(io.Discard)
assert.ErrorIs(t, err, ErrClosed)
_, err = rw.Read(buf)
assert.ErrorIs(t, err, ErrClosed)
}