diff --git a/weed/plugin/worker/admin_script_handler.go b/weed/plugin/worker/admin_script_handler.go index 0c0979943..9c4717be3 100644 --- a/weed/plugin/worker/admin_script_handler.go +++ b/weed/plugin/worker/admin_script_handler.go @@ -21,6 +21,8 @@ import ( const ( adminScriptJobType = "admin_script" maxAdminScriptOutputBytes = 16 * 1024 + defaultAdminScriptRunMins = 17 + adminScriptDetectTickSecs = 60 ) const defaultAdminScript = `volume.balance -apply @@ -77,6 +79,15 @@ func (h *AdminScriptHandler) Descriptor() *plugin_pb.JobTypeDescriptor { Widget: plugin_pb.ConfigWidget_CONFIG_WIDGET_TEXTAREA, Required: true, }, + { + Name: "run_interval_minutes", + Label: "Run Interval (minutes)", + Description: "Minimum interval between successful admin script runs.", + FieldType: plugin_pb.ConfigFieldType_CONFIG_FIELD_TYPE_INT64, + Widget: plugin_pb.ConfigWidget_CONFIG_WIDGET_NUMBER, + Required: true, + MinValue: &plugin_pb.ConfigValue{Kind: &plugin_pb.ConfigValue_Int64Value{Int64Value: 1}}, + }, }, }, }, @@ -84,11 +95,14 @@ func (h *AdminScriptHandler) Descriptor() *plugin_pb.JobTypeDescriptor { "script": { Kind: &plugin_pb.ConfigValue_StringValue{StringValue: defaultAdminScript}, }, + "run_interval_minutes": { + Kind: &plugin_pb.ConfigValue_Int64Value{Int64Value: defaultAdminScriptRunMins}, + }, }, }, AdminRuntimeDefaults: &plugin_pb.AdminRuntimeDefaults{ Enabled: false, - DetectionIntervalSeconds: 24 * 60 * 60, + DetectionIntervalSeconds: adminScriptDetectTickSecs, DetectionTimeoutSeconds: 300, MaxJobsPerDetection: 1, GlobalExecutionConcurrency: 1, @@ -113,6 +127,31 @@ func (h *AdminScriptHandler) Detect(ctx context.Context, request *plugin_pb.RunD script := normalizeAdminScript(readStringConfig(request.GetAdminConfigValues(), "script", "")) scriptName := strings.TrimSpace(readStringConfig(request.GetAdminConfigValues(), "script_name", "")) + runIntervalMinutes := readAdminScriptRunIntervalMinutes(request.GetAdminConfigValues()) + if shouldSkipDetectionByInterval(request.GetLastSuccessfulRun(), runIntervalMinutes*60) { + _ = sender.SendActivity(buildDetectorActivity( + "skipped_by_interval", + fmt.Sprintf("ADMIN SCRIPT: Detection skipped due to run interval (%dm)", runIntervalMinutes), + map[string]*plugin_pb.ConfigValue{ + "run_interval_minutes": { + Kind: &plugin_pb.ConfigValue_Int64Value{Int64Value: int64(runIntervalMinutes)}, + }, + }, + )) + if err := sender.SendProposals(&plugin_pb.DetectionProposals{ + JobType: adminScriptJobType, + Proposals: []*plugin_pb.JobProposal{}, + HasMore: false, + }); err != nil { + return err + } + return sender.SendComplete(&plugin_pb.DetectionComplete{ + JobType: adminScriptJobType, + Success: true, + TotalProposals: 0, + }) + } + commands := parseAdminScriptCommands(script) execCount := countExecutableCommands(commands) if execCount == 0 { @@ -346,6 +385,14 @@ func (h *AdminScriptHandler) Execute(ctx context.Context, request *plugin_pb.Exe }) } +func readAdminScriptRunIntervalMinutes(values map[string]*plugin_pb.ConfigValue) int { + runIntervalMinutes := int(readInt64Config(values, "run_interval_minutes", defaultAdminScriptRunMins)) + if runIntervalMinutes <= 0 { + return defaultAdminScriptRunMins + } + return runIntervalMinutes +} + type adminScriptCommand struct { Name string Args []string diff --git a/weed/plugin/worker/admin_script_handler_test.go b/weed/plugin/worker/admin_script_handler_test.go new file mode 100644 index 000000000..7f2ab2236 --- /dev/null +++ b/weed/plugin/worker/admin_script_handler_test.go @@ -0,0 +1,100 @@ +package pluginworker + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/seaweedfs/seaweedfs/weed/pb/plugin_pb" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func TestAdminScriptDescriptorDefaults(t *testing.T) { + descriptor := NewAdminScriptHandler(nil).Descriptor() + if descriptor == nil { + t.Fatalf("expected descriptor") + } + if descriptor.AdminRuntimeDefaults == nil { + t.Fatalf("expected admin runtime defaults") + } + if descriptor.AdminRuntimeDefaults.DetectionIntervalSeconds != adminScriptDetectTickSecs { + t.Fatalf("unexpected detection interval seconds: got=%d want=%d", + descriptor.AdminRuntimeDefaults.DetectionIntervalSeconds, adminScriptDetectTickSecs) + } + if descriptor.AdminConfigForm == nil { + t.Fatalf("expected admin config form") + } + runInterval := readInt64Config(descriptor.AdminConfigForm.DefaultValues, "run_interval_minutes", 0) + if runInterval != defaultAdminScriptRunMins { + t.Fatalf("unexpected run_interval_minutes default: got=%d want=%d", runInterval, defaultAdminScriptRunMins) + } + script := readStringConfig(descriptor.AdminConfigForm.DefaultValues, "script", "") + if strings.TrimSpace(script) == "" { + t.Fatalf("expected non-empty default script") + } +} + +func TestAdminScriptDetectSkipsByRunInterval(t *testing.T) { + handler := NewAdminScriptHandler(nil) + sender := &recordingDetectionSender{} + err := handler.Detect(context.Background(), &plugin_pb.RunDetectionRequest{ + JobType: adminScriptJobType, + LastSuccessfulRun: timestamppb.New(time.Now().Add(-2 * time.Minute)), + AdminConfigValues: map[string]*plugin_pb.ConfigValue{ + "script": { + Kind: &plugin_pb.ConfigValue_StringValue{StringValue: defaultAdminScript}, + }, + "run_interval_minutes": { + Kind: &plugin_pb.ConfigValue_Int64Value{Int64Value: 17}, + }, + }, + }, sender) + if err != nil { + t.Fatalf("detect returned err = %v", err) + } + if sender.proposals == nil { + t.Fatalf("expected proposals message") + } + if len(sender.proposals.Proposals) != 0 { + t.Fatalf("expected zero proposals, got %d", len(sender.proposals.Proposals)) + } + if sender.complete == nil || !sender.complete.Success { + t.Fatalf("expected successful completion message") + } + if len(sender.events) == 0 { + t.Fatalf("expected detector activity events") + } + if !strings.Contains(sender.events[0].Message, "run interval") { + t.Fatalf("unexpected skip message: %q", sender.events[0].Message) + } +} + +func TestAdminScriptDetectCreatesProposalWhenIntervalElapsed(t *testing.T) { + handler := NewAdminScriptHandler(nil) + sender := &recordingDetectionSender{} + err := handler.Detect(context.Background(), &plugin_pb.RunDetectionRequest{ + JobType: adminScriptJobType, + LastSuccessfulRun: timestamppb.New(time.Now().Add(-20 * time.Minute)), + AdminConfigValues: map[string]*plugin_pb.ConfigValue{ + "script": { + Kind: &plugin_pb.ConfigValue_StringValue{StringValue: defaultAdminScript}, + }, + "run_interval_minutes": { + Kind: &plugin_pb.ConfigValue_Int64Value{Int64Value: 17}, + }, + }, + }, sender) + if err != nil { + t.Fatalf("detect returned err = %v", err) + } + if sender.proposals == nil { + t.Fatalf("expected proposals message") + } + if len(sender.proposals.Proposals) != 1 { + t.Fatalf("expected one proposal, got %d", len(sender.proposals.Proposals)) + } + if sender.complete == nil || !sender.complete.Success || sender.complete.TotalProposals != 1 { + t.Fatalf("unexpected completion message: %+v", sender.complete) + } +}