mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-25 01:14:50 +00:00
Cut multi-paragraph headers and what-the-code-does narration across the Phase 3 rate-limit plumbing. Kept the small WHY notes (rate.Limiter needs burst>=1, base ClusterContext is shared across parallel jobs, contract sync between admin and worker constant pairs). Same code, 139 fewer lines of comment.
156 lines
6.4 KiB
Go
156 lines
6.4 KiB
Go
package plugin
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/plugin_pb"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// pluginWithExecutors builds a Plugin with n execute-capable workers
|
|
// for jobType, bypassing UpsertFromHello.
|
|
func pluginWithExecutors(t *testing.T, jobType string, n int) *Plugin {
|
|
t.Helper()
|
|
reg := NewRegistry()
|
|
now := time.Now()
|
|
for i := 0; i < n; i++ {
|
|
id := "worker-" + string(rune('a'+i))
|
|
reg.sessions[id] = &WorkerSession{
|
|
WorkerID: id,
|
|
LastSeenAt: now,
|
|
ConnectedAt: now,
|
|
Capabilities: map[string]*plugin_pb.JobTypeCapability{
|
|
jobType: {CanExecute: true},
|
|
},
|
|
}
|
|
}
|
|
return &Plugin{registry: reg}
|
|
}
|
|
|
|
func adminConfig(pairs ...interface{}) map[string]*plugin_pb.ConfigValue {
|
|
if len(pairs)%2 != 0 {
|
|
panic("adminConfig expects key/value pairs")
|
|
}
|
|
out := map[string]*plugin_pb.ConfigValue{}
|
|
for i := 0; i < len(pairs); i += 2 {
|
|
key := pairs[i].(string)
|
|
switch v := pairs[i+1].(type) {
|
|
case int:
|
|
out[key] = &plugin_pb.ConfigValue{Kind: &plugin_pb.ConfigValue_Int64Value{Int64Value: int64(v)}}
|
|
case int64:
|
|
out[key] = &plugin_pb.ConfigValue{Kind: &plugin_pb.ConfigValue_Int64Value{Int64Value: v}}
|
|
case float64:
|
|
out[key] = &plugin_pb.ConfigValue{Kind: &plugin_pb.ConfigValue_DoubleValue{DoubleValue: v}}
|
|
default:
|
|
panic("adminConfig: unsupported value type")
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestDecorateClusterContext_NonS3LifecycleIsPassThrough(t *testing.T) {
|
|
r := pluginWithExecutors(t, s3LifecycleJobType, 4)
|
|
in := &plugin_pb.ClusterContext{Metadata: map[string]string{"unrelated": "v"}}
|
|
out := r.decorateClusterContextForJob(in, "some_other_job", adminConfig(s3LifecycleClusterDeletesPerSecondKey, 100), 1)
|
|
assert.Same(t, in, out, "non-allocator job type must return the same pointer")
|
|
}
|
|
|
|
func TestDecorateClusterContext_RpsZeroSkipsAllocation(t *testing.T) {
|
|
// rps=0 must NOT write "0" — worker would read it as zero-throughput.
|
|
r := pluginWithExecutors(t, s3LifecycleJobType, 4)
|
|
in := &plugin_pb.ClusterContext{}
|
|
out := r.decorateClusterContextForJob(in, s3LifecycleJobType, adminConfig(s3LifecycleClusterDeletesPerSecondKey, 0), 1)
|
|
if out.Metadata != nil {
|
|
_, hasRps := out.Metadata[s3LifecycleMetadataDeletesPerSecond]
|
|
assert.False(t, hasRps, "rps=0 must not write a deletes_per_second key")
|
|
}
|
|
}
|
|
|
|
func TestDecorateClusterContext_NoExecutorsSkipsAllocation(t *testing.T) {
|
|
r := pluginWithExecutors(t, s3LifecycleJobType, 0)
|
|
in := &plugin_pb.ClusterContext{}
|
|
out := r.decorateClusterContextForJob(in, s3LifecycleJobType, adminConfig(s3LifecycleClusterDeletesPerSecondKey, 100), 1)
|
|
if out.Metadata != nil {
|
|
_, hasRps := out.Metadata[s3LifecycleMetadataDeletesPerSecond]
|
|
assert.False(t, hasRps, "0 executors must not write share metadata (would divide by zero)")
|
|
}
|
|
}
|
|
|
|
func TestDecorateClusterContext_SingletonJobGetsFullBudget(t *testing.T) {
|
|
// maxJobs=1: budget must go undivided to the single active worker.
|
|
r := pluginWithExecutors(t, s3LifecycleJobType, 4)
|
|
in := &plugin_pb.ClusterContext{}
|
|
out := r.decorateClusterContextForJob(in, s3LifecycleJobType,
|
|
adminConfig(s3LifecycleClusterDeletesPerSecondKey, 100), 1)
|
|
require.NotNil(t, out.Metadata)
|
|
assert.Equal(t, "100", out.Metadata[s3LifecycleMetadataDeletesPerSecond])
|
|
}
|
|
|
|
func TestDecorateClusterContext_SharedEvenlyWhenParallelLimited(t *testing.T) {
|
|
r := pluginWithExecutors(t, s3LifecycleJobType, 4)
|
|
in := &plugin_pb.ClusterContext{}
|
|
out := r.decorateClusterContextForJob(in, s3LifecycleJobType,
|
|
adminConfig(s3LifecycleClusterDeletesPerSecondKey, 100), 4)
|
|
require.NotNil(t, out.Metadata)
|
|
assert.Equal(t, "25", out.Metadata[s3LifecycleMetadataDeletesPerSecond])
|
|
}
|
|
|
|
func TestDecorateClusterContext_MaxJobsExceedsExecutors(t *testing.T) {
|
|
// divisor = min(executors=4, maxJobs=10).
|
|
r := pluginWithExecutors(t, s3LifecycleJobType, 4)
|
|
in := &plugin_pb.ClusterContext{}
|
|
out := r.decorateClusterContextForJob(in, s3LifecycleJobType,
|
|
adminConfig(s3LifecycleClusterDeletesPerSecondKey, 100), 10)
|
|
require.NotNil(t, out.Metadata)
|
|
assert.Equal(t, "25", out.Metadata[s3LifecycleMetadataDeletesPerSecond])
|
|
}
|
|
|
|
func TestDecorateClusterContext_BurstSharedWhenParallel(t *testing.T) {
|
|
r := pluginWithExecutors(t, s3LifecycleJobType, 2)
|
|
in := &plugin_pb.ClusterContext{}
|
|
out := r.decorateClusterContextForJob(in, s3LifecycleJobType,
|
|
adminConfig(s3LifecycleClusterDeletesPerSecondKey, 100, s3LifecycleClusterDeletesBurstKey, 20), 2)
|
|
require.NotNil(t, out.Metadata)
|
|
assert.Equal(t, "50", out.Metadata[s3LifecycleMetadataDeletesPerSecond])
|
|
assert.Equal(t, "10", out.Metadata[s3LifecycleMetadataDeletesBurst])
|
|
}
|
|
|
|
func TestDecorateClusterContext_BurstZeroOmitsKey(t *testing.T) {
|
|
// burst=0 means "let the worker default to 2*rps" — omit the key.
|
|
r := pluginWithExecutors(t, s3LifecycleJobType, 4)
|
|
in := &plugin_pb.ClusterContext{}
|
|
out := r.decorateClusterContextForJob(in, s3LifecycleJobType,
|
|
adminConfig(s3LifecycleClusterDeletesPerSecondKey, 100, s3LifecycleClusterDeletesBurstKey, 0), 4)
|
|
_, hasBurst := out.Metadata[s3LifecycleMetadataDeletesBurst]
|
|
assert.False(t, hasBurst, "burst=0 must NOT write the burst key (worker default kicks in)")
|
|
}
|
|
|
|
func TestDecorateClusterContext_BurstFloorIsOneWhenDividesBelowOne(t *testing.T) {
|
|
// rate.Limiter with burst<1 never refills.
|
|
r := pluginWithExecutors(t, s3LifecycleJobType, 4)
|
|
in := &plugin_pb.ClusterContext{}
|
|
out := r.decorateClusterContextForJob(in, s3LifecycleJobType,
|
|
adminConfig(s3LifecycleClusterDeletesPerSecondKey, 100, s3LifecycleClusterDeletesBurstKey, 1), 4)
|
|
assert.Equal(t, "1", out.Metadata[s3LifecycleMetadataDeletesBurst])
|
|
}
|
|
|
|
func TestDecorateClusterContext_DoesNotMutateInput(t *testing.T) {
|
|
// Base ClusterContext is shared across parallel ExecuteJob calls.
|
|
r := pluginWithExecutors(t, s3LifecycleJobType, 4)
|
|
baseMeta := map[string]string{"existing": "value"}
|
|
in := &plugin_pb.ClusterContext{Metadata: baseMeta}
|
|
_ = r.decorateClusterContextForJob(in, s3LifecycleJobType,
|
|
adminConfig(s3LifecycleClusterDeletesPerSecondKey, 100), 1)
|
|
_, leaked := baseMeta[s3LifecycleMetadataDeletesPerSecond]
|
|
assert.False(t, leaked, "decorator must not mutate the input metadata map")
|
|
assert.Equal(t, "value", baseMeta["existing"])
|
|
}
|
|
|
|
func TestDecorateClusterContext_NilInputPassesThrough(t *testing.T) {
|
|
r := pluginWithExecutors(t, s3LifecycleJobType, 4)
|
|
out := r.decorateClusterContextForJob(nil, s3LifecycleJobType, adminConfig(s3LifecycleClusterDeletesPerSecondKey, 100), 1)
|
|
assert.Nil(t, out)
|
|
}
|