You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by la...@apache.org on 2026/10/02 17:01:43 UTC
(airavata-custos) 01/01: Usage monitor's job query within a defined range to avoid wide query issues
This is an automated email from the ASF dual-hosted git repository.
lahirujayathilake pushed a commit to branch bug-fix
in repository https://gitbox.apache.org/repos/asf/airavata-custos.git
commit b72f7f4de0f9a067da9888bb2b98f5f02daa4ee3
Author: lahiruj <la...@gmail.com>
AuthorDate: Fri Oct 2 12:58:20 2026 -0400
Usage monitor's job query within a defined range to avoid wide query issues
---
.../Usage-Monitor/internal/smonitor/smonitor.go | 18 +++---
.../internal/smonitor/smonitor_test.go | 66 +++++++++++++++++++++-
2 files changed, 73 insertions(+), 11 deletions(-)
diff --git a/connectors/SLURM/Usage-Monitor/internal/smonitor/smonitor.go b/connectors/SLURM/Usage-Monitor/internal/smonitor/smonitor.go
index f857db293..fe02586c6 100644
--- a/connectors/SLURM/Usage-Monitor/internal/smonitor/smonitor.go
+++ b/connectors/SLURM/Usage-Monitor/internal/smonitor/smonitor.go
@@ -40,6 +40,12 @@ const monitorInterval = 30 * time.Second
// depends on the cluster's slurmdbd commit lag, so it is configurable.
const defaultPollOverlap = 15 * time.Minute
+// maxPollStep is the value of the next poll step keeping the gap not too wide.
+const maxPollStep = 24 * time.Hour
+
+// startupCatchUp is the value how far back the poller scans for jobs.
+const startupCatchUp = 24 * time.Hour
+
type jobLister interface {
ListJobs(filter client.JobFilter) ([]client.JobInfo, error)
}
@@ -63,7 +69,7 @@ func NewSlurmMonitor(slurmClient *client.Client, eventBus *events.Bus, coreServi
coreService: coreService,
clusterId: clusterId,
pollOverlap: pollOverlap,
- lastMonitorTime: 1, // initialize to 1 to avoid issues with zero value
+ lastMonitorTime: time.Now().Add(-startupCatchUp).Unix(),
}
}
@@ -71,7 +77,7 @@ func (m *SlurmMonitor) StartMonitor(ctx context.Context) {
ticker := time.NewTicker(monitorInterval)
defer ticker.Stop()
- slog.Info("Starting SLURM usage monitor", "interval", monitorInterval)
+ slog.Info("Starting SLURM usage monitor", "interval", monitorInterval, "first_poll_from", time.Unix(m.lastMonitorTime, 0).UTC())
for {
select {
case <-ctx.Done():
@@ -99,13 +105,9 @@ func (m *SlurmMonitor) poll() {
return
}
- windowStart := m.lastMonitorTime - int64(m.pollOverlap.Seconds())
- if windowStart < 1 {
- windowStart = 1
- }
jobFilter := client.JobFilter{
- StartTime: windowStart,
- EndTime: time.Now().Unix(),
+ StartTime: m.lastMonitorTime - int64(m.pollOverlap.Seconds()),
+ EndTime: min(time.Now().Unix(), m.lastMonitorTime+int64(maxPollStep.Seconds())),
}
jobs, err := m.slurmClient.ListJobs(jobFilter)
diff --git a/connectors/SLURM/Usage-Monitor/internal/smonitor/smonitor_test.go b/connectors/SLURM/Usage-Monitor/internal/smonitor/smonitor_test.go
index 75fd320a7..4605fd45c 100644
--- a/connectors/SLURM/Usage-Monitor/internal/smonitor/smonitor_test.go
+++ b/connectors/SLURM/Usage-Monitor/internal/smonitor/smonitor_test.go
@@ -19,6 +19,7 @@ package smonitor
import (
"context"
+ "errors"
"testing"
"time"
@@ -28,11 +29,14 @@ import (
)
type fakeJobLister struct {
- jobs []client.JobInfo
+ jobs []client.JobInfo
+ err error // returned by every query when set
+ filters []client.JobFilter // every query it was asked for, in order
}
func (f *fakeJobLister) ListJobs(filter client.JobFilter) ([]client.JobInfo, error) {
- return f.jobs, nil
+ f.filters = append(f.filters, filter)
+ return f.jobs, f.err
}
func fixtureJob(jobID int64, user, partition string) client.JobInfo {
@@ -95,7 +99,63 @@ func newTestMonitor(core *service.CoreServiceMock, jobs ...client.JobInfo) *Slur
coreService: core,
clusterId: "cl-1",
pollOverlap: defaultPollOverlap,
- lastMonitorTime: 1,
+ lastMonitorTime: time.Now().Unix(),
+ }
+}
+
+// maxQuerySeconds is the longest time range one job query may cover.
+const maxQuerySeconds = int64((maxPollStep + defaultPollOverlap) / time.Second)
+
+// Make sure the first poll after a start asks only for the last day of jobs.
+func TestPollFirstQueryAfterAStartCoversOneStep(t *testing.T) {
+ lister := &fakeJobLister{}
+ m := NewSlurmMonitor(nil, nil, newMockCore(nil, nil), "cl-1", 0)
+ m.slurmClient = lister
+
+ m.poll()
+
+ query := lister.filters[0]
+ if seconds := query.EndTime - query.StartTime; seconds > maxQuerySeconds {
+ t.Errorf("first query covers %d seconds, want at most %d", seconds, maxQuerySeconds)
+ }
+}
+
+// Make sure a long gap is polled one step at a time, with no time skipped.
+func TestPollWalksALongGapInSteps(t *testing.T) {
+ lister := &fakeJobLister{}
+ m := newTestMonitor(newMockCore(nil, nil))
+ m.slurmClient = lister
+ m.lastMonitorTime = time.Now().Add(-60 * time.Hour).Unix()
+
+ before := time.Now().Unix()
+ for range 3 {
+ m.poll()
+ }
+
+ for i, query := range lister.filters {
+ if seconds := query.EndTime - query.StartTime; seconds > maxQuerySeconds {
+ t.Errorf("query %d covers %d seconds, want at most %d", i, seconds, maxQuerySeconds)
+ }
+ if i > 0 && query.StartTime > lister.filters[i-1].EndTime {
+ t.Errorf("query %d starts at %d, after query %d ended at %d", i, query.StartTime, i-1, lister.filters[i-1].EndTime)
+ }
+ }
+ if last := lister.filters[2].EndTime; last < before {
+ t.Errorf("after three polls the queries reach %d, want now (%d)", last, before)
+ }
+}
+
+// Make sure a failed query does not move the window forward.
+func TestPollKeepsTheWindowAfterAFailedQuery(t *testing.T) {
+ lister := &fakeJobLister{err: errors.New("cluster unreachable")}
+ m := newTestMonitor(newMockCore(nil, nil))
+ m.slurmClient = lister
+
+ m.poll()
+ m.poll()
+
+ if first, second := lister.filters[0].StartTime, lister.filters[1].StartTime; second != first {
+ t.Errorf("second query starts at %d, want the same start as the failed one (%d)", second, first)
}
}