func (attempt *attempt) Finish(data map[string]interface{}) error { return attempt.do(func() error { if attempt.status != coordinate.Failed && !attempt.isPending() { return coordinate.ErrNotPending } attempt.finish(coordinate.Finished, data) // Does the work unit data include an "output" key // that we understand? if attempt.workUnit.activeAttempt != attempt { return nil } if data == nil { data = attempt.data } if data == nil { data = attempt.workUnit.data } var newUnits map[string]coordinate.AddWorkUnitItem var nextWorkSpec *workSpec output, ok := data["output"] if ok { newUnits = coordinate.ExtractWorkUnitOutput(output, attempt.Coordinate().clock.Now()) } if newUnits != nil { then := attempt.workUnit.workSpec.meta.NextWorkSpecName if then != "" { nextWorkSpec, ok = attempt.workUnit.workSpec.namespace.workSpecs[then] nextWorkSpec.addWorkUnits(newUnits) } } return nil }) }
func (a *attempt) Finish(data map[string]interface{}) error { // Mark the attempt finished, then create any new work units // declared in an "output" key. // // These do not have to happen atomically. So first, just mark // the attempt as done. err := withTx(a, false, func(tx *sql.Tx) error { return a.complete(tx, data, "finished") }) if err != nil { return err } // A fast path: if we have a data dictionary and there is // no "output", stop. if data != nil { if _, present := data["output"]; !present { return nil } } // Otherwise we maybe have "output". Do one query to the // database that gets back the work unit data (if we need it) // and the matching next work spec. A join could fail, which // would result in nothing coming back, which would be okay. // This also depends on this attempt still being the active // attempt, which again, we can check in the query. params := queryParams{} outputs := []string{ "next.id", "next.name", } tables := []string{ workUnitTable, workSpecTable, workSpecTable + " next", } conditions := []string{ isWorkUnit(¶ms, a.unit.id), workUnitHasAttempt(¶ms, a.id), workUnitInThisSpec, workSpecNextWorkSpec + "=next.name", workSpecNamespace + "=next.namespace_id", } if data == nil { // We need both the most recent attempt data and // the original unit data outputs = append(outputs, workUnitData, attemptData) tables = append(tables, attemptTable) conditions = append(conditions, attemptThisWorkUnit) } query := buildSelect(outputs, tables, conditions) spec := workSpec{namespace: a.unit.spec.namespace} err = withTx(a, true, func(tx *sql.Tx) (err error) { row := tx.QueryRow(query, params...) if data == nil { var unitData, attemptData []byte err = row.Scan(&spec.id, &spec.name, &unitData, &attemptData) if err == nil { if attemptData != nil { data, err = bytesToMap(attemptData) } else if unitData != nil { data, err = bytesToMap(unitData) } else { data = map[string]interface{}{} } } } else { err = row.Scan(&spec.id, &spec.name) } return }) // Now, either that query failed, or we have both work unit // data and a next work spec. if err == sql.ErrNoRows { // As a reminder: // * a isn't the active attempt; or // * spec["then"] points nowhere // In any case, no outputs and we're done return nil } if err != nil { return err } units := coordinate.ExtractWorkUnitOutput(data["output"], a.Coordinate().clock.Now()) if units == nil { return nil // nothing to do } for name, item := range units { var dataBytes []byte dataBytes, err = mapToBytes(item.Data) if err != nil { return err } _, err = spec.addWorkUnit(name, dataBytes, item.Meta) if err != nil { return err } } return nil }