mapreduce.go 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. package jobs
  2. /*
  3. Map / Reduce on top of the job runtime.
  4. A map/reduce job carries one script that defines both halves:
  5. function mapper(files, emit) { // files = paths of this partition
  6. files.forEach(function(f){ emit(keyFor(f), 1); });
  7. }
  8. function reducer(key, values) { return values.length; }
  9. The leader expands the dataset glob over the namespace, groups the files
  10. by the node that already holds a healthy copy (so map work runs where the
  11. bytes are), splits each group into partitions and submits one child map
  12. job per partition. When every map child has succeeded it groups the
  13. emitted pairs by key and submits a single reduce child; the reduce output
  14. becomes the parent's output.
  15. Parent jobs are coordinated by the leader and never assigned to a node.
  16. */
  17. import (
  18. "encoding/json"
  19. "errors"
  20. "sort"
  21. "strings"
  22. "time"
  23. "imuslab.com/arozos/mod/cluster/metadata"
  24. "imuslab.com/arozos/mod/info/logger"
  25. )
  26. const defaultPartitionMax = 50
  27. // mapOutput is one emitted pair from a mapper.
  28. type mapOutput [2]json.RawMessage
  29. // reduceArgs is what a reduce child receives.
  30. type reduceArgs struct {
  31. Groups map[string][]json.RawMessage `json:"groups"`
  32. }
  33. // buildMapSource wraps a script for the map half.
  34. func buildMapSource(script string) string {
  35. return jobPrelude + "\n" + script + `
  36. if (typeof mapper !== "function") { throw new Error("this map/reduce script defines no mapper(files, emit) function"); }
  37. var __emitted = [];
  38. function emit(k, v) { __emitted.push([String(k), v === undefined ? null : v]); }
  39. mapper(JOB.inputs, emit);
  40. _job_output(JSON.stringify(__emitted));
  41. `
  42. }
  43. // buildReduceSource wraps a script for the reduce half.
  44. func buildReduceSource(script string) string {
  45. return jobPrelude + "\n" + script + `
  46. if (typeof reducer !== "function") { throw new Error("this map/reduce script defines no reducer(key, values) function"); }
  47. var __groups = (JOB.args && JOB.args.groups) ? JOB.args.groups : {};
  48. var __out = {};
  49. var __keys = Object.keys(__groups);
  50. for (var __i = 0; __i < __keys.length; __i++) {
  51. JOB.progress(__i / Math.max(1, __keys.length));
  52. __out[__keys[__i]] = reducer(__keys[__i], __groups[__keys[__i]]);
  53. }
  54. _job_output(JSON.stringify(__out));
  55. `
  56. }
  57. // sourceFor picks the right wrapper for a job kind.
  58. func sourceFor(rec Record) string {
  59. switch rec.Spec.Kind {
  60. case KindMap:
  61. return buildMapSource(rec.Spec.Script)
  62. case KindReduce:
  63. return buildReduceSource(rec.Spec.Script)
  64. }
  65. return buildSource(rec.Spec.Script)
  66. }
  67. // children returns the child jobs of a parent.
  68. func (j *Manager) children(parentID string) []Record {
  69. out := []Record{}
  70. for _, mj := range j.meta.Jobs() {
  71. if mj.Removed {
  72. continue
  73. }
  74. rec, ok := j.decode(&mj)
  75. if !ok || rec.Spec.ParentID != parentID {
  76. continue
  77. }
  78. out = append(out, rec)
  79. }
  80. sort.Slice(out, func(a, b int) bool { return out[a].Spec.Created < out[b].Spec.Created })
  81. return out
  82. }
  83. // expandDataset lists the files of a dataset and groups them by the node
  84. // holding a healthy copy (files with no reachable copy are reported).
  85. func (j *Manager) expandDataset(pattern string) (byNode map[string][]metadata.FileRecord, orphans []string) {
  86. byNode = map[string][]metadata.FileRecord{}
  87. for _, rec := range j.meta.Glob(pattern) {
  88. best := ""
  89. for _, loc := range rec.HealthyLocations() {
  90. for _, n := range j.m.NodeViews() {
  91. if n.ID == loc.NodeID && usableState(n.State) {
  92. best = loc.NodeID
  93. break
  94. }
  95. }
  96. if best != "" {
  97. break
  98. }
  99. }
  100. if best == "" {
  101. orphans = append(orphans, rec.Path)
  102. continue
  103. }
  104. byNode[best] = append(byNode[best], rec)
  105. }
  106. return
  107. }
  108. // startMapReduce expands the dataset and submits the map children. It runs
  109. // once per parent: submitting a child re-enters the scheduler, so the parent
  110. // is marked running before any child exists.
  111. func (j *Manager) startMapReduce(parent Record) {
  112. id := parent.Spec.ID
  113. j.mu.Lock()
  114. if j.starting[id] {
  115. j.mu.Unlock()
  116. return
  117. }
  118. j.starting[id] = true
  119. j.mu.Unlock()
  120. defer func() {
  121. j.mu.Lock()
  122. delete(j.starting, id)
  123. j.mu.Unlock()
  124. }()
  125. latest, ok := j.Get(id)
  126. if !ok || latest.State.Status != StatusQueued {
  127. return //another pass already expanded it
  128. }
  129. parent = latest
  130. pattern := strings.TrimSpace(parent.Spec.Dataset)
  131. if pattern == "" {
  132. j.failParent(parent, "the job has no dataset pattern")
  133. return
  134. }
  135. byNode, orphans := j.expandDataset(pattern)
  136. if len(orphans) > 0 {
  137. list := strings.Join(orphans, ", ")
  138. if len(list) > 300 {
  139. list = list[:300] + "..."
  140. }
  141. j.failParent(parent, "no reachable copy of: "+list)
  142. return
  143. }
  144. partitionMax := parent.Spec.PartitionMax
  145. if partitionMax <= 0 {
  146. partitionMax = defaultPartitionMax
  147. }
  148. nodes := make([]string, 0, len(byNode))
  149. for id := range byNode {
  150. nodes = append(nodes, id)
  151. }
  152. sort.Strings(nodes)
  153. //Claim the parent before creating children, so the scheduler pass that
  154. //that each child submission triggers cannot start it a second time.
  155. parent.State.Status = StatusRunning
  156. parent.State.Started = time.Now().Unix()
  157. parent.State.Progress = 0
  158. if err := j.put(parent); err != nil {
  159. return
  160. }
  161. created := 0
  162. for _, node := range nodes {
  163. files := byNode[node]
  164. for start := 0; start < len(files); start += partitionMax {
  165. end := start + partitionMax
  166. if end > len(files) {
  167. end = len(files)
  168. }
  169. inputs := []string{}
  170. for _, f := range files[start:end] {
  171. inputs = append(inputs, f.Path)
  172. }
  173. child := parent.Spec
  174. child.ID = ""
  175. child.Kind = KindMap
  176. child.ParentID = parent.Spec.ID
  177. child.Name = parent.Spec.Name + " map " + itoa(created+1)
  178. child.Inputs = inputs
  179. child.Dataset = ""
  180. if _, err := j.Submit(child); err != nil {
  181. j.failParent(parent, "could not create a map task: "+err.Error())
  182. return
  183. }
  184. created++
  185. }
  186. }
  187. if created == 0 {
  188. //Nothing matched: succeed with an empty result
  189. parent, _ = j.Get(parent.Spec.ID)
  190. parent.State.Status = StatusSucceeded
  191. parent.State.Progress = 1
  192. parent.State.Output = json.RawMessage(`{}`)
  193. parent.State.Finished = time.Now().Unix()
  194. parent.State.Log = append(parent.State.Log, "dataset matched no files")
  195. j.put(parent)
  196. j.finishedEvent(parent)
  197. return
  198. }
  199. parent, _ = j.Get(parent.Spec.ID)
  200. parent.State.Log = append(parent.State.Log, "expanded "+pattern+" into "+itoa(created)+" map task(s) across "+itoa(len(nodes))+" node(s)")
  201. j.put(parent)
  202. logger.PrintAndLog("Cluster", "Map/reduce "+parent.Spec.Name+" started with "+itoa(created)+" map tasks", nil)
  203. }
  204. // advanceMapReduce moves a running parent forward: it checks the children,
  205. // creates the reduce task when the map half is complete, and publishes the
  206. // final result.
  207. func (j *Manager) advanceMapReduce(parent Record) {
  208. kids := j.children(parent.Spec.ID)
  209. if len(kids) == 0 {
  210. return
  211. }
  212. var maps, reduces []Record
  213. for _, k := range kids {
  214. if k.Spec.Kind == KindReduce {
  215. reduces = append(reduces, k)
  216. } else {
  217. maps = append(maps, k)
  218. }
  219. }
  220. //A failed or cancelled child fails the whole job
  221. for _, k := range kids {
  222. if k.State.Status == StatusFailed || k.State.Status == StatusCancelled {
  223. j.failParent(parent, k.Spec.Name+": "+firstNonEmpty(k.State.Error, k.State.Status))
  224. return
  225. }
  226. }
  227. doneMaps := 0
  228. for _, k := range maps {
  229. if k.State.Status == StatusSucceeded {
  230. doneMaps++
  231. }
  232. }
  233. total := len(maps) + 1 //the reduce task counts as one more step
  234. progress := float64(doneMaps) / float64(total)
  235. //Reduce already running or finished?
  236. if len(reduces) > 0 {
  237. red := reduces[0]
  238. switch red.State.Status {
  239. case StatusSucceeded:
  240. parent.State.Status = StatusSucceeded
  241. parent.State.Progress = 1
  242. parent.State.Output = red.State.Output
  243. parent.State.Finished = time.Now().Unix()
  244. parent.State.Node = red.State.Node
  245. j.put(parent)
  246. j.finishedEvent(parent)
  247. logger.PrintAndLog("Cluster", "Map/reduce "+parent.Spec.Name+" finished", nil)
  248. default:
  249. parent.State.Progress = float64(len(maps)) / float64(total)
  250. j.put(parent)
  251. }
  252. return
  253. }
  254. if doneMaps < len(maps) {
  255. if parent.State.Progress != progress {
  256. parent.State.Progress = progress
  257. j.put(parent)
  258. }
  259. return
  260. }
  261. //Every map task is done: group the emitted pairs by key
  262. groups := map[string][]json.RawMessage{}
  263. for _, k := range maps {
  264. var emitted []mapOutput
  265. if len(k.State.Output) == 0 {
  266. continue
  267. }
  268. if err := json.Unmarshal(k.State.Output, &emitted); err != nil {
  269. j.failParent(parent, k.Spec.Name+" produced an unreadable result")
  270. return
  271. }
  272. for _, pair := range emitted {
  273. key := strings.Trim(string(pair[0]), `"`)
  274. groups[key] = append(groups[key], pair[1])
  275. }
  276. }
  277. args, err := json.Marshal(reduceArgs{Groups: groups})
  278. if err != nil {
  279. j.failParent(parent, "could not assemble the reduce input: "+err.Error())
  280. return
  281. }
  282. if len(args) > 8<<20 {
  283. j.failParent(parent, "the mappers emitted more than 8 MB; narrow the dataset or emit fewer values")
  284. return
  285. }
  286. child := parent.Spec
  287. child.ID = ""
  288. child.Kind = KindReduce
  289. child.ParentID = parent.Spec.ID
  290. child.Name = parent.Spec.Name + " reduce"
  291. child.Inputs = nil
  292. child.Dataset = ""
  293. child.Args = json.RawMessage(args)
  294. if _, err := j.Submit(child); err != nil {
  295. j.failParent(parent, "could not create the reduce task: "+err.Error())
  296. return
  297. }
  298. parent.State.Progress = float64(len(maps)) / float64(total)
  299. parent.State.Log = append(parent.State.Log, "mapped "+itoa(len(groups))+" key(s), reducing")
  300. j.put(parent)
  301. }
  302. func (j *Manager) failParent(parent Record, reason string) {
  303. parent.State.Status = StatusFailed
  304. parent.State.Error = reason
  305. parent.State.Finished = time.Now().Unix()
  306. j.put(parent)
  307. j.finishedEvent(parent)
  308. //Cancel anything still pending underneath
  309. for _, k := range j.children(parent.Spec.ID) {
  310. if !k.Done() {
  311. j.Cancel(k.Spec.ID, k.Spec.Owner, true)
  312. }
  313. }
  314. logger.PrintAndLog("Cluster", "Map/reduce "+parent.Spec.Name+" failed: "+reason, nil)
  315. }
  316. // ErrNoMapper is returned when a map/reduce script is incomplete.
  317. var ErrNoMapper = errors.New("a map/reduce script must define mapper(files, emit) and reducer(key, values)")