From 961c270aba36f4553c19a8bd2687b92b74a572cc Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Sun, 8 Mar 2026 14:03:51 -0700 Subject: [PATCH] admin: expose per-job-type detection interval in plugin UI (#8552) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * admin: expose per-job-type detection interval in plugin UI The detection_interval_seconds field was not editable in the admin UI. collectAdminSettings() silently preserved the existing value, making it impossible for users to change how often a job type checks for new work. Users would change the global "Sleep Between Iterations" setting expecting it to control job scheduling frequency, but that only controls the scheduler loop's idle polling rate. Add a "Detection Interval (s)" input to the per-job-type admin settings form so users can actually configure it. Fixes #8549 * admin: remove global Sleep Between Iterations setting Now that per-job-type detection intervals are exposed in the UI, the global IdleSleepSeconds setting is redundant and confusing. It only controlled the scheduler loop's idle polling rate, which is always overridden by earliestNextDetectionAt() when job types exist. Replace the three usages with simpler alternatives: - Scheduler loop sleep: use defaultSchedulerIdleSleep constant - Initial delay for new job types: use policy.DetectionInterval/2 (more logical since it's already per-job-type) - Status fallback: use the constant The API endpoints are kept for backward compatibility but the UI no longer exposes or calls them. * admin: restore configurable idle sleep in scheduler loop The EC integration test sets idle_sleep_seconds=1 via the scheduler config API so the scheduler wakes quickly after workers connect. The previous commit replaced this with a hardcoded 613s constant, causing the scheduler to sleep through the entire test window. Restore GetSchedulerConfig().IdleSleepDuration() in the scheduler loop and status reporting. The UI removal of the setting is still correct — the API endpoint remains for programmatic use (e.g., tests). * admin: cap first-run initial delay to 5s instead of DetectionInterval/2 The initial delay for first-run job types was set to policy.DetectionInterval/2, which creates unbounded first-run latency (e.g., 1 hour for vacuum with a 2-hour detection interval). A small fixed 5-second delay provides sufficient stagger without penalizing startup time. --- weed/admin/plugin/plugin_scheduler.go | 3 +- weed/admin/view/app/plugin.templ | 114 ++------------------------ weed/admin/view/app/plugin_templ.go | 4 +- 3 files changed, 12 insertions(+), 109 deletions(-) diff --git a/weed/admin/plugin/plugin_scheduler.go b/weed/admin/plugin/plugin_scheduler.go index 2a91f4e07..ec0eddf4b 100644 --- a/weed/admin/plugin/plugin_scheduler.go +++ b/weed/admin/plugin/plugin_scheduler.go @@ -112,7 +112,6 @@ func (r *Plugin) runSchedulerIteration() bool { } active := make(map[string]struct{}, len(jobTypes)) - schedulerIdleSleep := r.GetSchedulerConfig().IdleSleepDuration() hadJobs := false for _, jobType := range jobTypes { @@ -129,7 +128,7 @@ func (r *Plugin) runSchedulerIteration() bool { } initialDelay := time.Duration(0) if runInfo := r.snapshotSchedulerRun(jobType); runInfo.lastRunStartedAt.IsZero() { - initialDelay = schedulerIdleSleep / 2 + initialDelay = 5 * time.Second } if !r.markDetectionDue(jobType, policy.DetectionInterval, initialDelay) { continue diff --git a/weed/admin/view/app/plugin.templ b/weed/admin/view/app/plugin.templ index dbd8684b4..e95c6899e 100644 --- a/weed/admin/view/app/plugin.templ +++ b/weed/admin/view/app/plugin.templ @@ -156,14 +156,7 @@ templ Plugin(page string) { Global
-
- - -
Used when no jobs are detected.
-
- +

Detection intervals are configured per job type in the job type settings below.

@@ -274,6 +267,11 @@ templ Plugin(page string) { +
+ + +
How often to check for new work.
+
@@ -311,14 +309,7 @@ templ Plugin(page string) {
Scheduler Settings
-
- - -
Used when no jobs are detected.
-
- +

Detection intervals are configured per job type in the settings above.

@@ -632,8 +623,6 @@ templ Plugin(page string) { activities: [], schedulerStates: [], schedulerStatus: null, - schedulerConfig: null, - schedulerConfigLoaded: false, allJobs: [], allActivities: [], loadedJobType: '', @@ -2461,6 +2450,7 @@ templ Plugin(page string) { } document.getElementById('plugin-admin-enabled').checked = pickBool('enabled'); + document.getElementById('plugin-admin-detection-interval').value = String(pickNumber('detection_interval_seconds')); document.getElementById('plugin-admin-detection-timeout').value = String(pickNumber('detection_timeout_seconds')); document.getElementById('plugin-admin-max-runtime').value = String(pickNumber('job_type_max_runtime_seconds')); document.getElementById('plugin-admin-max-results').value = String(pickNumber('max_jobs_per_detection')); @@ -2471,9 +2461,6 @@ templ Plugin(page string) { } function collectAdminSettings() { - var existingRuntime = (state.config && state.config.admin_runtime) ? state.config.admin_runtime : {}; - var existingDetectionInterval = Number(existingRuntime.detection_interval_seconds || 0); - function getInt(id) { var raw = String(document.getElementById(id).value || '').trim(); if (!raw) { @@ -2488,7 +2475,7 @@ templ Plugin(page string) { return { enabled: !!document.getElementById('plugin-admin-enabled').checked, - detection_interval_seconds: existingDetectionInterval, + detection_interval_seconds: getInt('plugin-admin-detection-interval'), detection_timeout_seconds: getInt('plugin-admin-detection-timeout'), job_type_max_runtime_seconds: getInt('plugin-admin-max-runtime'), max_jobs_per_detection: getInt('plugin-admin-max-results'), @@ -2806,75 +2793,6 @@ templ Plugin(page string) { } } - async function loadSchedulerConfig(forceRefresh) { - if (state.schedulerConfigLoaded && !forceRefresh) { - return; - } - var idleInputs = [ - document.getElementById('plugin-scheduler-idle-sleep'), - document.getElementById('plugin-scheduler-idle-sleep-overview'), - ].filter(Boolean); - if (idleInputs.length === 0) { - return; - } - try { - var cfg = await pluginRequest('GET', '/api/plugin/scheduler-config'); - state.schedulerConfig = cfg || {}; - state.schedulerConfigLoaded = true; - var idleSeconds = Number((cfg && cfg.idle_sleep_seconds) || 0); - idleInputs.forEach(function(input) { - input.value = idleSeconds > 0 ? String(idleSeconds) : ''; - }); - } catch (e) { - notify('Failed to load scheduler config: ' + e.message, 'error'); - } - } - - async function saveSchedulerConfig(sourceInput) { - var idleInputs = [ - document.getElementById('plugin-scheduler-idle-sleep'), - document.getElementById('plugin-scheduler-idle-sleep-overview'), - ].filter(Boolean); - if (idleInputs.length === 0) { - return; - } - var raw = ''; - if (sourceInput) { - raw = String(sourceInput.value || '').trim(); - } - if (!raw) { - for (var i = 0; i < idleInputs.length; i++) { - if (idleInputs[i] === sourceInput) { - continue; - } - var candidate = String(idleInputs[i].value || '').trim(); - if (candidate) { - raw = candidate; - break; - } - } - } - var parsed = raw ? parseInt(raw, 10) : 0; - if (Number.isNaN(parsed) || parsed < 0) { - notify('Invalid idle sleep value', 'error'); - return; - } - try { - var updated = await pluginRequest('PUT', '/api/plugin/scheduler-config', { - idle_sleep_seconds: parsed, - }); - state.schedulerConfig = updated || {}; - state.schedulerConfigLoaded = true; - var idleSeconds = Number((updated && updated.idle_sleep_seconds) || 0); - idleInputs.forEach(function(input) { - input.value = idleSeconds > 0 ? String(idleSeconds) : ''; - }); - notify('Scheduler settings saved', 'success'); - } catch (e) { - notify('Failed to save scheduler config: ' + e.message, 'error'); - } - } - function getMaxResults() { var raw = String(document.getElementById('plugin-admin-max-results').value || '').trim(); if (!raw) { @@ -3051,19 +2969,6 @@ templ Plugin(page string) { saveConfig(); }); - var saveSchedulerBtn = document.getElementById('plugin-save-scheduler-btn'); - if (saveSchedulerBtn) { - saveSchedulerBtn.addEventListener('click', function() { - saveSchedulerConfig(document.getElementById('plugin-scheduler-idle-sleep')); - }); - } - var saveSchedulerBtnOverview = document.getElementById('plugin-save-scheduler-btn-overview'); - if (saveSchedulerBtnOverview) { - saveSchedulerBtnOverview.addEventListener('click', function() { - saveSchedulerConfig(document.getElementById('plugin-scheduler-idle-sleep-overview')); - }); - } - document.getElementById('plugin-trigger-detection-btn').addEventListener('click', function() { runDetection(); }); @@ -3148,7 +3053,6 @@ templ Plugin(page string) { ensureActiveNavigation(); renderNavigationState(); await refreshAll(); - await loadSchedulerConfig(false); state.refreshTimer = setInterval(function() { refreshAll(); diff --git a/weed/admin/view/app/plugin_templ.go b/weed/admin/view/app/plugin_templ.go index b795099d6..91acea8e4 100644 --- a/weed/admin/view/app/plugin_templ.go +++ b/weed/admin/view/app/plugin_templ.go @@ -40,13 +40,13 @@ func Plugin(page string) templ.Component { var templ_7745c5c3_Var2 string templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(currentPage) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/plugin.templ`, Line: 10, Col: 80} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `plugin.templ`, Line: 10, Col: 80} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\">

Workers

Cluster-wide worker status, per-job configuration, detection, queue, and execution workflows.

Workers

0

Active Jobs

0

Activities (recent)

0

Per Job Type Summary
Job TypeActive JobsRecent Activities
Loading...
Scheduler State
Sequential scheduler with per-job runtime limits
Job TypeEnabledDetectorIn FlightMax RuntimeExec GlobalExec/WorkerExecutor WorkersEffective ExecLast Run
Loading...
Scheduler Settings
Global
Used when no jobs are detected.
Next Run
Scheduler
-
Not scheduled
Workers
WorkerAddressCapabilitiesLoad
Loading...
Job Type Configuration
Not loaded
Selected Job Type
-
Descriptor
Select a job type to load schema and config.
Admin Config Form
No admin form loaded.
Worker Config Form
No worker form loaded.
Job Scheduling Settings
Scheduler Settings
Used when no jobs are detected.
Next Run
Scheduler
-
Not scheduled
Run History
Keep last 10 success + last 10 errors
Successful Runs
TimeJob IDWorkerDuration
No data
Error Runs
TimeJob IDWorkerError
No data
Detection Results
Run detection to see proposals.
Job Queue
States: pending/assigned/running
Job IDTypeStateProgressWorkerUpdatedMessage
Loading...
Detection Jobs
Detection activities for selected job type
TimeJob TypeRequest IDWorkerStageSourceMessage
Loading...
Execution Jobs
Job IDTypeStateProgressWorkerUpdatedMessage
Loading...
Execution Activities
Non-detection events only
TimeJob TypeJob IDSourceStageMessage
Loading...
Job Detail
Select a job to view details.
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\">

Workers

Cluster-wide worker status, per-job configuration, detection, queue, and execution workflows.

Workers

0

Active Jobs

0

Activities (recent)

0

Per Job Type Summary
Job TypeActive JobsRecent Activities
Loading...
Scheduler State
Sequential scheduler with per-job runtime limits
Job TypeEnabledDetectorIn FlightMax RuntimeExec GlobalExec/WorkerExecutor WorkersEffective ExecLast Run
Loading...
Scheduler Settings
Global

Detection intervals are configured per job type in the job type settings below.

Next Run
Scheduler
-
Not scheduled
Workers
WorkerAddressCapabilitiesLoad
Loading...
Job Type Configuration
Not loaded
Selected Job Type
-
Descriptor
Select a job type to load schema and config.
Admin Config Form
No admin form loaded.
Worker Config Form
No worker form loaded.
Job Scheduling Settings
How often to check for new work.
Scheduler Settings

Detection intervals are configured per job type in the settings above.

Next Run
Scheduler
-
Not scheduled
Run History
Keep last 10 success + last 10 errors
Successful Runs
TimeJob IDWorkerDuration
No data
Error Runs
TimeJob IDWorkerError
No data
Detection Results
Run detection to see proposals.
Job Queue
States: pending/assigned/running
Job IDTypeStateProgressWorkerUpdatedMessage
Loading...
Detection Jobs
Detection activities for selected job type
TimeJob TypeRequest IDWorkerStageSourceMessage
Loading...
Execution Jobs
Job IDTypeStateProgressWorkerUpdatedMessage
Loading...
Execution Activities
Non-detection events only
TimeJob TypeJob IDSourceStageMessage
Loading...
Job Detail
Select a job to view details.
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err }