// 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) }
// 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)) }
func (g giImpl) RequestID() string { return appengine.RequestID(g.aeCtx) }
// 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 }