// StartJob launches a job on the given queue. It is not executed immediately but
// scheduled to run as a task which performs splitting of the input reader based
// on the number of shards.
func (m *mapper) startJobHandler(w http.ResponseWriter, r *http.Request) {
	c := appengine.NewContext(r)

	values := r.URL.Query()
	name := values.Get("name")
	jobSpec, err := CreateJobInstance(name)
	if err != nil {
		return
	}

	shards, err := strconv.Atoi(values.Get("shards"))
	if shards == 0 || err != nil {
		shards = m.config.Shards
	}

	queue := values.Get("queue")
	if queue != "" {
		// override the queue for this request
		// (used by locker.Schedule later)
		c = locker.WithQueue(c, queue)
	}
	bucket := values.Get("bucket")

	query, err := jobSpec.Query(r)
	if err != nil {
		log.Errorf(c, "error creating query %v", err)
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	requestHash := r.Header.Get("X-Appengine-Request-Id-Hash")
	if requestHash == "" {
		// this should only happen when testing, we just need a short hash
		requestID := appengine.RequestID(c)
		requestHash = strconv.FormatUint(uint64(adler32.Checksum([]byte(requestID))), 16)
	}

	id := fmt.Sprintf("%s/%s", name, requestHash)
	job := &job{
		JobName:   name,
		JobSpec:   jobSpec,
		Bucket:    bucket,
		Shards:    shards,
		Iterating: true,
	}
	job.common.start(query)

	key := datastore.NewKey(c, m.config.DatastorePrefix+jobKind, id, 0, nil)
	m.locker.Schedule(c, key, job, m.config.Path+jobURL, nil)
}
Example #2
0
// deal makes the response in error cases somewhat nicer. It will try
// to figure out what actually went wrong and inform the user.
// It should not be called if the request went fine. If status is below
// 400, and err is not nil, it will assume an internal server error.
// Generally, if you pass a nil error, don't expect deal to do anything
// useful.
func deal(ctx context.Context, w http.ResponseWriter, r *http.Request, status int, err error) {
	// Getting an error and a status code blow 400 is somewhat paradox.
	// Also, if the status is the zero value, assume that we're dealing
	// with an internal server error.
	if err != nil && status < 400 || status == 0 {
		status = http.StatusInternalServerError
	}

	msg := "Sorry, Coduno encountered an error: " + http.StatusText(status) + "\n\n"
	msg += "If you think this is a bug, please consider filing\n"
	msg += "it at https://github.com/coduno/api/issues\n\n"

	if ctx != nil {
		msg += "Your request ID is " + appengine.RequestID(ctx) + " (important to track down what went wrong)\n\n"
	}

	// If we don't have an error it's really hard to make sense.
	if err == nil {
		w.WriteHeader(status)
		w.Write([]byte(msg))
		return
	}

	if t, ok := err.(trace); ok {
		msg += "Trace:\n"
		msg += strings.Replace(string(t.t), "\n", "\n\t", -1)
		msg += "\n"
		err = t.e
	}

	if appengine.IsOverQuota(err) {
		msg += "Reason: Over Quota"
	} else if appengine.IsTimeoutError(err) {
		msg += "Reason: Timeout Error"
	} else {
		msg += fmt.Sprintf("Reason: %s", err)
	}

	w.WriteHeader(status)
	w.Write([]byte(msg))
}
Example #3
0
func (g giImpl) RequestID() string {
	return appengine.RequestID(g.aeCtx)
}
Example #4
0
// Aquire attempts to get and lock an entity with the given identifier
// If successful it will write a new lock entity to the datastore
// and return nil, otherwise it will return an error to indicate
// the reason for failure.
func (l *Locker) Aquire(c context.Context, key *datastore.Key, entity Lockable, sequence int) error {
	requestID := appengine.RequestID(c)
	lock := new(Lock)
	success := false

	// we need to run in a transaction for consistency guarantees
	// in case two tasks start at the exact same moment and each
	// of them sees no lock in place
	err := storage.RunInTransaction(c, func(tc context.Context) error {
		// reset flag here in case of transaction retries
		success = false

		if err := storage.Get(tc, key, entity); err != nil {
			return err
		}

		// we got the entity successfully, check if it's locked
		// and try to claim the lease if it isn't
		lock = entity.getLock()
		if lock.RequestID == "" && lock.Sequence == sequence {
			lock.Timestamp = getTime()
			lock.RequestID = requestID
			if _, err := storage.Put(tc, key, entity); err != nil {
				return err
			}
			success = true
			return nil
		}

		// lock already exists, return nil because there is no point doing
		// any more retries but we'll need to figure out if we can claim it
		return nil
	}, nil)

	// if there was any error then we failed to get the lock due to datastore
	// errors. Returning an error indicates to the caller that they should mark
	// the task as failed so it will be re-attempted
	if err != nil {
		return ErrLockFailed
	}

	// success is true if we got the lock
	if success {
		return nil
	}

	// If there wasn't any error but we weren't successful then a lock is
	// already in place. We're most likely here because a duplicate task has
	// been scheduled or executed so we need to examine the lock itself
	log.Debugf(c, "lock %v %d %d %s", lock.Timestamp, lock.Sequence, lock.Retries, lock.RequestID)

	// if the lock sequence is already past this task so it should be dropped
	if lock.Sequence > sequence {
		return ErrTaskExpired
	}

	// if the lock is within the lease duration we return that it's locked so
	// that this task will be retried
	if lock.Timestamp.Add(l.LeaseDuration).After(getTime()) {
		return ErrLockFailed
	}

	// if the lock has been held for longer than the lease duration then we
	// start querying the logs api to see if the previous request completed.
	// if it has then we will be overwriting the lock. It's possible that the
	// log entry is missing or we simply don't have access to them (managed VM)
	// so the lease timeout is a failsafe to catch extreme undetectable failures
	if lock.Timestamp.Add(l.LeaseTimeout).Before(getTime()) && l.previousRequestEnded(c, lock.RequestID) {
		if err := l.overwriteLock(c, key, entity, requestID); err == nil {
			// success (at least we grabbed the lock)
			return nil
		}
	}

	return ErrLockFailed
}