diff options
author | Paul Hu <paulhu@google.com> | 2019-04-02 01:39:38 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2019-04-02 01:39:38 +0000 |
commit | 8899d2c6b1ef41b40edb58de0cf7a7690b3d495a (patch) | |
tree | 7ef211562f5876920ff299182754c19bd96122a5 /src | |
parent | 03d1e652267cab9724b1e85ac10d77f0a2917132 (diff) | |
parent | 75f42cb7e5dc799cf7822d7a9b4bc4de461baf88 (diff) |
Merge "[IPMS] Implement regular maintenance"
Diffstat (limited to 'src')
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; + } +} |