@@ -31,20 +31,38 @@ import (
3131
3232const monitorInterval = 30 * time .Second
3333
34+ // defaultPollOverlap is the look-back used when usage_lookback is not set.
35+ // The look-back makes each poll re-scan a short way before where the last one
36+ // stopped. SLURM saves a finished job to its accounting database a little
37+ // after the job ends, so a late-arriving record can land after the poll
38+ // window it belongs to has already passed. The look-back re-covers that gap,
39+ // and a repeat of the same job id replaces its earlier row. Its right value
40+ // depends on the cluster's slurmdbd commit lag, so it is configurable.
41+ const defaultPollOverlap = 15 * time .Minute
42+
43+ type jobLister interface {
44+ ListJobs (filter client.JobFilter ) ([]client.JobInfo , error )
45+ }
46+
3447type SlurmMonitor struct {
35- slurmClient * client. Client
48+ slurmClient jobLister
3649 eventBus * events.Bus
3750 coreService service.CoreService
3851 clusterId string
52+ pollOverlap time.Duration
3953 lastMonitorTime int64
4054}
4155
42- func NewSlurmMonitor (slurmClient * client.Client , eventBus * events.Bus , coreService service.CoreService , clusterId string ) * SlurmMonitor {
56+ func NewSlurmMonitor (slurmClient * client.Client , eventBus * events.Bus , coreService service.CoreService , clusterId string , pollOverlap time.Duration ) * SlurmMonitor {
57+ if pollOverlap <= 0 {
58+ pollOverlap = defaultPollOverlap
59+ }
4360 return & SlurmMonitor {
4461 slurmClient : slurmClient ,
4562 eventBus : eventBus ,
4663 coreService : coreService ,
4764 clusterId : clusterId ,
65+ pollOverlap : pollOverlap ,
4866 lastMonitorTime : 1 , // initialize to 1 to avoid issues with zero value
4967 }
5068}
@@ -81,8 +99,12 @@ func (m *SlurmMonitor) poll() {
8199 return
82100 }
83101
102+ windowStart := m .lastMonitorTime - int64 (m .pollOverlap .Seconds ())
103+ if windowStart < 1 {
104+ windowStart = 1
105+ }
84106 jobFilter := client.JobFilter {
85- StartTime : m . lastMonitorTime ,
107+ StartTime : windowStart ,
86108 EndTime : time .Now ().Unix (),
87109 }
88110
@@ -94,89 +116,98 @@ func (m *SlurmMonitor) poll() {
94116 m .lastMonitorTime = jobFilter .EndTime
95117
96118 for _ , job := range jobs {
97- //slog.Debug("processing SLURM job", "job_id", job.JobID, "job_name", job.Name)
98- //m.coreService.GetComputeAllocationResource()
99- slog .Info ("Job object" , "job" , job )
100- targetAccount := job .Account
101- for _ , alloc := range allocations {
102- if alloc .Name == targetAccount {
103- slog .Info ("found matching compute allocation for SLURM job" , "job_id" , job .JobID , "allocation_id" , alloc .ID )
104-
105- user , err := m .coreService .GetComputeClusterUserByLocalUsernameAndCluster (context , job .User , cluster .ID )
106- if err != nil {
107- if err == service .ErrNotFound {
108- slog .Warn ("compute cluster user not found for SLURM job, skipping usage recording" , "local_username" , job .User , "cluster_id" , cluster .ID )
109- return
110- } else {
111- slog .Error ("failed to get compute cluster user" , "error" , err )
112- return
113- }
114- }
115-
116- resource , err := m .coreService .GetComputeAllocationResourceByNameAndCluster (context , job .Partition , cluster .ID )
117-
118- if err != nil {
119- if err == service .ErrNotFound {
120- slog .Warn ("compute allocation resource not found for SLURM job, skipping usage recording" , "resource_name" , job .Partition , "cluster_id" , cluster .ID )
121- return
122- } else {
123- slog .Error ("failed to get compute allocation resource" , "error" , err )
124- return
125- }
126- }
127-
128- jobId := strconv .FormatInt (job .JobID , 10 )
129- existing , err := m .coreService .GetComputeAllocationUsageByComputeAllocationIDAndJobID (context , alloc .ID , jobId )
130-
131- if err != nil && err != service .ErrNotFound {
132- slog .Error ("failed to check for existing compute allocation usage" , "error" , err )
133- return
134- }
135-
136- jobDurationMs := job .Time .End - job .Time .Start
137-
138- if jobDurationMs <= 0 {
139- slog .Warn ("SLURM job has non-positive duration, skipping usage recording" , "job_id" , job .JobID , "duration" , jobDurationMs )
140- return
141- }
142-
143- tresType := resource .ResourceType
144-
145- resourceAmount := int64 (0 )
146- nodeCount := int64 (0 )
147- for _ , tres := range job .Tres .Allocated {
148- // Process each TRES type and its allocated amount as needed
149- // Example tres entry Allocated:[{Type:cpu Name: Count:1} {Type:mem Name: Count:8000} {Type:energy Name: Count:-2} {Type:node Name: Count:1} {Type:billing Name: Count:1}]
150- if tres .Type == tresType {
151- resourceAmount = tres .Count
152- }
153- if tres .Type == "node" {
154- nodeCount = tres .Count
155- }
156- }
157-
158- calulatedRawAmount := float64 (resourceAmount ) * float64 (nodeCount ) * float64 (jobDurationMs ) / (1000 * 3600 ) // Convert to minutes, adjust as needed based on how you want to calculate usage
159-
160- usageModel := & models.ComputeAllocationUsage {
161- ComputeAllocationID : alloc .ID ,
162- UsedRawAmount : calulatedRawAmount , // This is a simplification, adjust as needed based on how you want to calculate usage
163- UsedSUAmount : calulatedRawAmount * 1 , // Assuming 1 SU per second for simplicity, adjust as needed based on your SU calculation logic
164- CalculatedTime : time .Now (),
165- UserID : user .ID ,
166- JobID : jobId ,
167- ComputeAllocationResourceID : resource .ID ,
168- }
169-
170- if existing != nil {
171- m .coreService .DeleteComputeAllocationUsage (context , existing .ID )
172- slog .Info ("deleted existing compute allocation usage for SLURM job" , "job_id" , job .JobID , "existing_usage_id" , existing .ID )
173- }
174- m .coreService .CreateComputeAllocationUsage (context , usageModel )
175- break
176- }
177- }
119+ m .recordJob (context , job , cluster , allocations )
178120 }
179121
180122 slog .Info ("successfully polled SLURM usage" , "num_allocations" , len (allocations ), "num_jobs" , len (jobs ))
181123
182124}
125+
126+ // recordJob writes one usage row for a matched job; returning skips only this
127+ // job so one bad lookup cannot abort the rest of the poll cycle.
128+ func (m * SlurmMonitor ) recordJob (ctx context.Context , job client.JobInfo , cluster * models.ComputeCluster , allocations []models.ComputeAllocation ) {
129+ slog .Info ("Job object" , "job" , job )
130+ targetAccount := job .Account
131+ for _ , alloc := range allocations {
132+ if alloc .Name != targetAccount {
133+ continue
134+ }
135+ slog .Info ("found matching compute allocation for SLURM job" , "job_id" , job .JobID , "allocation_id" , alloc .ID )
136+
137+ user , err := m .coreService .GetComputeClusterUserByClusterAndLocalUsername (ctx , cluster .ID , job .User )
138+ if err != nil {
139+ if err == service .ErrNotFound {
140+ slog .Warn ("compute cluster user not found for SLURM job, skipping usage recording" , "local_username" , job .User , "cluster_id" , cluster .ID )
141+ } else {
142+ slog .Error ("failed to get compute cluster user" , "error" , err )
143+ }
144+ return
145+ }
146+
147+ resource , err := m .coreService .GetComputeAllocationResourceByNameAndCluster (ctx , job .Partition , cluster .ID )
148+ if err != nil {
149+ if err == service .ErrNotFound {
150+ slog .Warn ("compute allocation resource not found for SLURM job, skipping usage recording" , "resource_name" , job .Partition , "cluster_id" , cluster .ID )
151+ } else {
152+ slog .Error ("failed to get compute allocation resource" , "error" , err )
153+ }
154+ return
155+ }
156+
157+ jobId := strconv .FormatInt (job .JobID , 10 )
158+ existing , err := m .coreService .GetComputeAllocationUsageByComputeAllocationIDAndJobID (ctx , alloc .ID , jobId )
159+ if err != nil && err != service .ErrNotFound {
160+ slog .Error ("failed to check for existing compute allocation usage" , "error" , err )
161+ return
162+ }
163+
164+ jobDurationSec := job .Time .End - job .Time .Start
165+ if jobDurationSec <= 0 {
166+ slog .Warn ("SLURM job has non-positive duration, skipping usage recording" , "job_id" , job .JobID , "duration_seconds" , jobDurationSec )
167+ return
168+ }
169+
170+ tresType := resource .ResourceType
171+
172+ resourceAmount := int64 (0 )
173+ nodeCount := int64 (0 )
174+ for _ , tres := range job .Tres .Allocated {
175+ // Example tres entry Allocated:[{Type:cpu Name: Count:1} {Type:mem Name: Count:8000} {Type:energy Name: Count:-2} {Type:node Name: Count:1} {Type:billing Name: Count:1}]
176+ if tres .Type == tresType {
177+ resourceAmount = tres .Count
178+ }
179+ if tres .Type == "node" {
180+ nodeCount = tres .Count
181+ }
182+ }
183+
184+ calculatedRawAmount := float64 (resourceAmount ) * float64 (nodeCount ) * float64 (jobDurationSec ) / 3600
185+
186+ rate , err := m .coreService .GetEffectiveRateForResource (ctx , resource .ID , time .Unix (job .Time .End , 0 ))
187+ if err != nil {
188+ if err == service .ErrNotFound {
189+ slog .Warn ("no rate covers the job end time, skipping usage recording" , "job_id" , job .JobID , "resource_id" , resource .ID )
190+ } else {
191+ slog .Error ("failed to get effective rate for resource" , "error" , err , "job_id" , job .JobID , "resource_id" , resource .ID )
192+ }
193+ return
194+ }
195+
196+ usageModel := & models.ComputeAllocationUsage {
197+ ComputeAllocationID : alloc .ID ,
198+ UsedRawAmount : calculatedRawAmount ,
199+ UsedSUAmount : calculatedRawAmount * rate .Rate ,
200+ CalculatedTime : time .Now (),
201+ UserID : user .UserID ,
202+ JobID : jobId ,
203+ ComputeAllocationResourceID : resource .ID ,
204+ }
205+
206+ if existing != nil {
207+ m .coreService .DeleteComputeAllocationUsage (ctx , existing .ID )
208+ slog .Info ("deleted existing compute allocation usage for SLURM job" , "job_id" , job .JobID , "existing_usage_id" , existing .ID )
209+ }
210+ m .coreService .CreateComputeAllocationUsage (ctx , usageModel )
211+ return
212+ }
213+ }
0 commit comments