Beispiel #1
0
// getWorkFromSpec forcibly retrieves a work unit from a work spec.
// It could create a work unit if spec is a continuous spec with no
// available units.  It ignores other constraints, such as whether the
// work spec is paused.
func (w *worker) getWorkFromSpec(spec *workSpec, meta *coordinate.WorkSpecMeta) *attempt {
	var unit *workUnit
	now := w.Coordinate().clock.Now()
	if len(spec.available) != 0 {
		unit = spec.available.Next()
	} else if meta.CanStartContinuous(now) {
		// Make a brand new work unit.  Its key is the string
		// form of a time_t.
		seconds := now.Unix()
		nano := now.Nanosecond()
		milli := nano / 1000000
		name := fmt.Sprintf("%d.%03d", seconds, milli)
		var exists bool
		unit, exists = spec.workUnits[name]
		if !exists {
			unit = &workUnit{
				name:     name,
				data:     map[string]interface{}{},
				workSpec: spec,
			}
			spec.workUnits[name] = unit
		}
		spec.meta.NextContinuous = now.Add(meta.Interval)
	} else {
		return nil
	}
	return w.makeAttempt(unit, time.Duration(0))
}
Beispiel #2
0
func (spec *workSpec) SetMeta(meta coordinate.WorkSpecMeta) error {
	return spec.do(func() error {
		// Preserve immutable fields (taking advantage of meta pass-by-value)
		meta.CanBeContinuous = spec.meta.CanBeContinuous
		meta.NextWorkSpecName = spec.meta.NextWorkSpecName
		meta.Runtime = spec.meta.Runtime

		// If this cannot be continuous, force-clear that flag
		if !meta.CanBeContinuous {
			meta.Continuous = false
		}

		spec.meta = meta
		return nil
	})
}
Beispiel #3
0
// TestMetaContinuous specifically checks that you cannot enable the
// "continuous" flag on non-continuous work specs.
func TestMetaContinuous(t *testing.T) {
	var (
		err       error
		namespace coordinate.Namespace
		spec      coordinate.WorkSpec
		meta      coordinate.WorkSpecMeta
	)

	namespace, err = Coordinate.Namespace("TestMetaContinuous")
	if !assert.NoError(t, err) {
		return
	}
	defer namespace.Destroy()

	spec, err = namespace.SetWorkSpec(map[string]interface{}{
		"name":   "spec",
		"min_gb": 1,
	})
	if !assert.NoError(t, err) {
		return
	}

	meta, err = spec.Meta(false)
	if assert.NoError(t, err) {
		assert.False(t, meta.Continuous)
		assert.False(t, meta.CanBeContinuous)
	}

	meta.Continuous = true
	err = spec.SetMeta(meta)
	assert.NoError(t, err)

	meta, err = spec.Meta(false)
	if assert.NoError(t, err) {
		// Cannot set the "continuous" flag
		assert.False(t, meta.Continuous)
		assert.False(t, meta.CanBeContinuous)
	}
}
Beispiel #4
0
// ControlWorkSpec makes changes to a work spec that are not directly
// reflected in the work spec definition.  This allows work specs to
// be paused or to stop generating new continuous jobs.
// ControlWorkSpecOptions has a complete listing of what can be done.
func (jobs *JobServer) ControlWorkSpec(workSpecName string, options map[string]interface{}) (bool, string, error) {
	var (
		cwsOptions ControlWorkSpecOptions
		decoder    *mapstructure.Decoder
		err        error
		metadata   mapstructure.Metadata
		workSpec   coordinate.WorkSpec
		wsMeta     coordinate.WorkSpecMeta
	)

	workSpec, err = jobs.Namespace.WorkSpec(workSpecName)
	if err == nil {
		// We care a lot about "false" vs. not present for
		// these things.  Manually create the decoder.
		config := mapstructure.DecoderConfig{
			Result:   &cwsOptions,
			Metadata: &metadata,
		}
		decoder, err = mapstructure.NewDecoder(&config)
	}
	if err == nil {
		err = decoder.Decode(options)
	}

	// Get the existing metadata, then change it based on what
	// we got provided
	if err == nil {
		wsMeta, err = workSpec.Meta(false)
	}
	if err == nil {
		for _, key := range metadata.Keys {
			switch key {
			case "Continuous":
				wsMeta.Continuous = cwsOptions.Continuous
			case "Status":
				wsMeta.Paused = cwsOptions.Status == Paused
			case "Weight":
				wsMeta.Weight = cwsOptions.Weight
			case "Interval":
				wsMeta.Interval = time.Duration(cwsOptions.Interval) * time.Second
			case "MaxRunning":
				wsMeta.MaxRunning = cwsOptions.MaxRunning
			}
		}
	}
	if err == nil {
		err = workSpec.SetMeta(wsMeta)
	}
	return err == nil, "", err
}
Beispiel #5
0
func (w *worker) requestAttemptsForSpec(req coordinate.AttemptRequest, spec *workSpec, meta *coordinate.WorkSpecMeta) ([]coordinate.Attempt, error) {
	var (
		attempts []coordinate.Attempt
		count    int
		err      error
	)

	// Adjust the work unit count based on what's possible here
	count = req.NumberOfWorkUnits
	if count < 1 {
		count = 1
	}
	if meta.MaxAttemptsReturned > 0 && count > meta.MaxAttemptsReturned {
		count = meta.MaxAttemptsReturned
	}
	if meta.MaxRunning > 0 && count > meta.MaxRunning-meta.PendingCount {
		count = meta.MaxRunning - meta.PendingCount
	}

	continuous := false
	length := time.Duration(15) * time.Minute
	err = withTx(w, false, func(tx *sql.Tx) error {
		var err error
		now := w.Coordinate().clock.Now()

		// Take an advisory lock for the work spec.  It doesn't
		// make sense for multiple concurrent actors to progress
		// beyond this point (they will hit the same work units
		// in chooseAndMakeAttempts() and one will roll back) and
		// this reduces the database load.  We are still protected
		// by standard SQL transactionality.
		params := queryParams{}
		query := "SELECT pg_advisory_xact_lock(0, " + params.Param(spec.id) + ")"
		_, err = tx.Exec(query, params...)
		if err != nil {
			return err
		}

		// Try to create attempts from pre-existing work units
		// (assuming we expect there to be some)
		if meta.AvailableCount > 0 {
			attempts, err = w.chooseAndMakeAttempts(tx, spec, count, now, length)
		}
		if err != nil || len(attempts) > 0 {
			return err
		}

		// If there were none, but the selected work spec is
		// continuous, maybe we can create a work unit and an
		// attempt
		if meta.CanStartContinuous(now) {
			var unit *workUnit
			var attempt *attempt
			continuous = true
			unit, err = w.createContinuousUnit(tx, spec, meta, now)
			if err == nil && unit != nil {
				attempt, err = makeAttempt(tx, unit, w, length)
			}
			if err == nil && attempt != nil {
				attempts = []coordinate.Attempt{attempt}
			}
		}

		// Whatever happened, end of the road
		return err
	})
	// On a very bad day, we could have gone to create continuous
	// work units, but an outright INSERT failed from a duplicate
	// key.  If this happened, just pretend we didn't actually get
	// any attempts back, which will trigger the retry loop in our
	// caller.
	if continuous && err != nil && isDuplicateUnitName(err) {
		attempts = nil
		err = nil
	}
	return attempts, err
}
Beispiel #6
0
// AllMetas retrieves the metadata for all work specs.  This is
// expected to run within a pre-existing transaction.  On success,
// returns maps from work spec name to work spec object and to
// metadata object.
func (ns *namespace) allMetas(tx *sql.Tx, withCounts bool) (map[string]*workSpec, map[string]*coordinate.WorkSpecMeta, error) {
	params := queryParams{}
	query := buildSelect([]string{
		workSpecID,
		workSpecName,
		workSpecPriority,
		workSpecWeight,
		workSpecPaused,
		workSpecContinuous,
		workSpecCanBeContinuous,
		workSpecMinMemoryGb,
		workSpecInterval,
		workSpecNextContinuous,
		workSpecMaxRunning,
		workSpecMaxAttemptsReturned,
		workSpecNextWorkSpec,
		workSpecRuntime,
	}, []string{
		workSpecTable,
	}, []string{
		workSpecInNamespace(&params, ns.id),
	})
	rows, err := tx.Query(query, params...)
	if err != nil {
		return nil, nil, err
	}
	specs := make(map[string]*workSpec)
	metas := make(map[string]*coordinate.WorkSpecMeta)
	err = scanRows(rows, func() error {
		var (
			spec           workSpec
			meta           coordinate.WorkSpecMeta
			interval       string
			nextContinuous pq.NullTime
			err            error
		)
		err = rows.Scan(&spec.id, &spec.name, &meta.Priority,
			&meta.Weight, &meta.Paused, &meta.Continuous,
			&meta.CanBeContinuous, &meta.MinMemoryGb,
			&interval, &nextContinuous, &meta.MaxRunning,
			&meta.MaxAttemptsReturned,
			&meta.NextWorkSpecName, &meta.Runtime)
		if err != nil {
			return err
		}
		spec.namespace = ns
		meta.NextContinuous = nullTimeToTime(nextContinuous)
		meta.Interval, err = sqlToDuration(interval)
		if err != nil {
			return err
		}
		specs[spec.name] = &spec
		metas[spec.name] = &meta
		return nil
	})
	if err != nil {
		return nil, nil, err
	}
	if withCounts {
		// A single query that selects both "available" and
		// "pending" is hopelessly expensive.  Also, in the
		// only place this is called (in RequestAttempts) we
		// need to know whether or not there are any available
		// attempts, but we don't really care how many there
		// are so long as there are more than zero.
		//
		// Pending:
		params = queryParams{}
		query = buildSelect([]string{workSpecName, "COUNT(*)"},
			[]string{workSpecTable, attemptTable},
			[]string{
				workSpecInNamespace(&params, ns.id),
				attemptInThisSpec,
				attemptIsPending,
			})
		query += " GROUP BY " + workSpecName
		rows, err = tx.Query(query, params...)
		if err != nil {
			return nil, nil, err
		}
		err = scanRows(rows, func() error {
			var name string
			var count int
			err := rows.Scan(&name, &count)
			if err == nil {
				metas[name].PendingCount = count
			}
			return err
		})

		// Available count (0/1):
		now := ns.Coordinate().clock.Now()
		params = queryParams{}
		query = buildSelect([]string{
			workUnitSpec,
		}, []string{
			workUnitTable,
		}, []string{
			workUnitHasNoAttempt,
			"NOT " + workUnitTooSoon(&params, now),
		})
		query = buildSelect([]string{
			workSpecName,
		}, []string{
			workSpecTable,
		}, []string{
			workSpecInNamespace(&params, ns.id),
			workSpecID + " IN (" + query + ")",
		})
		rows, err = tx.Query(query, params...)
		err = scanRows(rows, func() error {
			var name string
			err := rows.Scan(&name)
			if err == nil {
				metas[name].AvailableCount = 1
			}
			return err
		})
		if err != nil {
			return nil, nil, err
		}
	}
	return specs, metas, nil
}
Beispiel #7
0
func (spec *workSpec) Meta(withCounts bool) (coordinate.WorkSpecMeta, error) {
	// If we need counts, we need to run expiry so that the
	// available/pending counts are rightish
	if withCounts {
		spec.Coordinate().Expiry.Do(spec)
	}
	var meta coordinate.WorkSpecMeta
	err := withTx(spec, true, func(tx *sql.Tx) error {
		var (
			params         queryParams
			query          string
			interval       string
			nextContinuous pq.NullTime
		)
		query = buildSelect([]string{
			workSpecPriority,
			workSpecWeight,
			workSpecPaused,
			workSpecContinuous,
			workSpecCanBeContinuous,
			workSpecMinMemoryGb,
			workSpecInterval,
			workSpecNextContinuous,
			workSpecMaxRunning,
			workSpecMaxAttemptsReturned,
			workSpecNextWorkSpec,
			workSpecRuntime,
		}, []string{
			workSpecTable,
		}, []string{
			isWorkSpec(&params, spec.id),
		})
		row := tx.QueryRow(query, params...)
		err := row.Scan(
			&meta.Priority,
			&meta.Weight,
			&meta.Paused,
			&meta.Continuous,
			&meta.CanBeContinuous,
			&meta.MinMemoryGb,
			&interval,
			&nextContinuous,
			&meta.MaxRunning,
			&meta.MaxAttemptsReturned,
			&meta.NextWorkSpecName,
			&meta.Runtime,
		)
		if err == sql.ErrNoRows {
			return coordinate.ErrGone
		}
		if err != nil {
			return err
		}
		meta.NextContinuous = nullTimeToTime(nextContinuous)
		meta.Interval, err = sqlToDuration(interval)
		if err != nil {
			return err
		}

		// Find counts with a second query, if requested
		if !withCounts {
			return nil
		}
		params = queryParams{}
		query = buildSelect([]string{
			attemptStatus,
			"COUNT(*)",
		}, []string{
			workUnitAttemptJoin,
		}, []string{
			workUnitInSpec(&params, spec.id),
		})
		query += " GROUP BY " + attemptStatus
		rows, err := tx.Query(query, params...)
		if err != nil {
			return err
		}
		return scanRows(rows, func() error {
			var status sql.NullString
			var count int
			err := rows.Scan(&status, &count)
			if err != nil {
				return err
			}
			if !status.Valid {
				meta.AvailableCount += count
			} else {
				switch status.String {
				case "expired":
					meta.AvailableCount += count
				case "retryable":
					meta.AvailableCount += count
				case "pending":
					meta.PendingCount += count
				}
			}
			return nil
		})
	})
	return meta, err
}