mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-22 05:54:24 +02:00
* feat(s3/lifecycle): scheduler — N pipelines over an even shard split
Scheduler.Run spawns Workers Pipeline goroutines plus one engine-refresh
ticker. Each worker owns a contiguous AssignShards(idx, total) slice of
[0, ShardCount) and runs Pipeline.Run with EventBudget bounding each
iteration; brief RetryBackoff between iterations avoids hot-loop on
errors. The refresh ticker rebuilds the engine snapshot from the filer's
bucket configs every RefreshInterval.
LoadCompileInputs / IsBucketVersioned / AllActivePriorStates are
exported from a configload.go sibling so the shell command can move to
this shared implementation in a follow-up.
* refactor(shell): reuse scheduler.LoadCompileInputs in run-shard
Drop the local copies of loadLifecycleCompileInputs / isBucketVersioned
/ allActivePriorStates / lifecycleParseError that the new
scheduler package now exports. Same behavior, one source of truth.
* feat(worker/s3_lifecycle): plugin handler with admin UI config
Registers a JobHandler for s3_lifecycle via pluginworker.RegisterHandler.
Admin pulls the descriptor over the worker plugin gRPC and renders the
AdminConfigForm + WorkerConfigForm in the existing UI:
Admin form (cluster shape):
- workers (1..16, default 1)
- s3_grpc_endpoints (comma list)
Worker form (operational tuning):
- dispatch_tick_ms (default 5000)
- checkpoint_tick_ms (default 30000)
- refresh_interval_ms (default 300000)
- event_budget (default 0 = unbounded)
Detect emits a single proposal whenever S3 endpoints + filer addresses
are configured. MaxExecutionConcurrency=1 so admin only ever runs one
lifecycle daemon per worker; a fresh proposal next cycle restarts it
if the prior Execute exits.
Execute dials the configured S3 endpoint + filer, builds a
scheduler.Scheduler with the parsed config, and runs it until
ctx cancellation. Reuses the existing scheduler / dispatcher /
reader / engine packages — the handler is the thin glue that
parses descriptor values and wires the long-running daemon.
* proto(plugin): add s3_grpc_addresses to ClusterContext
So workers can dial s3 servers discovered by the master rather than a
hand-typed list in the admin form.
* feat(admin): populate ClusterContext.s3_grpc_addresses from master
ListClusterNodes(S3Type) returns the live S3 servers; the plugin
scheduler now hands these to job handlers alongside filer/volume
addresses.
* feat(worker/s3_lifecycle): discover s3 endpoints from cluster context
Drop the s3_grpc_endpoints admin form field and read the master-supplied
ClusterContext.S3GrpcAddresses instead. Operators no longer maintain a
hand-typed list, and a stale entry self-heals when the master's view
updates.
* feat(worker/s3_lifecycle): time-based runtime cap, friendlier cadence units
- dispatch_tick_minutes (was *_ms): minutes is the natural granularity
for a daily batch; default 1 minute.
- checkpoint_tick_seconds: seconds for the durable cursor write; default
30 seconds.
- refresh_interval_minutes: minutes for the engine snapshot rebuild.
- max_runtime_minutes replaces event_budget. Each daily run is bounded
by wall clock — typical run wraps in well under an hour because the
cursor persists and the meta-log streams fast. Default 60 minutes.
- AdminRuntimeDefaults.DetectionIntervalSeconds = 86400 so the admin
schedules one job per day.
87 lines
2.3 KiB
Go
87 lines
2.3 KiB
Go
package scheduler
|
|
|
|
import (
|
|
"reflect"
|
|
"testing"
|
|
)
|
|
|
|
func TestAssignShardsSingleWorker(t *testing.T) {
|
|
got := AssignShards(0, 1)
|
|
want := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("got %v, want %v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestAssignShardsEvenSplit(t *testing.T) {
|
|
a := AssignShards(0, 2)
|
|
b := AssignShards(1, 2)
|
|
wantA := []int{0, 1, 2, 3, 4, 5, 6, 7}
|
|
wantB := []int{8, 9, 10, 11, 12, 13, 14, 15}
|
|
if !reflect.DeepEqual(a, wantA) {
|
|
t.Fatalf("worker 0: got %v, want %v", a, wantA)
|
|
}
|
|
if !reflect.DeepEqual(b, wantB) {
|
|
t.Fatalf("worker 1: got %v, want %v", b, wantB)
|
|
}
|
|
}
|
|
|
|
func TestAssignShardsUnevenSplit(t *testing.T) {
|
|
// 16 shards / 3 workers = 5,5,5 + 1 remainder. Earlier workers absorb
|
|
// the remainder so the slices stay contiguous: 6,5,5.
|
|
w0 := AssignShards(0, 3)
|
|
w1 := AssignShards(1, 3)
|
|
w2 := AssignShards(2, 3)
|
|
if len(w0) != 6 || w0[0] != 0 || w0[len(w0)-1] != 5 {
|
|
t.Fatalf("w0 expected [0..5], got %v", w0)
|
|
}
|
|
if len(w1) != 5 || w1[0] != 6 || w1[len(w1)-1] != 10 {
|
|
t.Fatalf("w1 expected [6..10], got %v", w1)
|
|
}
|
|
if len(w2) != 5 || w2[0] != 11 || w2[len(w2)-1] != 15 {
|
|
t.Fatalf("w2 expected [11..15], got %v", w2)
|
|
}
|
|
// All shards covered, exactly once.
|
|
covered := map[int]int{}
|
|
for _, w := range [][]int{w0, w1, w2} {
|
|
for _, s := range w {
|
|
covered[s]++
|
|
}
|
|
}
|
|
if len(covered) != 16 {
|
|
t.Fatalf("expected 16 unique shards, got %d", len(covered))
|
|
}
|
|
for s, c := range covered {
|
|
if c != 1 {
|
|
t.Fatalf("shard %d covered %d times, expected 1", s, c)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAssignShardsMoreWorkersThanShards(t *testing.T) {
|
|
// 20 workers, 16 shards: first 16 get one shard each, last 4 get nothing.
|
|
for i := 0; i < 16; i++ {
|
|
got := AssignShards(i, 20)
|
|
if len(got) != 1 || got[0] != i {
|
|
t.Fatalf("worker %d: got %v, want [%d]", i, got, i)
|
|
}
|
|
}
|
|
for i := 16; i < 20; i++ {
|
|
if got := AssignShards(i, 20); got != nil {
|
|
t.Fatalf("worker %d should get nil, got %v", i, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAssignShardsBounds(t *testing.T) {
|
|
if got := AssignShards(-1, 4); got != nil {
|
|
t.Fatalf("idx -1: got %v, want nil", got)
|
|
}
|
|
if got := AssignShards(4, 4); got != nil {
|
|
t.Fatalf("idx==total: got %v, want nil", got)
|
|
}
|
|
if got := AssignShards(0, 0); got != nil {
|
|
t.Fatalf("total 0: got %v, want nil", got)
|
|
}
|
|
}
|