// GetWork requests one or more work units to perform. The work unit // attempts are associated with workerID, which need not have been // previously registered. If there is no work to do, may return // neither work nor an error. // // Each work unit is returned as a cborrpc.PythonTuple holding the // work spec name, work unit key as a byte slice, and work unit data // dictionary. If options does not contain "max_jobs" or if that // value is 1, returns a tuple or nil, otherwise returns a slice of // tuples (maybe 1 or none). func (jobs *JobServer) GetWork(workerID string, options map[string]interface{}) (interface{}, string, error) { // This is the Big Kahuna. The Python Coordinate server tries // to be extra clever with its return value, returning None if // there is no work, a concrete value if one work unit was // requested, and a list if more than one was requested, and // this same rule is enforced in the client code. So, this will // return either exactly one PythonTuple or a list of PythonTuple. var ( attempts []coordinate.Attempt err error gwOptions GetWorkOptions worker coordinate.Worker ) err = decode(&gwOptions, options) if err == nil { worker, err = jobs.Namespace.Worker(workerID) } if err == nil { if gwOptions.MaxJobs < 1 { gwOptions.MaxJobs = 1 } req := coordinate.AttemptRequest{ NumberOfWorkUnits: gwOptions.MaxJobs, Runtimes: []string{""}, WorkSpecs: gwOptions.WorkSpecNames, } attempts, err = worker.RequestAttempts(req) } if err != nil { return nil, "", err } // successful return if gwOptions.MaxJobs == 1 { if len(attempts) == 0 { tuple := cborrpc.PythonTuple{ Items: []interface{}{nil, nil, nil}, } return tuple, "", nil } if len(attempts) == 1 { tuple, err := getWorkTuple(attempts[0]) if err != nil { return nil, "", err } return tuple, "", nil } } result := make([]cborrpc.PythonTuple, len(attempts)) for i, attempt := range attempts { tuple, err := getWorkTuple(attempt) if err != nil { return nil, "", err } result[i] = tuple } return result, "", nil }
// doWork gets attempts and runs them. It assumes it is running in its // own goroutine. It signals gotWork when the call to RequestAttempts // returns, and signals finished immediately before returning. func (w *Worker) doWork(id string, worker coordinate.Worker, ctx context.Context, gotWork chan<- bool, finished chan<- string) { // When we finish, signal the finished channel with our own ID defer func() { finished <- id }() attempts, err := worker.RequestAttempts(coordinate.AttemptRequest{ Runtimes: []string{"go"}, NumberOfWorkUnits: w.MaxAttempts, }) if err != nil { // Handle the error if we can, but otherwise act just like // we got no attempts back if w.ErrorHandler != nil { w.ErrorHandler(err) } gotWork <- false return } if len(attempts) == 0 { // Nothing to do gotWork <- false return } // Otherwise we have actual work (and at least one attempt). gotWork <- true // See if we can find a task for the work spec spec := attempts[0].WorkUnit().WorkSpec() task := spec.Name() data, err := spec.Data() if err == nil { aTask, present := data["task"] if present { bTask, ok := aTask.(string) if ok { task = bTask } } } // Try to find the task function var taskFn func(context.Context, []coordinate.Attempt) if err == nil { taskFn = w.Tasks[task] if taskFn == nil { err = fmt.Errorf("No such task function %q", task) } } if err == nil { taskCtx, cancellation := context.WithCancel(ctx) w.cancellations[id] = cancellation taskFn(taskCtx, attempts) // It appears to be recommended to call this; calling // it multiple times is documented to have no effect cancellation() } else { failure := map[string]interface{}{ "traceback": err.Error(), } // Try to fail all the attempts, ignoring errors for _, attempt := range attempts { _ = attempt.Fail(failure) } } }