jobs.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731
  1. package jobs
  2. /*
  3. ArozOS Cluster Job Runtime (AJR) and Scheduler (AJS)
  4. An application submits a job; the cluster picks a node that satisfies the
  5. job's requirements and preferably already holds its input data; that node
  6. runs the job as the submitting user and publishes the result. Jobs are
  7. AGI scripts (JavaScript) because that is the only payload portable across
  8. nodes of different OS and CPU architecture.
  9. Every job is one replicated record (metadata.KindJob), so any node can
  10. answer status queries locally and a new leader can resume scheduling.
  11. The scheduler loop runs on the metadata leader only; execution happens
  12. anywhere.
  13. */
  14. import (
  15. "context"
  16. "encoding/json"
  17. "errors"
  18. "net/http"
  19. "runtime"
  20. "sort"
  21. "strings"
  22. "sync"
  23. "time"
  24. uuid "github.com/satori/go.uuid"
  25. "imuslab.com/arozos/mod/cluster/capability"
  26. "imuslab.com/arozos/mod/cluster/membership"
  27. "imuslab.com/arozos/mod/cluster/metadata"
  28. "imuslab.com/arozos/mod/cluster/scheduling"
  29. "imuslab.com/arozos/mod/info/logger"
  30. )
  31. // Job statuses.
  32. const (
  33. StatusQueued = "queued"
  34. StatusScheduled = "scheduled"
  35. StatusRunning = "running"
  36. StatusSucceeded = "succeeded"
  37. StatusFailed = "failed"
  38. StatusCancelled = "cancelled"
  39. )
  40. // Job kinds.
  41. const (
  42. KindRun = "run"
  43. KindMap = "map"
  44. KindReduce = "reduce"
  45. KindMapReduce = "mapreduce"
  46. )
  47. const maxLogLines = 200
  48. // Spec is what the submitter asks for.
  49. type Spec struct {
  50. ID string `json:"id"`
  51. Name string `json:"name"`
  52. Owner string `json:"owner"`
  53. Script string `json:"script"` //full JS source, captured at submit time
  54. ScriptName string `json:"scriptName"` //for logs
  55. Args json.RawMessage `json:"args,omitempty"`
  56. Inputs []string `json:"inputs,omitempty"`
  57. Requirements capability.Requirements `json:"requirements"`
  58. //Nodes limits placement to these node ids; empty means any node. Map
  59. //and reduce children inherit it.
  60. Nodes []string `json:"nodes,omitempty"`
  61. Priority int `json:"priority"`
  62. TimeoutSec int `json:"timeoutSec"`
  63. MaxAttempts int `json:"maxAttempts"`
  64. Kind string `json:"kind"`
  65. ParentID string `json:"parentId,omitempty"`
  66. //Map/reduce only: the dataset glob and how many files one map task takes
  67. Dataset string `json:"dataset,omitempty"`
  68. PartitionMax int `json:"partitionMax,omitempty"`
  69. Created int64 `json:"created"`
  70. }
  71. // State is what happened to it.
  72. type State struct {
  73. Status string `json:"status"`
  74. Node string `json:"node,omitempty"`
  75. Attempts int `json:"attempts"`
  76. LeaseExpires int64 `json:"leaseExpires,omitempty"`
  77. Progress float64 `json:"progress"`
  78. Output json.RawMessage `json:"output,omitempty"`
  79. Log []string `json:"log,omitempty"`
  80. Error string `json:"error,omitempty"`
  81. Started int64 `json:"started,omitempty"`
  82. Finished int64 `json:"finished,omitempty"`
  83. Reason string `json:"reason,omitempty"` //why it is still queued
  84. Blocked []string `json:"blocked,omitempty"` //nodes that refused this job
  85. }
  86. // Record is the replicated job (spec + state in one document, so the two can
  87. // never disagree after a merge).
  88. type Record struct {
  89. Spec Spec `json:"spec"`
  90. State State `json:"state"`
  91. }
  92. // Done reports whether the job reached a terminal state.
  93. func (r *Record) Done() bool {
  94. switch r.State.Status {
  95. case StatusSucceeded, StatusFailed, StatusCancelled:
  96. return true
  97. }
  98. return false
  99. }
  100. // Option configures the job service.
  101. type Option struct {
  102. Membership *membership.Manager
  103. Metadata *metadata.Manager
  104. Executor Executor
  105. //Scheduler scores the candidate nodes; nil falls back to the defaults.
  106. Scheduler *scheduling.Manager
  107. // LocalityBytes reports how many bytes of the given cluster:/ paths have a
  108. // healthy copy on nodeID, and the total size of those paths.
  109. LocalityBytes func(paths []string, nodeID string) (onNode int64, total int64)
  110. ScheduleInterval time.Duration
  111. TaskLease time.Duration
  112. LeaseRenew time.Duration
  113. MaxParallel int
  114. RetentionHours int
  115. }
  116. // Executor runs one job's script on this node. It is implemented by the core
  117. // on top of the AGI gateway.
  118. type Executor interface {
  119. // UserExists reports whether the job's owner has an account on this node.
  120. UserExists(owner string) bool
  121. // Run executes source as owner and returns what the script produced.
  122. // The context is cancelled on timeout or cancellation.
  123. Run(ctx context.Context, rec Record, source string, hooks ExecHooks) (output json.RawMessage, err error)
  124. }
  125. // ExecHooks are the callbacks the script drives through the job object.
  126. type ExecHooks struct {
  127. Log func(line string)
  128. Progress func(p float64)
  129. Cancelled func() bool
  130. }
  131. // Manager is the job service of this node.
  132. type Manager struct {
  133. m *membership.Manager
  134. meta *metadata.Manager
  135. exec Executor
  136. sched *scheduling.Manager
  137. opt Option
  138. mu sync.Mutex
  139. running map[string]*runState
  140. starting map[string]bool //map/reduce parents currently being expanded
  141. accepted map[string]bool //jobs this node took on and has not finished
  142. //schedMu serialises scheduling passes. Every submission, completion and
  143. //refusal starts a pass, and two passes running at once would both see a
  144. //job as queued and hand it out twice.
  145. schedMu sync.Mutex
  146. sem chan struct{}
  147. stop chan struct{}
  148. wg sync.WaitGroup
  149. once sync.Once
  150. // OnFinished fires when a job reaches a terminal state on this node.
  151. OnFinished func(rec Record)
  152. }
  153. type runState struct {
  154. cancel context.CancelFunc
  155. logs []string
  156. prog float64
  157. mu sync.Mutex
  158. }
  159. var (
  160. ErrNotLeader = errors.New("this node is not the metadata leader")
  161. ErrNotFound = errors.New("job not found")
  162. ErrNoUser = errors.New("the job owner has no account on this node")
  163. ErrNoExecutor = errors.New("this node cannot execute jobs")
  164. )
  165. // New creates the job service and starts its loops.
  166. func New(opt Option) (*Manager, error) {
  167. if opt.Membership == nil || opt.Metadata == nil {
  168. return nil, errors.New("membership and metadata are required")
  169. }
  170. if opt.ScheduleInterval <= 0 {
  171. opt.ScheduleInterval = 3 * time.Second
  172. }
  173. if opt.TaskLease <= 0 {
  174. opt.TaskLease = 30 * time.Second
  175. }
  176. if opt.LeaseRenew <= 0 {
  177. opt.LeaseRenew = 10 * time.Second
  178. }
  179. if opt.MaxParallel <= 0 {
  180. opt.MaxParallel = runtime.NumCPU()
  181. }
  182. if opt.RetentionHours <= 0 {
  183. opt.RetentionHours = 72
  184. }
  185. if opt.Scheduler == nil {
  186. sc, err := scheduling.New(opt.Membership, opt.Metadata)
  187. if err != nil {
  188. return nil, err
  189. }
  190. opt.Scheduler = sc
  191. }
  192. j := &Manager{
  193. m: opt.Membership,
  194. meta: opt.Metadata,
  195. exec: opt.Executor,
  196. sched: opt.Scheduler,
  197. opt: opt,
  198. running: map[string]*runState{},
  199. starting: map[string]bool{},
  200. accepted: map[string]bool{},
  201. sem: make(chan struct{}, opt.MaxParallel),
  202. stop: make(chan struct{}),
  203. }
  204. j.registerACNHandlers()
  205. j.wg.Add(1)
  206. go j.loop()
  207. return j, nil
  208. }
  209. // Close stops the scheduler and cancels local jobs.
  210. func (j *Manager) Close() {
  211. j.once.Do(func() { close(j.stop) })
  212. j.mu.Lock()
  213. for _, rs := range j.running {
  214. rs.cancel()
  215. }
  216. j.mu.Unlock()
  217. j.wg.Wait()
  218. }
  219. func (j *Manager) loop() {
  220. defer j.wg.Done()
  221. ticker := time.NewTicker(j.opt.ScheduleInterval)
  222. defer ticker.Stop()
  223. gc := time.NewTicker(time.Hour)
  224. defer gc.Stop()
  225. for {
  226. select {
  227. case <-j.stop:
  228. return
  229. case <-ticker.C:
  230. if j.meta.IsLeader() {
  231. j.schedule()
  232. }
  233. case <-gc.C:
  234. if j.meta.IsLeader() {
  235. cutoff := time.Now().Add(-time.Duration(j.opt.RetentionHours) * time.Hour).Unix()
  236. j.meta.GCJobs(cutoff, map[string]bool{StatusQueued: true, StatusScheduled: true, StatusRunning: true})
  237. }
  238. }
  239. }
  240. }
  241. /*
  242. Record helpers
  243. */
  244. func (j *Manager) decode(mj *metadata.Job) (Record, bool) {
  245. var rec Record
  246. if mj == nil || json.Unmarshal(mj.Body, &rec) != nil {
  247. return rec, false
  248. }
  249. return rec, true
  250. }
  251. // Get returns one job record.
  252. func (j *Manager) Get(id string) (Record, bool) {
  253. mj, ok := j.meta.Job(id)
  254. if !ok || mj.Removed {
  255. return Record{}, false
  256. }
  257. return j.decode(mj)
  258. }
  259. // List returns jobs, newest first; owner "" lists every job.
  260. func (j *Manager) List(owner string) []Record {
  261. out := []Record{}
  262. for _, mj := range j.meta.Jobs() {
  263. if mj.Removed || (owner != "" && mj.Owner != owner) {
  264. continue
  265. }
  266. if rec, ok := j.decode(&mj); ok {
  267. out = append(out, rec)
  268. }
  269. }
  270. sort.Slice(out, func(a, b int) bool { return out[a].Spec.Created > out[b].Spec.Created })
  271. return out
  272. }
  273. // put replicates a record.
  274. func (j *Manager) put(rec Record) error {
  275. body, err := json.Marshal(rec)
  276. if err != nil {
  277. return err
  278. }
  279. return j.meta.Submit(metadata.KindJob, &metadata.Job{
  280. ID: rec.Spec.ID,
  281. Owner: rec.Spec.Owner,
  282. Node: rec.State.Node,
  283. Status: rec.State.Status,
  284. Created: rec.Spec.Created,
  285. Body: body,
  286. })
  287. }
  288. /*
  289. Submission
  290. */
  291. // Submit validates and queues a job. The caller supplies the script source.
  292. func (j *Manager) Submit(spec Spec) (Record, error) {
  293. if !j.m.InCluster() {
  294. return Record{}, metadata.ErrNotInCluster
  295. }
  296. if strings.TrimSpace(spec.Script) == "" {
  297. return Record{}, errors.New("job script is empty")
  298. }
  299. if spec.Owner == "" {
  300. return Record{}, errors.New("job owner is required")
  301. }
  302. if spec.ID == "" {
  303. spec.ID = uuid.NewV4().String()
  304. }
  305. if strings.TrimSpace(spec.Name) == "" {
  306. spec.Name = spec.ScriptName
  307. }
  308. if spec.ScriptName == "" {
  309. spec.ScriptName = "job.agi"
  310. }
  311. if spec.Kind == "" {
  312. spec.Kind = KindRun
  313. }
  314. if spec.Kind == KindMapReduce && strings.TrimSpace(spec.Dataset) == "" {
  315. return Record{}, errors.New("a map/reduce job needs a dataset pattern")
  316. }
  317. if spec.TimeoutSec <= 0 {
  318. spec.TimeoutSec = 3600
  319. }
  320. if spec.MaxAttempts <= 0 {
  321. spec.MaxAttempts = 3
  322. }
  323. spec.Created = time.Now().Unix()
  324. for i, in := range spec.Inputs {
  325. spec.Inputs[i] = metadata.NormalizePath(in)
  326. }
  327. rec := Record{Spec: spec, State: State{Status: StatusQueued}}
  328. if err := j.put(rec); err != nil {
  329. return Record{}, err
  330. }
  331. logger.PrintAndLog("Cluster", "Job queued: "+spec.Name+" ("+spec.ID+")", nil)
  332. //Schedule immediately when we are the leader; otherwise the leader picks
  333. //it up on its next pass.
  334. if j.meta.IsLeader() {
  335. go j.schedule()
  336. }
  337. return rec, nil
  338. }
  339. // Cancel marks a job cancelled; a running node notices within a second.
  340. func (j *Manager) Cancel(id string, requester string, isAdmin bool) error {
  341. rec, ok := j.Get(id)
  342. if !ok {
  343. return ErrNotFound
  344. }
  345. if !isAdmin && rec.Spec.Owner != requester {
  346. return errors.New("permission denied")
  347. }
  348. if rec.Done() {
  349. return nil
  350. }
  351. rec.State.Status = StatusCancelled
  352. rec.State.Finished = time.Now().Unix()
  353. rec.State.Error = "cancelled by " + requester
  354. if err := j.put(rec); err != nil {
  355. return err
  356. }
  357. j.mu.Lock()
  358. if rs, running := j.running[id]; running {
  359. rs.cancel()
  360. }
  361. j.mu.Unlock()
  362. return nil
  363. }
  364. // Wait blocks until the job finishes or the timeout expires.
  365. func (j *Manager) Wait(id string, timeout time.Duration) (Record, error) {
  366. deadline := time.Now().Add(timeout)
  367. for {
  368. rec, ok := j.Get(id)
  369. if !ok {
  370. return Record{}, ErrNotFound
  371. }
  372. if rec.Done() {
  373. return rec, nil
  374. }
  375. if time.Now().After(deadline) {
  376. return rec, errors.New("timed out waiting for the job to finish")
  377. }
  378. select {
  379. case <-j.stop:
  380. return rec, errors.New("shutting down")
  381. case <-time.After(time.Second):
  382. }
  383. }
  384. }
  385. /*
  386. Scheduler (leader)
  387. */
  388. func usableState(s membership.NodeState) bool {
  389. return s == membership.StateOnline || s == membership.StateDegraded
  390. }
  391. // schedule is one scheduling pass.
  392. func (j *Manager) schedule() {
  393. if !j.meta.IsLeader() {
  394. return
  395. }
  396. j.schedMu.Lock()
  397. defer j.schedMu.Unlock()
  398. now := time.Now()
  399. nodes := j.m.NodeViews()
  400. loadPerNode := map[string]int{}
  401. pending := []Record{}
  402. for _, mj := range j.meta.Jobs() {
  403. if mj.Removed {
  404. continue
  405. }
  406. rec, ok := j.decode(&mj)
  407. if !ok || rec.Done() {
  408. continue
  409. }
  410. if rec.Spec.Kind == KindMapReduce && rec.State.Status == StatusRunning {
  411. continue //handled below, it runs on no node
  412. }
  413. switch rec.State.Status {
  414. case StatusScheduled, StatusRunning:
  415. if rec.State.LeaseExpires > 0 && rec.State.LeaseExpires < now.Unix() {
  416. //The worker went silent: requeue or give up
  417. if rec.State.Attempts >= rec.Spec.MaxAttempts {
  418. rec.State.Status = StatusFailed
  419. rec.State.Error = "node lost while running the job"
  420. rec.State.Finished = now.Unix()
  421. j.put(rec)
  422. j.finishedEvent(rec)
  423. continue
  424. }
  425. rec.State.Status = StatusQueued
  426. rec.State.Node = ""
  427. rec.State.LeaseExpires = 0
  428. rec.State.Reason = "previous node stopped reporting"
  429. j.put(rec)
  430. pending = append(pending, rec)
  431. continue
  432. }
  433. loadPerNode[rec.State.Node]++
  434. case StatusQueued:
  435. if rec.Spec.Kind == KindMapReduce {
  436. j.startMapReduce(rec)
  437. continue
  438. }
  439. pending = append(pending, rec)
  440. }
  441. }
  442. //Move running map/reduce parents forward
  443. for _, mj := range j.meta.Jobs() {
  444. if mj.Removed || mj.Status != StatusRunning {
  445. continue
  446. }
  447. if rec, ok := j.decode(&mj); ok && rec.Spec.Kind == KindMapReduce {
  448. j.advanceMapReduce(rec)
  449. }
  450. }
  451. if len(pending) == 0 {
  452. return
  453. }
  454. sort.Slice(pending, func(a, b int) bool {
  455. if pending[a].Spec.Priority != pending[b].Spec.Priority {
  456. return pending[a].Spec.Priority > pending[b].Spec.Priority
  457. }
  458. return pending[a].Spec.Created < pending[b].Spec.Created
  459. })
  460. for _, rec := range pending {
  461. node, reason := j.pick(rec, nodes, loadPerNode)
  462. if node == "" {
  463. if rec.State.Reason != reason {
  464. rec.State.Reason = reason
  465. j.put(rec)
  466. }
  467. continue
  468. }
  469. rec.State.Status = StatusScheduled
  470. rec.State.Node = node
  471. rec.State.Attempts++
  472. rec.State.LeaseExpires = now.Add(j.opt.TaskLease).Unix()
  473. rec.State.Reason = ""
  474. if err := j.put(rec); err != nil {
  475. continue
  476. }
  477. loadPerNode[node]++
  478. go j.assign(rec)
  479. }
  480. }
  481. // locality reports how many of the input bytes already sit on a node. The
  482. // host may supply a richer implementation (which also expands directories);
  483. // otherwise the metadata records answer it directly.
  484. func (j *Manager) locality(paths []string, nodeID string) (int64, int64) {
  485. if j.opt.LocalityBytes != nil {
  486. return j.opt.LocalityBytes(paths, nodeID)
  487. }
  488. var onNode, total int64
  489. for _, p := range paths {
  490. rec, err := j.meta.Stat(p)
  491. if err != nil || rec.IsDir {
  492. continue
  493. }
  494. size := rec.Size
  495. if size == 0 {
  496. size = 1 //count the file even when it is empty
  497. }
  498. total += size
  499. for _, loc := range rec.HealthyLocations() {
  500. if loc.NodeID == nodeID {
  501. onNode += size
  502. break
  503. }
  504. }
  505. }
  506. return onNode, total
  507. }
  508. // pick scores the eligible nodes and returns the best one, or a reason why
  509. // nothing fits. It also returns the full ranking, which the explain endpoint
  510. // shows to administrators.
  511. func (j *Manager) pick(rec Record, nodes []membership.NodeView, load map[string]int) (string, string) {
  512. results := j.rank(rec, nodes, load)
  513. return scheduling.Best(results)
  514. }
  515. // rank scores every node for a job.
  516. func (j *Manager) rank(rec Record, nodes []membership.NodeView, load map[string]int) []scheduling.Result {
  517. blocked := map[string]bool{}
  518. for _, id := range rec.State.Blocked {
  519. blocked[id] = true
  520. }
  521. candidates := []scheduling.Candidate{}
  522. for _, n := range nodes {
  523. c := scheduling.Candidate{Node: n, Queue: load[n.ID], FreeDisk: -1, LatencyToData: -1, Diversity: -1}
  524. if len(rec.Spec.Inputs) > 0 {
  525. onNode, total := j.locality(rec.Spec.Inputs, n.ID)
  526. if total > 0 {
  527. c.Locality = float64(onNode) / float64(total)
  528. }
  529. }
  530. candidates = append(candidates, c)
  531. }
  532. return j.sched.Rank(candidates, func(c scheduling.Candidate) (bool, string) {
  533. if !usableState(c.Node.State) {
  534. return false, "node is " + strings.ToLower(string(c.Node.State))
  535. }
  536. if blocked[c.Node.ID] {
  537. return false, "node refused this job earlier"
  538. }
  539. if len(rec.Spec.Nodes) > 0 && !contains(rec.Spec.Nodes, c.Node.ID) {
  540. return false, "job is limited to other nodes"
  541. }
  542. if ok, why := c.Node.Capabilities.Satisfies(rec.Spec.Requirements); !ok {
  543. return false, why
  544. }
  545. return true, ""
  546. })
  547. }
  548. // Explain returns the ranking the scheduler would use for a job right now.
  549. func (j *Manager) Explain(jobID string) ([]scheduling.Result, error) {
  550. rec, ok := j.Get(jobID)
  551. if !ok {
  552. return nil, ErrNotFound
  553. }
  554. load := map[string]int{}
  555. for _, mj := range j.meta.Jobs() {
  556. if mj.Removed {
  557. continue
  558. }
  559. if r, ok := j.decode(&mj); ok && (r.State.Status == StatusRunning || r.State.Status == StatusScheduled) {
  560. load[r.State.Node]++
  561. }
  562. }
  563. return j.rank(rec, j.m.NodeViews(), load), nil
  564. }
  565. // assign tells the chosen node to run the job (or runs it here). A node that
  566. // refuses (no account for the owner, cannot execute) is remembered so the
  567. // scheduler does not offer it the same job again.
  568. func (j *Manager) assign(rec Record) {
  569. node := rec.State.Node
  570. var err error
  571. if node == j.m.NodeID() {
  572. err = j.Start(rec.Spec.ID)
  573. } else {
  574. ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
  575. err = j.m.Transport().DoJSON(ctx, node, http.MethodPost, pathRun, RunRequest{JobID: rec.Spec.ID}, nil)
  576. cancel()
  577. }
  578. if err == nil {
  579. return
  580. }
  581. //Could not hand it over: put it back in the queue right away
  582. latest, ok := j.Get(rec.Spec.ID)
  583. if !ok || latest.Done() || latest.State.Status == StatusRunning {
  584. return
  585. }
  586. latest.State.Status = StatusQueued
  587. latest.State.Node = ""
  588. latest.State.LeaseExpires = 0
  589. latest.State.Reason = "node " + j.m.NodeName(node) + " could not take the job: " + err.Error()
  590. //Only a real refusal keeps the node away from this job for good. A
  591. //hiccup (a tunnel still reconnecting after a restart, a record that has
  592. //not reached the node yet) is retried on the next pass; blocking on it
  593. //would strand a job that is pinned to that node.
  594. if refusalIsPermanent(err) && !contains(latest.State.Blocked, node) {
  595. latest.State.Blocked = append(latest.State.Blocked, node)
  596. }
  597. //A refusal is not a failed attempt by the job itself
  598. if latest.State.Attempts > 0 {
  599. latest.State.Attempts--
  600. }
  601. j.put(latest)
  602. if j.meta.IsLeader() {
  603. go j.schedule()
  604. }
  605. }
  606. // refusalIsPermanent reports whether a node's answer to "run this job" means
  607. // it will never be able to run it: the owner has no account there, or it
  608. // cannot execute jobs at all. The error may come straight from Start or, for
  609. // a remote node, as the text the node sent back.
  610. func refusalIsPermanent(err error) bool {
  611. if err == nil {
  612. return false
  613. }
  614. if errors.Is(err, ErrNoUser) || errors.Is(err, ErrNoExecutor) {
  615. return true
  616. }
  617. msg := err.Error()
  618. return strings.Contains(msg, ErrNoUser.Error()) || strings.Contains(msg, ErrNoExecutor.Error())
  619. }
  620. func contains(list []string, v string) bool {
  621. for _, x := range list {
  622. if x == v {
  623. return true
  624. }
  625. }
  626. return false
  627. }
  628. func (j *Manager) finishedEvent(rec Record) {
  629. if j.OnFinished != nil {
  630. j.OnFinished(rec)
  631. }
  632. }
  633. /*
  634. Status
  635. */
  636. // Status summarises the job system for the UI.
  637. type Status struct {
  638. InCluster bool `json:"inCluster"`
  639. IsLeader bool `json:"isLeader"`
  640. LeaderName string `json:"leaderName"`
  641. Queued int `json:"queued"`
  642. Scheduled int `json:"scheduled"`
  643. Running int `json:"running"`
  644. Succeeded int `json:"succeeded"`
  645. Failed int `json:"failed"`
  646. Cancelled int `json:"cancelled"`
  647. LocalSlots int `json:"localSlots"`
  648. LocalBusy int `json:"localBusy"`
  649. Jobs []Record `json:"jobs"`
  650. }
  651. // Status builds the picture, filtered to owner when non-empty.
  652. func (j *Manager) Status(owner string) Status {
  653. st := Status{InCluster: j.m.InCluster(), IsLeader: j.meta.IsLeader(), LocalSlots: j.opt.MaxParallel, Jobs: []Record{}}
  654. if l := j.meta.Leader(); l != "" {
  655. st.LeaderName = j.m.NodeName(l)
  656. }
  657. for _, rec := range j.List(owner) {
  658. switch rec.State.Status {
  659. case StatusQueued:
  660. st.Queued++
  661. case StatusScheduled:
  662. st.Scheduled++
  663. case StatusRunning:
  664. st.Running++
  665. case StatusSucceeded:
  666. st.Succeeded++
  667. case StatusFailed:
  668. st.Failed++
  669. case StatusCancelled:
  670. st.Cancelled++
  671. }
  672. if len(st.Jobs) < 100 {
  673. st.Jobs = append(st.Jobs, rec)
  674. }
  675. }
  676. j.mu.Lock()
  677. st.LocalBusy = len(j.running)
  678. j.mu.Unlock()
  679. return st
  680. }