s3 lifecycle: count filer entries a walk reads but skips as progress (#12658)

* dailyrun: count entries read from the filer, skipped ones included

A versioned bucket's pre-pass and a resumed walk read entries without
emitting them, so a walk can read for hours with no visited entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* s3 lifecycle: count listed entries as progress work

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chris LuandClaude Opus 5.5 authored and GitHub committed 2026-11-14 11:12:33 -08:00
1 parent 53377bcb5d
commit fd4603d657
5 files changed
+75 -7

No files matched your search

+45 -1
View File
@@ -1,10 +1,14 @@
package dailyrun
import (
"context"
"strings"
"sync"
"sync/atomic"
"google.golang.org/grpc"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/stats"
)
@@ -46,7 +50,10 @@ type RunSummary struct {
EntriesVisited int64
// Distinct object keys among the listed entries.
ObjectsVisited int64
EventsScanned int64
// Every entry read from the filer, including the ones a walk skips
// without visiting (versioned pre-pass, resume start, other shards).
EntriesListed int64
EventsScanned int64
// LifecycleDelete outcomes keyed by the dispatch_total outcome label.
// They count RPC results, not reclaimed space.
Outcomes map[string]int64
@@ -63,6 +70,7 @@ type RunSummary struct {
type RunStats struct {
entries atomic.Int64
objects atomic.Int64
listed atomic.Int64
events atomic.Int64
mu sync.Mutex
@@ -123,6 +131,41 @@ func (s *RunStats) visitEntry(newObject bool) {
}
}
// CountListed returns client with its ListEntries results counted as
// EntriesListed.
func (s *RunStats) CountListed(client filer_pb.SeaweedFilerClient) filer_pb.SeaweedFilerClient {
if s == nil || client == nil {
return client
}
return &listCountingClient{SeaweedFilerClient: client, stats: s}
}
type listCountingClient struct {
filer_pb.SeaweedFilerClient
stats *RunStats
}
func (c *listCountingClient) ListEntries(ctx context.Context, in *filer_pb.ListEntriesRequest, opts ...grpc.CallOption) (filer_pb.SeaweedFiler_ListEntriesClient, error) {
stream, err := c.SeaweedFilerClient.ListEntries(ctx, in, opts...)
if err != nil {
return nil, err
}
return &listCountingStream{SeaweedFiler_ListEntriesClient: stream, stats: c.stats}, nil
}
type listCountingStream struct {
filer_pb.SeaweedFiler_ListEntriesClient
stats *RunStats
}
func (s *listCountingStream) Recv() (*filer_pb.ListEntriesResponse, error) {
resp, err := s.SeaweedFiler_ListEntriesClient.Recv()
if err == nil && resp.GetEntry() != nil {
s.stats.listed.Add(1)
}
return resp, err
}
func (s *RunStats) scanEvent() {
if s == nil {
return
@@ -176,6 +219,7 @@ func (s *RunStats) Summary() RunSummary {
out := RunSummary{
EntriesVisited: s.entries.Load(),
ObjectsVisited: s.objects.Load(),
EntriesListed: s.listed.Load(),
EventsScanned: s.events.Load(),
}
s.mu.Lock()
@@ -7,6 +7,7 @@ import (
"testing"
"time"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/pb/s3_lifecycle_pb"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3lifecycle"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3lifecycle/bootstrap"
@@ -167,3 +168,24 @@ func TestRunStats_NilIsANoOp(t *testing.T) {
runStats.setShardErrors(1)
assert.Equal(t, RunSummary{}, runStats.Summary())
}
// A walk can read a bucket without visiting anything: the versioned
// pre-pass reads plain files without emitting them, and a resumed walk
// skips every key up to its start.
func TestRunStats_CountListedCountsSkippedEntries(t *testing.T) {
mtime := time.Now().Add(-7 * 24 * time.Hour)
client := &fakeFiler{tree: map[string][]*filer_pb.Entry{
"/buckets/bkt": {file("a.txt", mtime, 1), file("b.txt", mtime, 1), file("c.txt", mtime, 1)},
}}
stats := &RunStats{}
emitted := 0
require.NoError(t, FilerListFunc(stats.CountListed(client), "/buckets", nil)(context.Background(), "bkt", "c.txt", func(*bootstrap.Entry) error {
emitted++
return nil
}))
assert.Zero(t, emitted)
assert.Equal(t, int64(6), stats.Summary().EntriesListed, "both passes over the versioned bucket")
var nilStats *RunStats
assert.Same(t, client, nilStats.CountListed(client))
}