summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorPaul Hu <paulhu@google.com>2019-04-02 01:39:38 +0000
committerGerrit Code Review <noreply-gerritcodereview@google.com>2019-04-02 01:39:38 +0000
commit8899d2c6b1ef41b40edb58de0cf7a7690b3d495a (patch)
tree7ef211562f5876920ff299182754c19bd96122a5 /src
parent03d1e652267cab9724b1e85ac10d77f0a2917132 (diff)
parent75f42cb7e5dc799cf7822d7a9b4bc4de461baf88 (diff)
Merge "[IPMS] Implement regular maintenance"
Diffstat (limited to 'src')
-rw-r--r--src/com/android/server/connectivity/ipmemorystore/IpMemoryStoreDatabase.java111
-rw-r--r--src/com/android/server/connectivity/ipmemorystore/IpMemoryStoreService.java100
-rw-r--r--src/com/android/server/connectivity/ipmemorystore/RegularMaintenanceJobService.java140
3 files changed, 348 insertions, 3 deletions
diff --git a/src/com/android/server/connectivity/ipmemorystore/IpMemoryStoreDatabase.java b/src/com/android/server/connectivity/ipmemorystore/IpMemoryStoreDatabase.java
index b4eeefd..764e2d0 100644
--- a/src/com/android/server/connectivity/ipmemorystore/IpMemoryStoreDatabase.java
+++ b/src/com/android/server/connectivity/ipmemorystore/IpMemoryStoreDatabase.java
@@ -139,8 +139,9 @@ public class IpMemoryStoreDatabase {
/** The SQLite DB helper */
public static class DbHelper extends SQLiteOpenHelper {
// Update this whenever changing the schema.
- private static final int SCHEMA_VERSION = 3;
+ private static final int SCHEMA_VERSION = 4;
private static final String DATABASE_FILENAME = "IpMemoryStore.db";
+ private static final String TRIGGER_NAME = "delete_cascade_to_private";
public DbHelper(@NonNull final Context context) {
super(context, DATABASE_FILENAME, null, SCHEMA_VERSION);
@@ -152,6 +153,7 @@ public class IpMemoryStoreDatabase {
public void onCreate(@NonNull final SQLiteDatabase db) {
db.execSQL(NetworkAttributesContract.CREATE_TABLE);
db.execSQL(PrivateDataContract.CREATE_TABLE);
+ createTrigger(db);
}
/** Called when the database is upgraded */
@@ -172,6 +174,10 @@ public class IpMemoryStoreDatabase {
+ " " + NetworkAttributesContract.COLTYPE_ASSIGNEDV4ADDRESSEXPIRY;
db.execSQL(sqlUpgradeAddressExpiry);
}
+
+ if (oldVersion < 4) {
+ createTrigger(db);
+ }
} catch (SQLiteException e) {
Log.e(TAG, "Could not upgrade to the new version", e);
// create database with new version
@@ -188,8 +194,20 @@ public class IpMemoryStoreDatabase {
// Downgrades always nuke all data and recreate an empty table.
db.execSQL(NetworkAttributesContract.DROP_TABLE);
db.execSQL(PrivateDataContract.DROP_TABLE);
+ db.execSQL("DROP TRIGGER " + TRIGGER_NAME);
onCreate(db);
}
+
+ private void createTrigger(@NonNull final SQLiteDatabase db) {
+ final String createTrigger = "CREATE TRIGGER " + TRIGGER_NAME
+ + " DELETE ON " + NetworkAttributesContract.TABLENAME
+ + " BEGIN"
+ + " DELETE FROM " + PrivateDataContract.TABLENAME + " WHERE OLD."
+ + NetworkAttributesContract.COLNAME_L2KEY
+ + "=" + PrivateDataContract.COLNAME_L2KEY
+ + "; END;";
+ db.execSQL(createTrigger);
+ }
}
@NonNull
@@ -336,7 +354,7 @@ public class IpMemoryStoreDatabase {
}
// If the attributes are null, this will only write the expiry.
- // Returns an int out of Status.{SUCCESS,ERROR_*}
+ // Returns an int out of Status.{SUCCESS, ERROR_*}
static int storeNetworkAttributes(@NonNull final SQLiteDatabase db, @NonNull final String key,
final long expiry, @Nullable final NetworkAttributes attributes) {
final ContentValues cv = toContentValues(key, attributes, expiry);
@@ -361,7 +379,7 @@ public class IpMemoryStoreDatabase {
return Status.ERROR_STORAGE;
}
- // Returns an int out of Status.{SUCCESS,ERROR_*}
+ // Returns an int out of Status.{SUCCESS, ERROR_*}
static int storeBlob(@NonNull final SQLiteDatabase db, @NonNull final String key,
@NonNull final String clientId, @NonNull final String name,
@NonNull final byte[] data) {
@@ -524,6 +542,93 @@ public class IpMemoryStoreDatabase {
return bestKey;
}
+ // Drops all records that are expired. Relevance has decayed to zero of these records. Returns
+ // an int out of Status.{SUCCESS, ERROR_*}
+ static int dropAllExpiredRecords(@NonNull final SQLiteDatabase db) {
+ db.beginTransaction();
+ try {
+ // Deletes NetworkAttributes that have expired.
+ db.delete(NetworkAttributesContract.TABLENAME,
+ NetworkAttributesContract.COLNAME_EXPIRYDATE + " < ?",
+ new String[]{Long.toString(System.currentTimeMillis())});
+ db.setTransactionSuccessful();
+ } catch (SQLiteException e) {
+ Log.e(TAG, "Could not delete data from memory store", e);
+ return Status.ERROR_STORAGE;
+ } finally {
+ db.endTransaction();
+ }
+
+ // Execute vacuuming here if above operation has no exception. If above operation got
+ // exception, vacuuming can be ignored for reducing unnecessary consumption.
+ try {
+ db.execSQL("VACUUM");
+ } catch (SQLiteException e) {
+ // Do nothing.
+ }
+ return Status.SUCCESS;
+ }
+
+ // Drops number of records that start from the lowest expiryDate. Returns an int out of
+ // Status.{SUCCESS, ERROR_*}
+ static int dropNumberOfRecords(@NonNull final SQLiteDatabase db, int number) {
+ if (number <= 0) {
+ return Status.ERROR_ILLEGAL_ARGUMENT;
+ }
+
+ // Queries number of NetworkAttributes that start from the lowest expiryDate.
+ final Cursor cursor = db.query(NetworkAttributesContract.TABLENAME,
+ new String[] {NetworkAttributesContract.COLNAME_EXPIRYDATE}, // columns
+ null, // selection
+ null, // selectionArgs
+ null, // groupBy
+ null, // having
+ NetworkAttributesContract.COLNAME_EXPIRYDATE, // orderBy
+ Integer.toString(number)); // limit
+ if (cursor == null || cursor.getCount() <= 0) return Status.ERROR_GENERIC;
+ cursor.moveToLast();
+
+ //Get the expiryDate from last record.
+ final long expiryDate = getLong(cursor, NetworkAttributesContract.COLNAME_EXPIRYDATE, 0);
+ cursor.close();
+
+ db.beginTransaction();
+ try {
+ // Deletes NetworkAttributes that expiryDate are lower than given value.
+ db.delete(NetworkAttributesContract.TABLENAME,
+ NetworkAttributesContract.COLNAME_EXPIRYDATE + " <= ?",
+ new String[]{Long.toString(expiryDate)});
+ db.setTransactionSuccessful();
+ } catch (SQLiteException e) {
+ Log.e(TAG, "Could not delete data from memory store", e);
+ return Status.ERROR_STORAGE;
+ } finally {
+ db.endTransaction();
+ }
+
+ // Execute vacuuming here if above operation has no exception. If above operation got
+ // exception, vacuuming can be ignored for reducing unnecessary consumption.
+ try {
+ db.execSQL("VACUUM");
+ } catch (SQLiteException e) {
+ // Do nothing.
+ }
+ return Status.SUCCESS;
+ }
+
+ static int getTotalRecordNumber(@NonNull final SQLiteDatabase db) {
+ // Query the total number of NetworkAttributes
+ final Cursor cursor = db.query(NetworkAttributesContract.TABLENAME,
+ new String[] {"COUNT(*)"}, // columns
+ null, // selection
+ null, // selectionArgs
+ null, // groupBy
+ null, // having
+ null); // orderBy
+ cursor.moveToFirst();
+ return cursor == null ? 0 : cursor.getInt(0);
+ }
+
// Helper methods
private static String getString(final Cursor cursor, final String columnName) {
final int columnIndex = cursor.getColumnIndex(columnName);
diff --git a/src/com/android/server/connectivity/ipmemorystore/IpMemoryStoreService.java b/src/com/android/server/connectivity/ipmemorystore/IpMemoryStoreService.java
index f801b35..5650f21 100644
--- a/src/com/android/server/connectivity/ipmemorystore/IpMemoryStoreService.java
+++ b/src/com/android/server/connectivity/ipmemorystore/IpMemoryStoreService.java
@@ -22,6 +22,7 @@ import static android.net.ipmemorystore.Status.ERROR_ILLEGAL_ARGUMENT;
import static android.net.ipmemorystore.Status.SUCCESS;
import static com.android.server.connectivity.ipmemorystore.IpMemoryStoreDatabase.EXPIRY_ERROR;
+import static com.android.server.connectivity.ipmemorystore.RegularMaintenanceJobService.InterruptMaintenance;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -43,6 +44,9 @@ import android.net.ipmemorystore.StatusParcelable;
import android.os.RemoteException;
import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.File;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -57,8 +61,17 @@ import java.util.concurrent.Executors;
public class IpMemoryStoreService extends IIpMemoryStore.Stub {
private static final String TAG = IpMemoryStoreService.class.getSimpleName();
private static final int MAX_CONCURRENT_THREADS = 4;
+ private static final int DATABASE_SIZE_THRESHOLD = 10 * 1024 * 1024; //10MB
+ private static final int MAX_DROP_RECORD_TIMES = 500;
+ private static final int MIN_DELETE_NUM = 5;
private static final boolean DBG = true;
+ // Error codes below are internal and used for notifying status beteween IpMemoryStore modules.
+ static final int ERROR_INTERNAL_BASE = -1_000_000_000;
+ // This error code is used for maintenance only to notify RegularMaintenanceJobService that
+ // full maintenance job has been interrupted.
+ static final int ERROR_INTERNAL_INTERRUPTED = ERROR_INTERNAL_BASE - 1;
+
@NonNull
final Context mContext;
@Nullable
@@ -111,6 +124,7 @@ public class IpMemoryStoreService extends IIpMemoryStore.Stub {
// with judicious subclassing of ThreadPoolExecutor, but that's a lot of dangerous
// complexity for little benefit in this case.
mExecutor = Executors.newWorkStealingPool(MAX_CONCURRENT_THREADS);
+ RegularMaintenanceJobService.schedule(mContext, this);
}
/**
@@ -125,6 +139,7 @@ public class IpMemoryStoreService extends IIpMemoryStore.Stub {
// guarantee the threads can be terminated in any given amount of time.
mExecutor.shutdownNow();
if (mDb != null) mDb.close();
+ RegularMaintenanceJobService.unschedule(mContext);
}
/** Helper function to make a status object */
@@ -394,4 +409,89 @@ public class IpMemoryStoreService extends IIpMemoryStore.Stub {
}
});
}
+
+ /** Get db size threshold. */
+ @VisibleForTesting
+ protected int getDbSizeThreshold() {
+ return DATABASE_SIZE_THRESHOLD;
+ }
+
+ private long getDbSize() {
+ final File dbFile = new File(mDb.getPath());
+ try {
+ return dbFile.length();
+ } catch (final SecurityException e) {
+ if (DBG) Log.e(TAG, "Read db size access deny.", e);
+ // Return zero value if can't get disk usage exactly.
+ return 0;
+ }
+ }
+
+ /** Check if db size is over the threshold. */
+ @VisibleForTesting
+ boolean isDbSizeOverThreshold() {
+ return getDbSize() > getDbSizeThreshold();
+ }
+
+ /**
+ * Full maintenance.
+ *
+ * @param listener A listener to inform of the completion of this call.
+ */
+ void fullMaintenance(@NonNull final IOnStatusListener listener,
+ @NonNull final InterruptMaintenance interrupt) {
+ mExecutor.execute(() -> {
+ try {
+ if (null == mDb) {
+ listener.onComplete(makeStatus(ERROR_DATABASE_CANNOT_BE_OPENED));
+ return;
+ }
+
+ // Interrupt maintenance because the scheduling job has been canceled.
+ if (checkForInterrupt(listener, interrupt)) return;
+
+ int result = SUCCESS;
+ // Drop all records whose relevance has decayed to zero.
+ // This is the first step to decrease memory store size.
+ result = IpMemoryStoreDatabase.dropAllExpiredRecords(mDb);
+
+ if (checkForInterrupt(listener, interrupt)) return;
+
+ // Aggregate historical data in passes
+ // TODO : Waiting for historical data implement.
+
+ // Check if db size meets the storage goal(10MB). If not, keep dropping records and
+ // aggregate historical data until the storage goal is met. Use for loop with 500
+ // times restriction to prevent infinite loop (Deleting records always fail and db
+ // size is still over the threshold)
+ for (int i = 0; isDbSizeOverThreshold() && i < MAX_DROP_RECORD_TIMES; i++) {
+ if (checkForInterrupt(listener, interrupt)) return;
+
+ final int totalNumber = IpMemoryStoreDatabase.getTotalRecordNumber(mDb);
+ final long dbSize = getDbSize();
+ final float decreaseRate = (dbSize == 0)
+ ? 0 : (float) (dbSize - getDbSizeThreshold()) / (float) dbSize;
+ final int deleteNumber = Math.max(
+ (int) (totalNumber * decreaseRate), MIN_DELETE_NUM);
+
+ result = IpMemoryStoreDatabase.dropNumberOfRecords(mDb, deleteNumber);
+
+ if (checkForInterrupt(listener, interrupt)) return;
+
+ // Aggregate historical data
+ // TODO : Waiting for historical data implement.
+ }
+ listener.onComplete(makeStatus(result));
+ } catch (final RemoteException e) {
+ // Client at the other end died
+ }
+ });
+ }
+
+ private boolean checkForInterrupt(@NonNull final IOnStatusListener listener,
+ @NonNull final InterruptMaintenance interrupt) throws RemoteException {
+ if (!interrupt.isInterrupted()) return false;
+ listener.onComplete(makeStatus(ERROR_INTERNAL_INTERRUPTED));
+ return true;
+ }
}
diff --git a/src/com/android/server/connectivity/ipmemorystore/RegularMaintenanceJobService.java b/src/com/android/server/connectivity/ipmemorystore/RegularMaintenanceJobService.java
new file mode 100644
index 0000000..2775fde
--- /dev/null
+++ b/src/com/android/server/connectivity/ipmemorystore/RegularMaintenanceJobService.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * 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 com.android.server.connectivity.ipmemorystore;
+
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.content.ComponentName;
+import android.content.Context;
+import android.net.ipmemorystore.IOnStatusListener;
+import android.net.ipmemorystore.Status;
+import android.net.ipmemorystore.StatusParcelable;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Regular maintenance job service.
+ * @hide
+ */
+public final class RegularMaintenanceJobService extends JobService {
+ // Must be unique within the system server uid.
+ public static final int REGULAR_MAINTENANCE_ID = 3345678;
+
+ /**
+ * Class for interrupt check of maintenance job.
+ */
+ public static final class InterruptMaintenance {
+ private volatile boolean mIsInterrupted;
+ private final int mJobId;
+
+ public InterruptMaintenance(int jobId) {
+ mJobId = jobId;
+ mIsInterrupted = false;
+ }
+
+ public int getJobId() {
+ return mJobId;
+ }
+
+ public void setInterrupted(boolean interrupt) {
+ mIsInterrupted = interrupt;
+ }
+
+ public boolean isInterrupted() {
+ return mIsInterrupted;
+ }
+ }
+
+ private static final ArrayList<InterruptMaintenance> sInterruptList = new ArrayList<>();
+ private static IpMemoryStoreService sIpMemoryStoreService;
+
+ @Override
+ public boolean onStartJob(JobParameters params) {
+ if (sIpMemoryStoreService == null) {
+ Log.wtf("RegularMaintenanceJobService",
+ "Can not start job because sIpMemoryStoreService is null.");
+ return false;
+ }
+ final InterruptMaintenance im = new InterruptMaintenance(params.getJobId());
+ sInterruptList.add(im);
+
+ sIpMemoryStoreService.fullMaintenance(new IOnStatusListener() {
+ @Override
+ public void onComplete(final StatusParcelable statusParcelable) throws RemoteException {
+ final Status result = new Status(statusParcelable);
+ if (!result.isSuccess()) {
+ Log.e("RegularMaintenanceJobService", "Regular maintenance failed."
+ + " Error is " + result.resultCode);
+ }
+ sInterruptList.remove(im);
+ jobFinished(params, !result.isSuccess());
+ }
+
+ @Override
+ public IBinder asBinder() {
+ return null;
+ }
+ }, im);
+ return true;
+ }
+
+ @Override
+ public boolean onStopJob(JobParameters params) {
+ final int jobId = params.getJobId();
+ for (InterruptMaintenance im : sInterruptList) {
+ if (im.getJobId() == jobId) {
+ im.setInterrupted(true);
+ }
+ }
+ return true;
+ }
+
+ /** Schedule regular maintenance job */
+ static void schedule(Context context, IpMemoryStoreService ipMemoryStoreService) {
+ final JobScheduler jobScheduler =
+ (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
+
+ final ComponentName maintenanceJobName =
+ new ComponentName(context, RegularMaintenanceJobService.class);
+
+ // Regular maintenance is scheduled for when the device is idle with access power and a
+ // minimum interval of one day.
+ final JobInfo regularMaintenanceJob =
+ new JobInfo.Builder(REGULAR_MAINTENANCE_ID, maintenanceJobName)
+ .setRequiresDeviceIdle(true)
+ .setRequiresCharging(true)
+ .setRequiresBatteryNotLow(true)
+ .setPeriodic(TimeUnit.HOURS.toMillis(24)).build();
+
+ jobScheduler.schedule(regularMaintenanceJob);
+ sIpMemoryStoreService = ipMemoryStoreService;
+ }
+
+ /** Unschedule regular maintenance job */
+ static void unschedule(Context context) {
+ final JobScheduler jobScheduler =
+ (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
+ jobScheduler.cancel(REGULAR_MAINTENANCE_ID);
+ sIpMemoryStoreService = null;
+ }
+}