mirror of
https://github.com/pgsty/minio.git
synced 2026-11-10 01:31:52 +03:00
8cc2360c3a
Signed-off-by: Feng Ruohang <rh@vonng.com>
249 lines
7.6 KiB
Go
249 lines
7.6 KiB
Go
// Copyright (c) 2026 PGSTY
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"net/http"
|
|
"testing"
|
|
"testing/synctest"
|
|
"time"
|
|
)
|
|
|
|
type batchDiscoveryLayer struct {
|
|
ObjectLayer
|
|
walk func(context.Context, chan<- itemOrErr[ObjectInfo]) error
|
|
read func(context.Context, string) ([]byte, error)
|
|
}
|
|
|
|
func (a batchDiscoveryLayer) Walk(ctx context.Context, _, _ string, results chan<- itemOrErr[ObjectInfo], _ WalkOptions) error {
|
|
return a.walk(ctx, results)
|
|
}
|
|
|
|
func (a batchDiscoveryLayer) GetObjectNInfo(ctx context.Context, _, object string, _ *HTTPRangeSpec, _ http.Header, opts ObjectOptions) (*GetObjectReader, error) {
|
|
data, err := a.read(ctx, object)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return NewGetObjectReaderFromReader(bytes.NewReader(data), ObjectInfo{Name: object}, opts)
|
|
}
|
|
|
|
// A partially successful startup scan must retry the missing work without
|
|
// re-admitting work that finished while the scan was waiting to retry.
|
|
func TestBatchDiscoveryRecoversInProcess(t *testing.T) {
|
|
for _, failure := range []string{"walk-start", "walk-stream", "manifest-read"} {
|
|
t.Run(failure, func(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
defer cancel()
|
|
pool := &BatchJobPool{ctx: ctx, jobCh: make(chan *BatchJobRequest, 10)}
|
|
attempts := 0
|
|
pool.objLayer = batchDiscoveryLayer{
|
|
walk: func(_ context.Context, results chan<- itemOrErr[ObjectInfo]) error {
|
|
attempts++
|
|
if attempts == 1 && failure == "walk-start" {
|
|
return errErasureReadQuorum
|
|
}
|
|
defer close(results)
|
|
results <- itemOrErr[ObjectInfo]{Item: ObjectInfo{Name: "finished"}}
|
|
if attempts == 1 && failure == "walk-stream" {
|
|
results <- itemOrErr[ObjectInfo]{Err: errErasureReadQuorum}
|
|
} else {
|
|
results <- itemOrErr[ObjectInfo]{Item: ObjectInfo{Name: "pending"}}
|
|
}
|
|
return nil
|
|
},
|
|
read: func(_ context.Context, name string) ([]byte, error) {
|
|
if attempts == 1 && failure == "manifest-read" && name == "pending" {
|
|
return nil, errErasureReadQuorum
|
|
}
|
|
job := BatchJobRequest{ID: name}
|
|
return job.MarshalMsg(nil)
|
|
},
|
|
}
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
pool.resume(func() time.Duration { return 0 })
|
|
}()
|
|
synctest.Wait()
|
|
if failure != "walk-start" {
|
|
select {
|
|
case job := <-pool.jobCh:
|
|
if job.ID != "finished" || !pool.beginJob(job) {
|
|
t.Fatal("first scan did not admit the healthy manifest")
|
|
}
|
|
pool.finishJob(job, nil)
|
|
default:
|
|
t.Fatal("first scan lost the healthy manifest")
|
|
}
|
|
}
|
|
select {
|
|
case <-done:
|
|
case <-time.After(20 * time.Minute):
|
|
t.Fatal("discovery failed to finish after storage recovered")
|
|
}
|
|
if attempts != 2 {
|
|
t.Fatalf("discovery attempts = %d, want 2", attempts)
|
|
}
|
|
want := 1
|
|
if failure == "walk-start" {
|
|
want = 2
|
|
}
|
|
if len(pool.jobCh) != want {
|
|
t.Fatalf("queued %d jobs, want %d; completed work was re-admitted or pending work was lost", len(pool.jobCh), want)
|
|
}
|
|
for len(pool.jobCh) > 0 {
|
|
job := <-pool.jobCh
|
|
if failure != "walk-start" && job.ID != "pending" {
|
|
t.Fatalf("re-admitted %q", job.ID)
|
|
}
|
|
if !pool.beginJob(job) {
|
|
t.Fatal("recovered job has no owner")
|
|
}
|
|
pool.finishJob(job, nil)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBatchDiscoverySkipsUnusableManifests(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
pool := &BatchJobPool{ctx: t.Context(), jobCh: make(chan *BatchJobRequest, 1)}
|
|
attempts := 0
|
|
pool.objLayer = batchDiscoveryLayer{
|
|
walk: func(_ context.Context, results chan<- itemOrErr[ObjectInfo]) error {
|
|
attempts++
|
|
defer close(results)
|
|
for _, name := range []string{"canceled", "deleted", "malformed", "future", "empty-id"} {
|
|
results <- itemOrErr[ObjectInfo]{Item: ObjectInfo{Name: name}}
|
|
}
|
|
return nil
|
|
},
|
|
read: func(_ context.Context, name string) ([]byte, error) {
|
|
switch name {
|
|
case "deleted":
|
|
return nil, errConfigNotFound
|
|
case "malformed":
|
|
return []byte("bad manifest"), nil
|
|
case "future":
|
|
return []byte("SILO-BATCH-V999\n"), nil
|
|
case "empty-id":
|
|
return (&BatchJobRequest{}).MarshalMsg(nil)
|
|
}
|
|
// The administrator cancels after Walk, during manifest load.
|
|
mustIAM(t, pool.canceler(name, true))
|
|
job := BatchJobRequest{ID: name}
|
|
return job.MarshalMsg(nil)
|
|
},
|
|
}
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
pool.resume(func() time.Duration { return 0 })
|
|
}()
|
|
select {
|
|
case <-done:
|
|
case <-time.After(time.Hour):
|
|
t.Fatal("unusable manifests kept discovery retrying")
|
|
}
|
|
if len(pool.jobCh) != 0 || attempts != 1 {
|
|
t.Fatalf("canceled work was queued or retried: queue=%d scans=%d", len(pool.jobCh), attempts)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestBatchDiscoveryShutdownDuringBackoff(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
defer cancel()
|
|
attempts := 0
|
|
pool := &BatchJobPool{ctx: ctx, objLayer: batchDiscoveryLayer{
|
|
walk: func(context.Context, chan<- itemOrErr[ObjectInfo]) error {
|
|
attempts++
|
|
return errErasureReadQuorum
|
|
},
|
|
}}
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
pool.resume(func() time.Duration { return 0 })
|
|
}()
|
|
synctest.Wait()
|
|
select {
|
|
case <-done:
|
|
t.Fatal("discovery abandoned a transient startup error")
|
|
default:
|
|
}
|
|
cancel()
|
|
select {
|
|
case <-done:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("discovery did not stop on shutdown")
|
|
}
|
|
if attempts != 1 {
|
|
t.Fatalf("discovery scanned after shutdown: %d", attempts)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestBatchDiscoveryKeepsNewlyFinishedJobsTerminal(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
pool := &BatchJobPool{ctx: t.Context(), jobCh: make(chan *BatchJobRequest, 1)}
|
|
attempts := 0
|
|
pool.objLayer = batchDiscoveryLayer{
|
|
walk: func(_ context.Context, results chan<- itemOrErr[ObjectInfo]) error {
|
|
attempts++
|
|
if attempts == 1 {
|
|
// This job was submitted and failed while startup discovery
|
|
// had not yet read any manifests. The retry must not rerun it.
|
|
job := &BatchJobRequest{ID: "new-job"}
|
|
mustIAM(t, pool.queueJob(job))
|
|
if !pool.beginJob(<-pool.jobCh) {
|
|
t.Fatal("submitted job has no owner")
|
|
}
|
|
pool.finishJob(job, errErasureWriteQuorum)
|
|
return errErasureReadQuorum
|
|
}
|
|
defer close(results)
|
|
if owner := pool.jobOwners["new-job"]; owner == nil || owner.state != "terminal" || owner.req != nil || owner.cancel != nil {
|
|
t.Fatalf("terminal marker lost or retained the job payload: %+v", owner)
|
|
}
|
|
results <- itemOrErr[ObjectInfo]{Item: ObjectInfo{Name: "new-job"}}
|
|
return nil
|
|
},
|
|
read: func(_ context.Context, name string) ([]byte, error) {
|
|
return (&BatchJobRequest{ID: name}).MarshalMsg(nil)
|
|
},
|
|
}
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
pool.resume(func() time.Duration { return 0 })
|
|
}()
|
|
select {
|
|
case <-done:
|
|
case <-time.After(time.Hour):
|
|
t.Fatal("discovery did not recover")
|
|
}
|
|
if attempts != 2 || len(pool.jobCh) != 0 {
|
|
t.Fatalf("discovery re-admitted a failed job: scans=%d queue=%d", attempts, len(pool.jobCh))
|
|
}
|
|
if !pool.discoveryDone || len(pool.jobOwners) != 0 {
|
|
t.Fatal("completed discovery retained terminal job markers")
|
|
}
|
|
job := &BatchJobRequest{ID: "after-discovery"}
|
|
mustIAM(t, pool.queueJob(job))
|
|
if !pool.beginJob(<-pool.jobCh) {
|
|
t.Fatal("new submission after discovery has no owner")
|
|
}
|
|
pool.finishJob(job, nil)
|
|
if len(pool.jobOwners) != 0 {
|
|
t.Fatal("normal completion after discovery leaked an owner")
|
|
}
|
|
})
|
|
}
|