| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345 |
- package jobs
- /*
- Map / Reduce on top of the job runtime.
- A map/reduce job carries one script that defines both halves:
- function mapper(files, emit) { // files = paths of this partition
- files.forEach(function(f){ emit(keyFor(f), 1); });
- }
- function reducer(key, values) { return values.length; }
- The leader expands the dataset glob over the namespace, groups the files
- by the node that already holds a healthy copy (so map work runs where the
- bytes are), splits each group into partitions and submits one child map
- job per partition. When every map child has succeeded it groups the
- emitted pairs by key and submits a single reduce child; the reduce output
- becomes the parent's output.
- Parent jobs are coordinated by the leader and never assigned to a node.
- */
- import (
- "encoding/json"
- "errors"
- "sort"
- "strings"
- "time"
- "imuslab.com/arozos/mod/cluster/metadata"
- "imuslab.com/arozos/mod/info/logger"
- )
- const defaultPartitionMax = 50
- // mapOutput is one emitted pair from a mapper.
- type mapOutput [2]json.RawMessage
- // reduceArgs is what a reduce child receives.
- type reduceArgs struct {
- Groups map[string][]json.RawMessage `json:"groups"`
- }
- // buildMapSource wraps a script for the map half.
- func buildMapSource(script string) string {
- return jobPrelude + "\n" + script + `
- if (typeof mapper !== "function") { throw new Error("this map/reduce script defines no mapper(files, emit) function"); }
- var __emitted = [];
- function emit(k, v) { __emitted.push([String(k), v === undefined ? null : v]); }
- mapper(JOB.inputs, emit);
- _job_output(JSON.stringify(__emitted));
- `
- }
- // buildReduceSource wraps a script for the reduce half.
- func buildReduceSource(script string) string {
- return jobPrelude + "\n" + script + `
- if (typeof reducer !== "function") { throw new Error("this map/reduce script defines no reducer(key, values) function"); }
- var __groups = (JOB.args && JOB.args.groups) ? JOB.args.groups : {};
- var __out = {};
- var __keys = Object.keys(__groups);
- for (var __i = 0; __i < __keys.length; __i++) {
- JOB.progress(__i / Math.max(1, __keys.length));
- __out[__keys[__i]] = reducer(__keys[__i], __groups[__keys[__i]]);
- }
- _job_output(JSON.stringify(__out));
- `
- }
- // sourceFor picks the right wrapper for a job kind.
- func sourceFor(rec Record) string {
- switch rec.Spec.Kind {
- case KindMap:
- return buildMapSource(rec.Spec.Script)
- case KindReduce:
- return buildReduceSource(rec.Spec.Script)
- }
- return buildSource(rec.Spec.Script)
- }
- // children returns the child jobs of a parent.
- func (j *Manager) children(parentID string) []Record {
- out := []Record{}
- for _, mj := range j.meta.Jobs() {
- if mj.Removed {
- continue
- }
- rec, ok := j.decode(&mj)
- if !ok || rec.Spec.ParentID != parentID {
- continue
- }
- out = append(out, rec)
- }
- sort.Slice(out, func(a, b int) bool { return out[a].Spec.Created < out[b].Spec.Created })
- return out
- }
- // expandDataset lists the files of a dataset and groups them by the node
- // holding a healthy copy (files with no reachable copy are reported).
- func (j *Manager) expandDataset(pattern string) (byNode map[string][]metadata.FileRecord, orphans []string) {
- byNode = map[string][]metadata.FileRecord{}
- for _, rec := range j.meta.Glob(pattern) {
- best := ""
- for _, loc := range rec.HealthyLocations() {
- for _, n := range j.m.NodeViews() {
- if n.ID == loc.NodeID && usableState(n.State) {
- best = loc.NodeID
- break
- }
- }
- if best != "" {
- break
- }
- }
- if best == "" {
- orphans = append(orphans, rec.Path)
- continue
- }
- byNode[best] = append(byNode[best], rec)
- }
- return
- }
- // startMapReduce expands the dataset and submits the map children. It runs
- // once per parent: submitting a child re-enters the scheduler, so the parent
- // is marked running before any child exists.
- func (j *Manager) startMapReduce(parent Record) {
- id := parent.Spec.ID
- j.mu.Lock()
- if j.starting[id] {
- j.mu.Unlock()
- return
- }
- j.starting[id] = true
- j.mu.Unlock()
- defer func() {
- j.mu.Lock()
- delete(j.starting, id)
- j.mu.Unlock()
- }()
- latest, ok := j.Get(id)
- if !ok || latest.State.Status != StatusQueued {
- return //another pass already expanded it
- }
- parent = latest
- pattern := strings.TrimSpace(parent.Spec.Dataset)
- if pattern == "" {
- j.failParent(parent, "the job has no dataset pattern")
- return
- }
- byNode, orphans := j.expandDataset(pattern)
- if len(orphans) > 0 {
- list := strings.Join(orphans, ", ")
- if len(list) > 300 {
- list = list[:300] + "..."
- }
- j.failParent(parent, "no reachable copy of: "+list)
- return
- }
- partitionMax := parent.Spec.PartitionMax
- if partitionMax <= 0 {
- partitionMax = defaultPartitionMax
- }
- nodes := make([]string, 0, len(byNode))
- for id := range byNode {
- nodes = append(nodes, id)
- }
- sort.Strings(nodes)
- //Claim the parent before creating children, so the scheduler pass that
- //that each child submission triggers cannot start it a second time.
- parent.State.Status = StatusRunning
- parent.State.Started = time.Now().Unix()
- parent.State.Progress = 0
- if err := j.put(parent); err != nil {
- return
- }
- created := 0
- for _, node := range nodes {
- files := byNode[node]
- for start := 0; start < len(files); start += partitionMax {
- end := start + partitionMax
- if end > len(files) {
- end = len(files)
- }
- inputs := []string{}
- for _, f := range files[start:end] {
- inputs = append(inputs, f.Path)
- }
- child := parent.Spec
- child.ID = ""
- child.Kind = KindMap
- child.ParentID = parent.Spec.ID
- child.Name = parent.Spec.Name + " map " + itoa(created+1)
- child.Inputs = inputs
- child.Dataset = ""
- if _, err := j.Submit(child); err != nil {
- j.failParent(parent, "could not create a map task: "+err.Error())
- return
- }
- created++
- }
- }
- if created == 0 {
- //Nothing matched: succeed with an empty result
- parent, _ = j.Get(parent.Spec.ID)
- parent.State.Status = StatusSucceeded
- parent.State.Progress = 1
- parent.State.Output = json.RawMessage(`{}`)
- parent.State.Finished = time.Now().Unix()
- parent.State.Log = append(parent.State.Log, "dataset matched no files")
- j.put(parent)
- j.finishedEvent(parent)
- return
- }
- parent, _ = j.Get(parent.Spec.ID)
- parent.State.Log = append(parent.State.Log, "expanded "+pattern+" into "+itoa(created)+" map task(s) across "+itoa(len(nodes))+" node(s)")
- j.put(parent)
- logger.PrintAndLog("Cluster", "Map/reduce "+parent.Spec.Name+" started with "+itoa(created)+" map tasks", nil)
- }
- // advanceMapReduce moves a running parent forward: it checks the children,
- // creates the reduce task when the map half is complete, and publishes the
- // final result.
- func (j *Manager) advanceMapReduce(parent Record) {
- kids := j.children(parent.Spec.ID)
- if len(kids) == 0 {
- return
- }
- var maps, reduces []Record
- for _, k := range kids {
- if k.Spec.Kind == KindReduce {
- reduces = append(reduces, k)
- } else {
- maps = append(maps, k)
- }
- }
- //A failed or cancelled child fails the whole job
- for _, k := range kids {
- if k.State.Status == StatusFailed || k.State.Status == StatusCancelled {
- j.failParent(parent, k.Spec.Name+": "+firstNonEmpty(k.State.Error, k.State.Status))
- return
- }
- }
- doneMaps := 0
- for _, k := range maps {
- if k.State.Status == StatusSucceeded {
- doneMaps++
- }
- }
- total := len(maps) + 1 //the reduce task counts as one more step
- progress := float64(doneMaps) / float64(total)
- //Reduce already running or finished?
- if len(reduces) > 0 {
- red := reduces[0]
- switch red.State.Status {
- case StatusSucceeded:
- parent.State.Status = StatusSucceeded
- parent.State.Progress = 1
- parent.State.Output = red.State.Output
- parent.State.Finished = time.Now().Unix()
- parent.State.Node = red.State.Node
- j.put(parent)
- j.finishedEvent(parent)
- logger.PrintAndLog("Cluster", "Map/reduce "+parent.Spec.Name+" finished", nil)
- default:
- parent.State.Progress = float64(len(maps)) / float64(total)
- j.put(parent)
- }
- return
- }
- if doneMaps < len(maps) {
- if parent.State.Progress != progress {
- parent.State.Progress = progress
- j.put(parent)
- }
- return
- }
- //Every map task is done: group the emitted pairs by key
- groups := map[string][]json.RawMessage{}
- for _, k := range maps {
- var emitted []mapOutput
- if len(k.State.Output) == 0 {
- continue
- }
- if err := json.Unmarshal(k.State.Output, &emitted); err != nil {
- j.failParent(parent, k.Spec.Name+" produced an unreadable result")
- return
- }
- for _, pair := range emitted {
- key := strings.Trim(string(pair[0]), `"`)
- groups[key] = append(groups[key], pair[1])
- }
- }
- args, err := json.Marshal(reduceArgs{Groups: groups})
- if err != nil {
- j.failParent(parent, "could not assemble the reduce input: "+err.Error())
- return
- }
- if len(args) > 8<<20 {
- j.failParent(parent, "the mappers emitted more than 8 MB; narrow the dataset or emit fewer values")
- return
- }
- child := parent.Spec
- child.ID = ""
- child.Kind = KindReduce
- child.ParentID = parent.Spec.ID
- child.Name = parent.Spec.Name + " reduce"
- child.Inputs = nil
- child.Dataset = ""
- child.Args = json.RawMessage(args)
- if _, err := j.Submit(child); err != nil {
- j.failParent(parent, "could not create the reduce task: "+err.Error())
- return
- }
- parent.State.Progress = float64(len(maps)) / float64(total)
- parent.State.Log = append(parent.State.Log, "mapped "+itoa(len(groups))+" key(s), reducing")
- j.put(parent)
- }
- func (j *Manager) failParent(parent Record, reason string) {
- parent.State.Status = StatusFailed
- parent.State.Error = reason
- parent.State.Finished = time.Now().Unix()
- j.put(parent)
- j.finishedEvent(parent)
- //Cancel anything still pending underneath
- for _, k := range j.children(parent.Spec.ID) {
- if !k.Done() {
- j.Cancel(k.Spec.ID, k.Spec.Owner, true)
- }
- }
- logger.PrintAndLog("Cluster", "Map/reduce "+parent.Spec.Name+" failed: "+reason, nil)
- }
- // ErrNoMapper is returned when a map/reduce script is incomplete.
- var ErrNoMapper = errors.New("a map/reduce script must define mapper(files, emit) and reducer(key, values)")
|