runner.go 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. package jobs
  2. /*
  3. Job execution on one node.
  4. The runtime wraps the submitted script with a prelude that exposes the
  5. job object and an epilogue that calls run(JOB) and publishes the result:
  6. function run(job) { // job = {id, name, args, inputs, node}
  7. job.log("starting");
  8. job.progress(0.5);
  9. job.abortIfCancelled();
  10. return {ok: true}; // becomes the job output
  11. }
  12. An optional setup(ctx) runs once before run(). Normal AGI libraries work
  13. inside the script, so cluster:/ paths behave like any other file path.
  14. */
  15. import (
  16. "context"
  17. "encoding/json"
  18. "errors"
  19. "strings"
  20. "time"
  21. "imuslab.com/arozos/mod/info/logger"
  22. )
  23. const jobPrelude = `var JOB = JSON.parse(_job_spec());
  24. JOB.log = function(m){ _job_log(String(m)); };
  25. JOB.progress = function(p){ _job_progress(p); };
  26. JOB.cancelled = function(){ return _job_cancelled(); };
  27. JOB.abortIfCancelled = function(){ if (_job_cancelled()) { throw new Error("job cancelled"); } };
  28. `
  29. // buildSource assembles what the VM executes. run() is called once and its
  30. // value captured, so the epilogue cannot run the job twice.
  31. func buildSource(script string) string {
  32. return jobPrelude + "\n" + script + `
  33. if (typeof setup === "function") { setup({node: JOB.node}); }
  34. if (typeof run !== "function") { throw new Error("this job script defines no run(job) function"); }
  35. var __jobResult = run(JOB);
  36. _job_output(JSON.stringify(__jobResult === undefined ? null : __jobResult));
  37. `
  38. }
  39. // JobContext is what the script sees as JOB (before the helper functions are
  40. // attached by the prelude).
  41. type JobContext struct {
  42. ID string `json:"id"`
  43. Name string `json:"name"`
  44. Kind string `json:"kind"`
  45. Args json.RawMessage `json:"args,omitempty"`
  46. Inputs []string `json:"inputs"`
  47. Node string `json:"node"`
  48. Owner string `json:"owner"`
  49. }
  50. // RecordWait bounds how long a node waits for a job record that the leader
  51. // has assigned to it but that has not replicated to it yet.
  52. var RecordWait = 5 * time.Second
  53. // awaitRecord polls the local store for a job record until it appears or the
  54. // wait runs out.
  55. func (j *Manager) awaitRecord(jobID string, wait time.Duration) (Record, bool) {
  56. deadline := time.Now().Add(wait)
  57. for time.Now().Before(deadline) {
  58. time.Sleep(100 * time.Millisecond)
  59. if rec, ok := j.Get(jobID); ok {
  60. return rec, true
  61. }
  62. }
  63. return Record{}, false
  64. }
  65. // Start runs a job that was assigned to this node.
  66. func (j *Manager) Start(jobID string) error {
  67. if j.exec == nil {
  68. return ErrNoExecutor
  69. }
  70. rec, ok := j.Get(jobID)
  71. if !ok {
  72. //The leader can hand a job over a moment before its record has
  73. //replicated here; give the log a chance to catch up
  74. rec, ok = j.awaitRecord(jobID, RecordWait)
  75. if !ok {
  76. return ErrNotFound
  77. }
  78. }
  79. if rec.Done() {
  80. return nil
  81. }
  82. if !j.exec.UserExists(rec.Spec.Owner) {
  83. return ErrNoUser
  84. }
  85. //Accept each job once. A job only shows up in running after it gets an
  86. //execution slot, so that alone cannot stop a second hand-over (from a
  87. //retry or a new leader) from starting it again.
  88. j.mu.Lock()
  89. if _, busy := j.running[jobID]; busy || j.accepted[jobID] {
  90. j.mu.Unlock()
  91. return nil
  92. }
  93. j.accepted[jobID] = true
  94. j.mu.Unlock()
  95. go j.run(rec)
  96. return nil
  97. }
  98. func (j *Manager) run(rec Record) {
  99. defer func() {
  100. j.mu.Lock()
  101. delete(j.accepted, rec.Spec.ID)
  102. j.mu.Unlock()
  103. }()
  104. //Wait for a local execution slot; the job stays "scheduled" until then
  105. select {
  106. case j.sem <- struct{}{}:
  107. case <-j.stop:
  108. return
  109. }
  110. defer func() { <-j.sem }()
  111. id := rec.Spec.ID
  112. timeout := time.Duration(rec.Spec.TimeoutSec) * time.Second
  113. ctx, cancel := context.WithTimeout(context.Background(), timeout)
  114. rs := &runState{cancel: cancel}
  115. j.mu.Lock()
  116. j.running[id] = rs
  117. j.mu.Unlock()
  118. defer func() {
  119. cancel()
  120. j.mu.Lock()
  121. delete(j.running, id)
  122. j.mu.Unlock()
  123. }()
  124. //Publish "running" and keep the lease alive while we work
  125. rec.State.Status = StatusRunning
  126. rec.State.Node = j.m.NodeID()
  127. rec.State.Started = time.Now().Unix()
  128. rec.State.LeaseExpires = time.Now().Add(j.opt.TaskLease).Unix()
  129. j.put(rec)
  130. stopRenew := make(chan struct{})
  131. go j.renewLoop(id, stopRenew)
  132. hooks := ExecHooks{
  133. Log: func(line string) {
  134. rs.mu.Lock()
  135. rs.logs = append(rs.logs, line)
  136. if len(rs.logs) > maxLogLines {
  137. rs.logs = rs.logs[len(rs.logs)-maxLogLines:]
  138. }
  139. rs.mu.Unlock()
  140. },
  141. Progress: func(p float64) {
  142. if p < 0 {
  143. p = 0
  144. }
  145. if p > 1 {
  146. p = 1
  147. }
  148. rs.mu.Lock()
  149. rs.prog = p
  150. rs.mu.Unlock()
  151. },
  152. Cancelled: func() bool {
  153. if ctx.Err() != nil {
  154. return true
  155. }
  156. cur, ok := j.Get(id)
  157. return ok && cur.State.Status == StatusCancelled
  158. },
  159. }
  160. output, err := j.exec.Run(ctx, rec, sourceFor(rec), hooks)
  161. close(stopRenew)
  162. rs.mu.Lock()
  163. logs := append([]string{}, rs.logs...)
  164. prog := rs.prog
  165. rs.mu.Unlock()
  166. final, ok := j.Get(id)
  167. if !ok {
  168. return
  169. }
  170. final.State.Log = logs
  171. final.State.Node = j.m.NodeID()
  172. final.State.Finished = time.Now().Unix()
  173. final.State.LeaseExpires = 0
  174. switch {
  175. case final.State.Status == StatusCancelled:
  176. //Left as cancelled; keep whatever the script logged
  177. case err != nil && ctx.Err() == context.DeadlineExceeded:
  178. final.State.Status = StatusFailed
  179. final.State.Error = "job timed out after " + itoa(rec.Spec.TimeoutSec) + "s"
  180. case err != nil:
  181. final.State.Status = StatusFailed
  182. final.State.Error = err.Error()
  183. default:
  184. final.State.Status = StatusSucceeded
  185. final.State.Progress = 1
  186. final.State.Output = output
  187. final.State.Error = ""
  188. }
  189. if final.State.Status != StatusSucceeded {
  190. final.State.Progress = prog
  191. }
  192. j.put(final)
  193. j.reportDone(final)
  194. j.finishedEvent(final)
  195. if final.State.Status == StatusFailed {
  196. logger.PrintAndLog("Cluster", "Job "+final.Spec.Name+" failed: "+final.State.Error, nil)
  197. }
  198. }
  199. // renewLoop keeps the lease alive and pushes progress while the job runs.
  200. func (j *Manager) renewLoop(id string, stop chan struct{}) {
  201. ticker := time.NewTicker(j.opt.LeaseRenew)
  202. defer ticker.Stop()
  203. for {
  204. select {
  205. case <-stop:
  206. return
  207. case <-j.stop:
  208. return
  209. case <-ticker.C:
  210. rec, ok := j.Get(id)
  211. if !ok || rec.Done() {
  212. return
  213. }
  214. j.mu.Lock()
  215. rs := j.running[id]
  216. j.mu.Unlock()
  217. if rs != nil {
  218. rs.mu.Lock()
  219. rec.State.Progress = rs.prog
  220. if len(rs.logs) > 0 {
  221. rec.State.Log = append([]string{}, rs.logs...)
  222. }
  223. rs.mu.Unlock()
  224. }
  225. rec.State.LeaseExpires = time.Now().Add(j.opt.TaskLease).Unix()
  226. j.put(rec)
  227. }
  228. }
  229. }
  230. // reportDone lets the leader know immediately (the record is replicated
  231. // anyway; this only shortens the delay when the leader is elsewhere).
  232. func (j *Manager) reportDone(rec Record) {
  233. if j.meta.IsLeader() {
  234. return
  235. }
  236. leader := j.meta.Leader()
  237. if leader == "" {
  238. return
  239. }
  240. ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
  241. defer cancel()
  242. j.m.Transport().DoJSON(ctx, leader, "POST", pathDone, DoneReport{JobID: rec.Spec.ID, Status: rec.State.Status}, nil)
  243. }
  244. func itoa(v int) string {
  245. if v == 0 {
  246. return "0"
  247. }
  248. neg := v < 0
  249. if neg {
  250. v = -v
  251. }
  252. buf := [20]byte{}
  253. i := len(buf)
  254. for v > 0 {
  255. i--
  256. buf[i] = byte('0' + v%10)
  257. v /= 10
  258. }
  259. if neg {
  260. i--
  261. buf[i] = '-'
  262. }
  263. return string(buf[i:])
  264. }
  265. // ContextFor builds the JOB object handed to the script.
  266. func ContextFor(rec Record, nodeID string) JobContext {
  267. inputs := rec.Spec.Inputs
  268. if inputs == nil {
  269. inputs = []string{}
  270. }
  271. return JobContext{
  272. ID: rec.Spec.ID, Name: rec.Spec.Name, Kind: rec.Spec.Kind,
  273. Args: rec.Spec.Args, Inputs: inputs, Node: nodeID, Owner: rec.Spec.Owner,
  274. }
  275. }
  276. // ErrScriptNoRun is returned when a script defines no run() function.
  277. var ErrScriptNoRun = errors.New("this job script defines no run(job) function")
  278. // IsCancelled reports whether an execution error means "cancelled".
  279. func IsCancelled(err error) bool {
  280. return err != nil && strings.Contains(strings.ToLower(err.Error()), "cancel")
  281. }