-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathscheduler.go
More file actions
288 lines (236 loc) · 6.71 KB
/
Copy pathscheduler.go
File metadata and controls
288 lines (236 loc) · 6.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
package probe
import (
"fmt"
"strings"
"sync"
"time"
"github.com/linyows/probe/dag"
)
type JobStatus int
const (
JobPending JobStatus = iota
JobRunning
JobCompleted
JobFailed
)
type JobScheduler struct {
jobs map[string]*Job
status map[string]JobStatus
results map[string]bool
repeatCounters map[string]int // Track repeat execution count
repeatTargets map[string]int // Target repeat count
mutex sync.RWMutex
wg sync.WaitGroup
}
func NewJobScheduler() *JobScheduler {
return &JobScheduler{
jobs: make(map[string]*Job),
status: make(map[string]JobStatus),
results: make(map[string]bool),
repeatCounters: make(map[string]int),
repeatTargets: make(map[string]int),
}
}
func (js *JobScheduler) AddJob(job *Job) error {
js.mutex.Lock()
defer js.mutex.Unlock()
// Generate unique ID if not provided
if job.ID == "" {
job.ID = js.generateUniqueID(job.Name)
}
// Check for duplicate IDs
if _, exists := js.jobs[job.ID]; exists {
return fmt.Errorf("duplicate job ID: %s", job.ID)
}
js.jobs[job.ID] = job
js.status[job.ID] = JobPending
// Set repeat targets
if job.Repeat != nil {
js.repeatTargets[job.ID] = job.Repeat.Count
js.repeatCounters[job.ID] = 0
} else {
js.repeatTargets[job.ID] = 1 // No repeat = run once
js.repeatCounters[job.ID] = 0
}
return nil
}
// generateUniqueID generates a unique ID based on the job name
// If the name is already taken, it appends a number to make it unique
func (js *JobScheduler) generateUniqueID(baseName string) string {
if baseName == "" {
baseName = "job"
}
// First try the base name
if _, exists := js.jobs[baseName]; !exists {
return baseName
}
// If base name exists, try with incrementing numbers
counter := 1
for {
candidateID := fmt.Sprintf("%s-%d", baseName, counter)
if _, exists := js.jobs[candidateID]; !exists {
return candidateID
}
counter++
// Safety check to prevent infinite loop (though very unlikely)
if counter > 10000 {
return fmt.Sprintf("%s-%d", baseName, int(time.Now().UnixNano()))
}
}
}
func (js *JobScheduler) ValidateDependencies() error {
js.mutex.RLock()
defer js.mutex.RUnlock()
// Check if all dependencies exist
for jobID, job := range js.jobs {
for _, dep := range job.Needs {
if _, exists := js.jobs[dep]; !exists {
return fmt.Errorf("job '%s' depends on non-existent job '%s'", jobID, dep)
}
}
}
// Check for circular dependencies
return js.checkCircularDependencies()
}
func (js *JobScheduler) checkCircularDependencies() error {
// Collect all job IDs
allIDs := make([]string, 0, len(js.jobs))
for jobID := range js.jobs {
allIDs = append(allIDs, jobID)
}
// Define dependency getter function
getDeps := func(jobID string) []string {
if job, ok := js.jobs[jobID]; ok {
return job.Needs
}
return nil
}
// Use dag package to detect cycles
cycle := dag.DetectCycleFn(allIDs, getDeps)
if cycle != nil {
return fmt.Errorf("circular dependency detected: %s", strings.Join(cycle, " -> "))
}
return nil
}
func (js *JobScheduler) CanRunJob(jobID string) bool {
js.mutex.RLock()
defer js.mutex.RUnlock()
return js.canRunJobLocked(jobID)
}
// canRunJobLocked is the lock-free implementation of CanRunJob.
// Callers must already hold js.mutex (read or write). Splitting this out
// avoids re-entering RLock from GetRunnableJobs, which would deadlock when
// a writer is waiting on the same mutex.
func (js *JobScheduler) canRunJobLocked(jobID string) bool {
job, ok := js.jobs[jobID]
if !ok {
// Unknown job: a missing key returns a nil *Job, and ranging
// over job.Needs below would panic. CanRunJob is exported, so
// guard external callers by reporting "not runnable" instead.
return false
}
if js.status[jobID] != JobPending {
return false
}
// Check if all dependencies are fully completed (all repeats done)
for _, dep := range job.Needs {
if !js.isJobFullyCompletedLocked(dep) {
return false
}
}
return true
}
// isJobFullyCompletedLocked is the lock-free check for whether a job has
// finished all of its repeat executions. Callers must already hold js.mutex.
func (js *JobScheduler) isJobFullyCompletedLocked(jobID string) bool {
if js.status[jobID] != JobCompleted {
return false
}
// Check if all repeat executions are done
target := js.repeatTargets[jobID]
counter := js.repeatCounters[jobID]
return counter >= target && js.results[jobID]
}
func (js *JobScheduler) SetJobStatus(jobID string, status JobStatus, success bool) {
js.mutex.Lock()
defer js.mutex.Unlock()
js.status[jobID] = status
if status == JobCompleted || status == JobFailed {
js.results[jobID] = success
}
}
// IncrementRepeatCounter increments the repeat counter for a job
func (js *JobScheduler) IncrementRepeatCounter(jobID string) {
js.mutex.Lock()
defer js.mutex.Unlock()
js.repeatCounters[jobID]++
}
// ShouldRepeatJob checks if a job should be repeated
func (js *JobScheduler) ShouldRepeatJob(jobID string) bool {
js.mutex.RLock()
defer js.mutex.RUnlock()
counter := js.repeatCounters[jobID]
target := js.repeatTargets[jobID]
return counter < target
}
// GetRepeatInfo returns current repeat counter and target for a job
func (js *JobScheduler) GetRepeatInfo(jobID string) (current, target int) {
js.mutex.RLock()
defer js.mutex.RUnlock()
return js.repeatCounters[jobID], js.repeatTargets[jobID]
}
func (js *JobScheduler) GetRunnableJobs() []string {
js.mutex.RLock()
defer js.mutex.RUnlock()
var runnable []string
for jobID := range js.jobs {
if js.canRunJobLocked(jobID) {
runnable = append(runnable, jobID)
}
}
return runnable
}
func (js *JobScheduler) AllJobsCompleted() bool {
js.mutex.RLock()
defer js.mutex.RUnlock()
for _, status := range js.status {
if status != JobCompleted && status != JobFailed {
return false
}
}
return true
}
// MarkJobsWithFailedDependencies marks jobs as failed if their dependencies have failed.
// It iterates until no more jobs are newly marked, propagating failures transitively
// through multi-level dependency chains.
func (js *JobScheduler) MarkJobsWithFailedDependencies() []string {
js.mutex.Lock()
defer js.mutex.Unlock()
var skippedJobs []string
for {
changed := false
for jobID, job := range js.jobs {
if js.status[jobID] != JobPending {
continue
}
// Check if any dependency has failed
hasFailedDependency := false
for _, dep := range job.Needs {
if js.status[dep] == JobFailed || (js.status[dep] == JobCompleted && !js.results[dep]) {
hasFailedDependency = true
break
}
}
if hasFailedDependency {
js.status[jobID] = JobFailed
js.results[jobID] = false
skippedJobs = append(skippedJobs, jobID)
changed = true
}
}
if !changed {
break
}
}
return skippedJobs
}