Skip to content

Commit

Permalink
Implements exponential backoff for Mozilla Location Services (#39)
Browse files Browse the repository at this point in the history
A patch to add exponential backoff for all 400 and 500 series errors from Mozilla Location Service.

I've reworked reworked the backend location provider to move
synchronization of variables into methods of the BackendService
that are called through a callback mechanism in the IchnaeaRequester.

The RATE_LIMIT_MS_PADDING time is multiplied by an EXP_BACKOFF_FACTOR to exponentially add time to the backoff timer.
The initial RATE_LIMIT_MS_FLOOR rate has also been reduced to once every 60 seconds.
  • Loading branch information
crankycoder authored and mar-v-in committed Jun 13, 2018
1 parent 8b0483d commit 174e736
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 77 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.1'
classpath 'com.android.tools.build:gradle:2.3.3'
}
}

Expand Down
141 changes: 65 additions & 76 deletions src/main/java/org/microg/nlp/backend/ichnaea/BackendService.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,45 +31,88 @@
import org.microg.nlp.api.LocationHelper;
import org.microg.nlp.api.WiFiBackendHelper;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Set;

import static org.microg.nlp.api.CellBackendHelper.Cell;
import static org.microg.nlp.api.WiFiBackendHelper.WiFi;

public class BackendService extends HelperLocationBackendService
implements WiFiBackendHelper.Listener, CellBackendHelper.Listener {
implements WiFiBackendHelper.Listener,
CellBackendHelper.Listener,
LocationCallback {

private static final String TAG = "IchnaeaBackendService";
private static final String SERVICE_URL = "https://location.services.mozilla.com/v1/geolocate?key=%s";
private static final String API_KEY = "068ab754-c06b-473d-a1e5-60e7b1a2eb77";
private static final long RATE_LIMIT_MS_FLOOR = 60000;
private static final long RATE_LIMIT_MS_PADDING = 10000;
private static final String PROVIDER = "ichnaea";
private static final int RATE_LIMIT_MS = 10000;

private long expBackoffFactor = 0;

private static BackendService instance;

private boolean running = false;
private Set<WiFi> wiFis;
private Set<Cell> cells;
private Thread thread;
private long lastRequestTime = 0;

private boolean useWiFis = true;
private boolean useCells = true;

private boolean replay = false;
private String lastRequest = null;
private Location lastResponse = null;


@Override
public synchronized void onCreate() {
super.onCreate();
reloadSettings();
reloadInstanceSettings();

}

@Override
public synchronized boolean canRun() {
long delay = RATE_LIMIT_MS_FLOOR + (RATE_LIMIT_MS_PADDING * expBackoffFactor);
return (lastRequestTime + delay > System.currentTimeMillis());
}

// Methods to extend or reduce the backoff time

@Override
public synchronized void extendBackoff() {
if (expBackoffFactor == 0) {
expBackoffFactor = 1;
} else if (expBackoffFactor > 0 && expBackoffFactor < 1024) {
expBackoffFactor *= 2;
}
}

@Override
public synchronized void reduceBackoff() {
if (expBackoffFactor == 1) {
// Turn the exponential backoff off entirely
expBackoffFactor = 0;
} else {
expBackoffFactor /= 2;
}
}

@Override
public synchronized void resultCallback(Location locationResult) {
if (locationResult == null) {
if (lastResponse == null) {
// There isn't even a lastResponse to work with
Log.d(TAG, "No previous location to replay");
return;
}
locationResult = LocationHelper.create(PROVIDER, lastResponse.getLatitude(), lastResponse.getLongitude(), lastResponse.getAccuracy());
Log.d(TAG, "Replaying location " + locationResult);
}
lastRequestTime = System.currentTimeMillis();
lastResponse = locationResult;
report(locationResult);
}

@Override
Expand Down Expand Up @@ -117,99 +160,45 @@ protected synchronized void onClose() {
@Override
public void onWiFisChanged(Set<WiFi> wiFis) {
this.wiFis = wiFis;
if (running) startCalculate();
if (running) {
startCalculate();
}
}

@Override
public void onCellsChanged(Set<Cell> cells) {
this.cells = cells;
Log.d(TAG, "Cells: " + cells.size());
if (running) startCalculate();
if (running) {
startCalculate();
}
}

@Override
protected synchronized Location update() {
replay = true; // We need to replay to ensure apps think they are up-to-date.
return super.update();
}

private synchronized void startCalculate() {
if (thread != null) return;
if (lastRequestTime + RATE_LIMIT_MS > System.currentTimeMillis()) return;
final Set<WiFi> wiFis = this.wiFis;
final Set<Cell> cells = this.cells;
if ((cells == null || cells.isEmpty()) && (wiFis == null || wiFis.size() < 2)) return;

try {
final String request = createRequest(cells, wiFis);
if (request.equals(lastRequest)) {
if (replay) {
Log.d(TAG, "No data changes, replaying location " + lastResponse);
lastResponse = LocationHelper.create(PROVIDER, lastResponse.getLatitude(), lastResponse.getLongitude(), lastResponse.getAccuracy());
report(lastResponse);
}
if (!this.canRun()) {
this.resultCallback(null);
return;
} else {
IchnaeaRequester requester = new IchnaeaRequester(this, request);
Thread t = new Thread(requester);
t.start();
}
replay = false;
thread = new Thread(new Runnable() {
@Override
public void run() {
HttpURLConnection conn = null;
Location response = null;
try {
conn = (HttpURLConnection) new URL(String.format(SERVICE_URL, API_KEY)).openConnection();
conn.setDoOutput(true);
conn.setDoInput(true);
Log.d(TAG, "request: " + request);
conn.getOutputStream().write(request.getBytes());
String r = new String(readStreamToEnd(conn.getInputStream()));
Log.d(TAG, "response: " + r);
JSONObject responseJson = new JSONObject(r);
double lat = responseJson.getJSONObject("location").getDouble("lat");
double lon = responseJson.getJSONObject("location").getDouble("lng");
double acc = responseJson.getDouble("accuracy");
response = LocationHelper.create(PROVIDER, lat, lon, (float) acc);
report(response);
} catch (IOException | JSONException e) {
if (conn != null) {
InputStream is = conn.getErrorStream();
if (is != null) {
try {
String error = new String(readStreamToEnd(is));
Log.w(TAG, "Error: " + error);
} catch (Exception ignored) {
}
}
}
Log.w(TAG, e);
}

lastRequest = request;
lastResponse = response;
lastRequestTime = System.currentTimeMillis();
thread = null;
}
});
thread.start();
} catch (Exception e) {
Log.w(TAG, e);
}
}

private static byte[] readStreamToEnd(InputStream is) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
if (is != null) {
byte[] buff = new byte[1024];
while (true) {
int nb = is.read(buff);
if (nb < 0) {
break;
}
bos.write(buff, 0, nb);
}
is.close();
}
return bos.toByteArray();
}

/**
* see https://mozilla-ichnaea.readthedocs.org/en/latest/cell.html
Expand Down
119 changes: 119 additions & 0 deletions src/main/java/org/microg/nlp/backend/ichnaea/IchnaeaRequester.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* Copyright (C) 2013-2017 microG Project Team
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.microg.nlp.backend.ichnaea;

import android.app.DownloadManager;
import android.location.Location;
import android.util.Log;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.microg.nlp.api.CellBackendHelper;
import org.microg.nlp.api.HelperLocationBackendService;
import org.microg.nlp.api.LocationHelper;
import org.microg.nlp.api.WiFiBackendHelper;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Set;

/*
* This class implements the runnable portion of a thread which
* accepts requests to process a location request and returns results
* via a callback
*/
public class IchnaeaRequester implements Runnable {

private static final String TAG = "IchnaeaBackendService";
private static final String SERVICE_URL = "https://location.services.mozilla.com/v1/geolocate?key=%s";
private static final String API_KEY = "068ab754-c06b-473d-a1e5-60e7b1a2eb77";
private static final String PROVIDER = "ichnaea";

private LocationCallback callback = null;
private String request;

public IchnaeaRequester(LocationCallback backendService, String request) {
this.callback = backendService;
this.request = request;
}


public void run() {
HttpURLConnection conn = null;
Location response = null;
try {
conn = (HttpURLConnection) new URL(String.format(SERVICE_URL, API_KEY)).openConnection();
conn.setDoOutput(true);
conn.setDoInput(true);
Log.d(TAG, "request: " + request);
conn.getOutputStream().write(request.getBytes());
int respCode = conn.getResponseCode();
if ((respCode >= 400) && (respCode <= 599)) {
// Increase exponential backoff time for
// any 400 or 500 series status code
this.callback.extendBackoff();
this.callback.resultCallback(null);
return;
} else {
// Adjust the backoff time back down
// towards the floor value
this.callback.reduceBackoff();
}
String r = new String(readStreamToEnd(conn.getInputStream()));
Log.d(TAG, "response: " + r);
JSONObject responseJson = new JSONObject(r);
double lat = responseJson.getJSONObject("location").getDouble("lat");
double lon = responseJson.getJSONObject("location").getDouble("lng");
double acc = responseJson.getDouble("accuracy");
response = LocationHelper.create(PROVIDER, lat, lon, (float) acc);
this.callback.resultCallback(response);
} catch (IOException | JSONException e) {
if (conn != null) {
InputStream is = conn.getErrorStream();
if (is != null) {
try {
String error = new String(readStreamToEnd(is));
Log.w(TAG, "Error: " + error);
} catch (Exception ignored) {
}
}
}
Log.w(TAG, e);
}
this.callback.resultCallback(null);
}

private static byte[] readStreamToEnd(InputStream is) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
if (is != null) {
byte[] buff = new byte[1024];
while (true) {
int nb = is.read(buff);
if (nb < 0) {
break;
}
bos.write(buff, 0, nb);
}
is.close();
}
return bos.toByteArray();
}
}

31 changes: 31 additions & 0 deletions src/main/java/org/microg/nlp/backend/ichnaea/LocationCallback.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.microg.nlp.backend.ichnaea;

/*
* Copyright (C) 2013-2017 microG Project Team
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import android.location.Location;

public interface LocationCallback {
// returns True if the backoff expiry time doesn't cancel a run
boolean canRun();

// Methods to extend or reduce the backoff time
void extendBackoff();
void reduceBackoff();

// How you return data back to the caller
void resultCallback(Location location_result);
}

0 comments on commit 174e736

Please sign in to comment.