Reader.curChunk can strand a waiter goroutine: Cond wakeups are sent without rmux held, and Close never broadcasts
Los mantenedores suelen responder en 1 día
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 4/5
- Tiempo estimado
- 1-2 días
- Aptitud para principiantes
- 56/100
Línea de trabajo
Start with the waiter loop and condition broadcasts in b2/reader.go, especially Reader.curChunk, setErr, and Close; the issue includes a regression test in b2/reader_wakeup_test.go. Read the retry block alongside related issue #57, then run the race detector command shown in the issue. Done means closed or failed readers leave no parked waiter goroutines and the existing test suites pass.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
Summary
(*Reader).curChunk starts a goroutine that waits on r.rcond (a sync.Cond) until the next chunk is ready, an error is set, or the context is done. Two things break that wait:
- Lost wakeups. The wait predicate reads
r.errandr.ctx, but those are changed without holdingrmux(theCond's lock), and the fetch workers callr.rcond.Broadcast()without holding it either. ABroadcastthat lands between the waiter's predicate check and its registration inWaitis lost. - No wakeup on
Close.Reader.Closecancels the context but never broadcasts, and idle fetch workers exit on<-r.ctx.Done()without broadcasting. So once a wakeup is lost, nothing else ever wakes the waiter.
The goroutine stays blocked in r.rcond.Wait() for the life of the process. The caller is not stuck — curChunk's outer select returns on <-r.ctx.Done() — which is why this goes unnoticed.
Affected: v0.8.0 (latest release) and current master — the relevant files are identical in both. Lower severity than #56-#58: it is timing-dependent.
Where it is (b2/reader.go)
- The waiter loop:
for r.chunks[r.chrid] == nil && r.getErr() == nil && r.ctx.Err() == nil { r.rcond.Wait() }(reader.go:192-196). setErrsetsr.errand callsr.cancel()underemux, notrmux(reader.go:84-90).- Every
r.rcond.Broadcast()is issued withoutrmuxheld (reader.go:143, 148, 171, 179, 185). Close(reader.go:78-82) cancels and removes the reader; noBroadcast.- Idle workers return on
<-r.ctx.Done()(reader.go:117) without broadcasting.
Reproduction
This is a race, so the test is a stress loop: 20,000 readers whose downloads fail after a small varying delay, then a count of curChunk goroutines still parked. It reproduces reliably only under the race detector, which slows scheduling enough to hit the window.
Regression test (b2/reader_wakeup_test.go)
package b2
import (
"context"
"errors"
"runtime"
"strings"
"sync/atomic"
"testing"
"time"
)
// wakeRoot fails every download after a small, varying delay so the fetch
// worker's setErr+Broadcast races the curChunk waiter's predicate check.
type wakeRoot struct {
*testRoot
calls int32
}
func (r *wakeRoot) createBucket(ctx context.Context, name, btype string, info map[string]string, rules []LifecycleRule, sse *ServerSideEncryption) (b2BucketInterface, error) {
b, err := r.testRoot.createBucket(ctx, name, btype, info, rules, sse)
if err != nil {
return nil, err
}
return &wakeBucket{testBucket: b.(*testBucket), r: r}, nil
}
type wakeBucket struct {
*testBucket
r *wakeRoot
}
func (b *wakeBucket) downloadFileByName(context.Context, string, int64, int64, bool) (b2FileReaderInterface, error) {
n := atomic.AddInt32(&b.r.calls, 1)
for spin := time.Now().Add(time.Duration(n%40) * time.Microsecond); time.Now().Before(spin); {
}
return nil, errors.New("injected download failure")
}
// Racy by nature: reliably red on v0.8.0 only under -race, which widens the
// window between the waiter's predicate check and its registration in Wait.
//
// go test -race -run TestReaderCurChunkWaitersDoNotLeak ./b2/
func TestReaderCurChunkWaitersDoNotLeak(t *testing.T) {
root := &wakeRoot{testRoot: &testRoot{bucketMap: make(map[string]map[string]string), errs: &errCont{}}}
bucket, err := (&Client{backend: &beRoot{b2i: root}}).NewBucket(context.Background(), bucketName, &BucketAttrs{Type: Private})
if err != nil {
t.Fatal(err)
}
const readers = 20000
for i := 0; i < readers; i++ {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
r := bucket.Object("obj").NewReader(ctx)
_, _ = r.Read(make([]byte, 10))
r.Close()
cancel()
}
time.Sleep(500 * time.Millisecond)
buf := make([]byte, 1<<24)
if n := strings.Count(string(buf[:runtime.Stack(buf, true)]), "(*Reader).curChunk.func1"); n > 0 {
t.Fatalf("goroutine leak: %d curChunk waiter goroutine(s) still parked after %d closed readers", n, readers)
}
}
On v0.8.0 / master, go test -race -run TestReaderCurChunkWaitersDoNotLeak ./b2/:
--- FAIL: TestReaderCurChunkWaitersDoNotLeak (1.33s)
reader_wakeup_test.go:61: goroutine leak: 2 curChunk waiter goroutine(s) still parked after 20000 closed readers
(2-3 per 20,000 readers in our runs. Without -race it did not reproduce in 100,000 iterations.)
Expected vs. actual
- Expected: once a reader is closed or has failed, no goroutine of its is left waiting.
- Actual: occasionally a
curChunkwaiter stays blocked inWaitforever.
Impact
A slow goroutine leak in programs that create many readers and see download errors or cancellations. No wrong data and no hung caller.
Proposed fix
Broadcast while holding rmux, so a wakeup cannot fall between the waiter's check and its Wait, and broadcast from Close as well. The diff below is written on top of the #57 fix (it also rewrites the Broadcast that #57 adds); on master alone it is the same change minus that one line.
Proposed diff (b2/reader.go, on top of #57)
diff --git b2/reader.go b2/reader.go
index 74a0111..e689677 100644
--- b2/reader.go
+++ b2/reader.go
@@ -81,4 +81,7 @@ type rchunk struct {
func (r *Reader) Close() error {
r.cancel()
+ if r.rcond != nil {
+ r.broadcast()
+ }
r.o.b.c.removeReader(r)
return nil
@@ -102,4 +105,12 @@ func (r *Reader) setErrNoCancel(err error) {
}
+// broadcast wakes curChunk waiters. It takes rmux so the wakeup cannot land
+// between a waiter's predicate check and its registration in Wait.
+func (r *Reader) broadcast() {
+ r.rmux.Lock()
+ r.rcond.Broadcast()
+ r.rmux.Unlock()
+}
+
func (r *Reader) getErr() error {
r.emux.RLock()
@@ -145,10 +156,10 @@ func (r *Reader) thread() {
r.chunks[chunkID] = buf
r.rmux.Unlock()
- r.rcond.Broadcast()
+ r.broadcast()
return
}
if err != nil {
r.setErr(err)
- r.rcond.Broadcast()
+ r.broadcast()
return
}
@@ -173,5 +184,5 @@ func (r *Reader) thread() {
if attempts >= maxShortReadAttempts {
r.setErr(fmt.Errorf("b2 reader %d: got %dB of %dB after %d attempts", chunkID, i, rsize, attempts))
- r.rcond.Broadcast()
+ r.broadcast()
return
}
@@ -179,5 +190,5 @@ func (r *Reader) thread() {
if err := b.wait(r.ctx); err != nil {
r.setErr(err)
- r.rcond.Broadcast()
+ r.broadcast()
return
}
@@ -187,5 +198,5 @@ func (r *Reader) thread() {
if err != nil {
r.setErr(err)
- r.rcond.Broadcast()
+ r.broadcast()
return
}
@@ -193,5 +204,5 @@ func (r *Reader) thread() {
r.chunks[chunkID] = buf
r.rmux.Unlock()
- r.rcond.Broadcast()
+ r.broadcast()
}
}()
rcond is created lazily in initFunc, so Close checks for nil (a reader closed before its first Read).
Validated on a copy of master with the #57 and #59 changes applied: the regression test passes under -race (three runs), go test ./... passes, and go test -race ./b2/ passes with no data-race reports.
Related
- #57 edits the same retry block; apply this change after it.
- Lenguaje dominante
- Go
- Estrellas
- 66
- Forks
- 18
- Merge medio
- 1 d 10 h
- PR fusionados (30 d)
- 132
Preparar el entorno
- Sin Dockerfile ni archivo de Docker Compose
- Tiene una plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de Backblaze/blazer
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 70/100
Los mantenedores suelen responder en 1 día
-
bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
Los mantenedores suelen responder en 1 día
-
Bucket.Attrs never returns ReplicationConfig: replication configuration is write-onlyQuizá libre de nuevo Un pull request para esta issue se cerró sin fusionarse. Abiertobug
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
Los mantenedores suelen responder en 1 día
-
bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
Los mantenedores suelen responder en 1 día
-
test: skip the fuzz non-vacuity guard when replaying a single seedPosiblemente ocupada @ffumero2003 la tomó hace 5 días. Abiertotesting
Backblaze/blazer#330 · 1 asignado ·
Los mantenedores suelen responder en 1 día
Todos los issues de Backblaze/blazer
Issues similares
-
needs-triage typo
Dificultad 1/5 Menos de una hora Aptitud para principiantes 88/100
llm-d-incubation/llm-d-scaling-manager#122 ·
Los mantenedores suelen responder en 1 día
-
good first issue severity: low tech-debt
Dificultad 2/5 Medio día Aptitud para principiantes 75/100
TMHSDigital/subenum#329 ·
Los mantenedores suelen responder en 1 día
-
[Bug] Backend summarize min() / max() panics when a field has no numeric valuesPosiblemente ocupada @weng5e la tomó hoy. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 64/100
grafana/grafana-infinity-datasource#1832 ·
Los mantenedores suelen responder en 1 día
-
tech-debt
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 62/100
GillMacedo/godebugdiff#13 ·