// gpsHandler is the handler of the requests originating from (GPS) clients // reporting GPS coordinates, start/stop events. func gpsHandler(w http.ResponseWriter, r *http.Request) { c := appengine.NewContext(r) // General logs for all requests c.Debugf("Location: %s;%s;%s;%s", r.Header.Get("X-AppEngine-Country"), r.Header.Get("X-AppEngine-Region"), r.Header.Get("X-AppEngine-City"), r.Header.Get("X-AppEngine-CityLatLong")) // If device id is invalid, we want to return sliently and not let the client know about it. // So first check non-user related params first, because else if the client would intentionally // provide an invalid param and get no error, he/she would know that the device id is invalid. RandID := r.FormValue("dev") if RandID == "" { c.Errorf("Missing Device ID (dev) parameter!") http.Error(w, "Missing Device ID (dev) parameter!", http.StatusBadRequest) return } // Do a check for RandID length: it is used to construct a memcache key which has a 250 bytes limit! if len(RandID) > 100 { c.Errorf("Invalid Device ID (dev) parameter!") http.Error(w, "Invalid Device ID (dev) parameter!", http.StatusBadRequest) return } gps := ds.GPS{Created: time.Now()} var err error tracker := r.FormValue("tracker") if tracker != "" { // Start-stop event switch tracker { case "start": gps.AreaCodes = []int64{int64(ds.EvtStart)} case "stop": gps.AreaCodes = []int64{int64(ds.EvtStop)} default: c.Errorf("Invalid tracker parameter!") http.Error(w, "Invalid tracker parameter!", http.StatusBadRequest) return } } else { // GPS coordinates; lat must be in range -90..90, lng must be in range -180..180 gps.GeoPoint.Lat, err = strconv.ParseFloat(r.FormValue("lat"), 64) if err != nil { c.Errorf("Missing or invalid latitude (lat) parameter!") http.Error(w, "Missing or invalid latitude (lat) parameter!", http.StatusBadRequest) return } gps.GeoPoint.Lng, err = strconv.ParseFloat(r.FormValue("lon"), 64) if err != nil { c.Errorf("Missing or invalid longitude (lon) parameter!") http.Error(w, "Missing or invalid longitude (lon) parameter!", http.StatusBadRequest) return } if !gps.GeoPoint.Valid() { c.Errorf("Invalid geopoint specified by latitude (lat) and longitude (lon) parameters (valid range: [-90, 90] latitude and [-180, 180] longitude)!") http.Error(w, "Invalid geopoint specified by latitude (lat) and longitude (lon) parameters (valid range: [-90, 90] latitude and [-180, 180] longitude)!", http.StatusBadRequest) return } } var dev *ds.Device dev, err = cache.GetDevice(c, RandID) if err != nil { c.Errorf("Failed to get Device with cache.GetDevice(): %v", err) http.Error(w, "", http.StatusInternalServerError) return } if dev == nil { // Invalid RandID. Do nothing and return silently. return } gps.DevKeyID = dev.KeyID if tracker == "" && dev.Indexed() { gps.AreaCodes = logic.AreaCodesForGeoPt(dev.AreaSize, gps.GeoPoint.Lat, gps.GeoPoint.Lng) } var gpsKey *datastore.Key if dev.DelOldLogs() { // There is a (positive) Logs Retention for the device. // We could delete old records, but that costs us a lot of Datastore write ops // (exactly the same as saving a new record). // Instead I query for old records (beyond the Logs Retention), // and "resave" to those records (basically save a new record with an existing key). // This way no deletion has to be paid for. // Optimal solution would be to pick the oldest record, but that would require a new index (G: d, t) // which is a "waste", so I just use the existing index (G: d, -t). This will result in the first record // that is just over the Retention period. // Load the existing record by key (strong consistency) and check if its timestamp is truely // beyond the retention (because a concurrent resave might have happened). // Query more than 1 record (limit>1) because if the latest is just concurrently resaved, // we have more records without executing another query. And small operations are free (reading keys). t := time.Now().Add(-24 * time.Hour * time.Duration(dev.LogsRetention)) q := datastore.NewQuery(ds.ENameGPS). Filter(ds.PNameDevKeyID+"=", dev.KeyID). Filter(ds.PNameCreated+"<", t). Order("-" + ds.PNameCreated).KeysOnly().Limit(7) keys, err := q.GetAll(c, nil) if err != nil { c.Errorf("Failed to list GPS records beyond Retention period: %v", err) http.Error(w, "", http.StatusInternalServerError) return } for _, key := range keys { // Load by key (strongly consistent) var oldGps ds.GPS if err = datastore.Get(c, key, &oldGps); err != nil { c.Errorf("Failed to load GPS by Key: %v", err) http.Error(w, "", http.StatusInternalServerError) return } if t.After(oldGps.Created) { // Good: it is still older (not resaved concurrently). We will use this! gpsKey = key break } } } if gpsKey == nil { // No current record to resave, create a new incomplete key for a new record: gpsKey = datastore.NewIncompleteKey(c, ds.ENameGPS, nil) } _, err = datastore.Put(c, gpsKey, &gps) if err != nil { c.Errorf("Failed to store GPS record: %v", err) http.Error(w, "", http.StatusInternalServerError) return } }