Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 9b1a4a5e authored by mrulhania's avatar mrulhania
Browse files

Schedule sqlite writes into the registry

XML registry takes signal from AppOpsService for
writing the data to disk on regular interval,
the scheduling is happening in AppOpsService.
I believe the registry should schedule its write
itself, and AppOpsService doesn't need know about
scheduling of writes in this registry.

Bug: 377584611
Test: manual
Test: atest com.android.server.appop.DiscreteOpsMigrationAndRollbackTest
Test: atest com.android.server.appop.DiscreteAppOpSqlPersistenceTest
FLAG: EXEMPT bug fix
Change-Id: I85d9a576a88ff6cd8c2c7794d0d06c2ffa11a07e
parent b71d9180
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -177,6 +177,8 @@ abstract class DiscreteOpsRegistry {
     */
    abstract void writeAndClearOldAccessHistory();

    void shutdown() {}

    /** Remove all discrete op events. */
    abstract void clearHistory();

+77 −36
Original line number Diff line number Diff line
@@ -57,13 +57,18 @@ import java.util.Set;
public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry {
    private static final String TAG = "DiscreteOpsSqlRegistry";

    private static final long DB_WRITE_INTERVAL = Duration.ofMinutes(10).toMillis();
    private static final long EXPIRED_ENTRY_DELETION_INTERVAL = Duration.ofHours(6).toMillis();

    // Event type handled by SqliteWriteHandler
    private static final int WRITE_DATABASE_RECURRING = 1;
    private static final int DELETE_EXPIRED_ENTRIES = 2;
    private static final int WRITE_DATABASE_CACHE_FULL = 3;

    private final Context mContext;
    private final DiscreteOpsDbHelper mDiscreteOpsDbHelper;
    private final SqliteWriteHandler mSqliteWriteHandler;
    private final DiscreteOpCache mDiscreteOpCache = new DiscreteOpCache(512);
    private static final long THREE_HOURS = Duration.ofHours(3).toMillis();
    private static final int WRITE_CACHE_EVICTED_OP_EVENTS = 1;
    private static final int DELETE_OLD_OP_EVENTS = 2;
    // Attribution chain id is used to identify an attribution source chain, This is
    // set for startOp only. PermissionManagerService resets this ID on device restart, so
    // we use previously persisted chain id as offset, and add it to chain id received from
@@ -83,6 +88,9 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry {
        mSqliteWriteHandler = new SqliteWriteHandler(thread.getLooper());
        mDiscreteOpsDbHelper = new DiscreteOpsDbHelper(context, databaseFile);
        mChainIdOffset = mDiscreteOpsDbHelper.getLargestAttributionChainId();
        mSqliteWriteHandler.sendEmptyMessageDelayed(WRITE_DATABASE_RECURRING, DB_WRITE_INTERVAL);
        mSqliteWriteHandler.sendEmptyMessageDelayed(DELETE_EXPIRED_ENTRIES,
                EXPIRED_ENTRY_DELETION_INTERVAL);
    }

    @Override
@@ -117,15 +125,14 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry {
    }

    @Override
    void writeAndClearOldAccessHistory() {
        // Let the sql impl also follow the same disk write frequencies as xml,
        // controlled by AppOpsService.
    void shutdown() {
        mSqliteWriteHandler.removeAllPendingMessages();
        mDiscreteOpsDbHelper.insertDiscreteOps(mDiscreteOpCache.getAllEventsAndClear());
        if (!mSqliteWriteHandler.hasMessages(DELETE_OLD_OP_EVENTS)) {
            if (mSqliteWriteHandler.sendEmptyMessageDelayed(DELETE_OLD_OP_EVENTS, THREE_HOURS)) {
                Slog.w(TAG, "DELETE_OLD_OP_EVENTS is not queued");
            }
    }

    @Override
    void writeAndClearOldAccessHistory() {
        // no-op
    }

    @Override
@@ -175,7 +182,7 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry {
            @Nullable String attributionTagFilter, int opFlagsFilter,
            Set<String> attributionExemptPkgs) {
        // flush the cache into database before read.
        writeAndClearOldAccessHistory();
        mDiscreteOpsDbHelper.insertDiscreteOps(mDiscreteOpCache.getAllEventsAndClear());
        boolean assembleChains = attributionExemptPkgs != null;
        IntArray opCodes = getAppOpCodes(filter, opNamesFilter);
        beginTimeMillis = Math.max(beginTimeMillis, Instant.now().minus(sDiscreteHistoryCutoff,
@@ -363,19 +370,58 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case WRITE_CACHE_EVICTED_OP_EVENTS:
                    List<DiscreteOp> opEvents = (List<DiscreteOp>) msg.obj;
                    mDiscreteOpsDbHelper.insertDiscreteOps(opEvents);
                    break;
                case DELETE_OLD_OP_EVENTS:
                case WRITE_DATABASE_RECURRING -> {
                    try {
                        List<DiscreteOp> evictedEvents;
                        synchronized (mDiscreteOpCache) {
                            evictedEvents = mDiscreteOpCache.evict();
                        }
                        mDiscreteOpsDbHelper.insertDiscreteOps(evictedEvents);
                    } finally {
                        mSqliteWriteHandler.sendEmptyMessageDelayed(WRITE_DATABASE_RECURRING,
                                DB_WRITE_INTERVAL);
                        // Schedule a cleanup to truncate older (before cutoff time) entries.
                        if (!mSqliteWriteHandler.hasMessages(DELETE_EXPIRED_ENTRIES)) {
                            mSqliteWriteHandler.sendEmptyMessageDelayed(DELETE_EXPIRED_ENTRIES,
                                    EXPIRED_ENTRY_DELETION_INTERVAL);
                        }
                    }
                }
                case DELETE_EXPIRED_ENTRIES -> {
                    long cutOffTimeStamp = System.currentTimeMillis() - sDiscreteHistoryCutoff;
                    mDiscreteOpsDbHelper.execSQL(
                            DiscreteOpsTable.DELETE_TABLE_DATA_BEFORE_ACCESS_TIME,
                            new Object[]{cutOffTimeStamp});
                    break;
                default:
                    throw new IllegalStateException("Unexpected value: " + msg.what);
                }
                case WRITE_DATABASE_CACHE_FULL -> {
                    try {
                        List<DiscreteOp> evictedEvents;
                        synchronized (mDiscreteOpCache) {
                            evictedEvents = mDiscreteOpCache.evict();
                            // if nothing to evict, just write the whole cache to database.
                            if (evictedEvents.isEmpty()
                                    && mDiscreteOpCache.size() >= mDiscreteOpCache.capacity()) {
                                evictedEvents.addAll(mDiscreteOpCache.mCache);
                                mDiscreteOpCache.clear();
                            }
                        }
                        mDiscreteOpsDbHelper.insertDiscreteOps(evictedEvents);
                    } finally {
                        // Just in case initial message is not scheduled.
                        if (!mSqliteWriteHandler.hasMessages(WRITE_DATABASE_RECURRING)) {
                            mSqliteWriteHandler.sendEmptyMessageDelayed(WRITE_DATABASE_RECURRING,
                                    DB_WRITE_INTERVAL);
                        }
                    }
                }
                default -> throw new IllegalStateException("Unexpected value: " + msg.what);
            }
        }

        void removeAllPendingMessages() {
            removeMessages(WRITE_DATABASE_RECURRING);
            removeMessages(DELETE_EXPIRED_ENTRIES);
            removeMessages(WRITE_DATABASE_CACHE_FULL);
        }
    }

@@ -390,6 +436,7 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry {
     * 4) During shutdown.
     */
    class DiscreteOpCache {
        private static final String TAG = "DiscreteOpCache";
        private final int mCapacity;
        private final ArraySet<DiscreteOp> mCache;

@@ -404,23 +451,9 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry {
                    return;
                }
                mCache.add(opEvent);

                if (mCache.size() >= mCapacity) {
                    if (DEBUG_LOG) {
                        Slog.i(TAG, "Current discrete ops cache size: " + mCache.size());
                    }
                    List<DiscreteOp> evictedEvents = evict();
                    if (DEBUG_LOG) {
                        Slog.i(TAG, "Evicted discrete ops size: " + evictedEvents.size());
                    }
                    // if nothing to evict, just write the whole cache to disk
                    if (evictedEvents.isEmpty()) {
                        Slog.w(TAG, "No discrete ops event is evicted, write cache to db.");
                        evictedEvents.addAll(mCache);
                        mCache.clear();
                    }
                    Message msg = mSqliteWriteHandler.obtainMessage(
                            WRITE_CACHE_EVICTED_OP_EVENTS, evictedEvents);
                    mSqliteWriteHandler.sendMessage(msg);
                    mSqliteWriteHandler.sendEmptyMessage(WRITE_DATABASE_CACHE_FULL);
                }
            }
        }
@@ -461,6 +494,14 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry {
            }
        }

        int size() {
            return mCache.size();
        }

        int capacity() {
            return mCapacity;
        }

        /**
         * Remove all entries from the cache.
         */
+1 −0
Original line number Diff line number Diff line
@@ -750,6 +750,7 @@ final class HistoricalRegistry {
        }
        // Do not call persistPendingHistory inside the memory lock, due to possible deadlock
        persistPendingHistory();
        mDiscreteRegistry.shutdown();
    }

    void persistPendingHistory() {
+2 −2
Original line number Diff line number Diff line
@@ -226,9 +226,9 @@ public class DiscreteAppOpSqlPersistenceTest {
        mDiscreteRegistry.recordDiscreteAccess(event2);
    }

    /** This clears in-memory cache and push records into the database. */
    private void flushDiscreteOpsToDatabase() {
        mDiscreteRegistry.writeAndClearOldAccessHistory();
        // This clears in-memory cache and push records from cache into the database.
        mDiscreteRegistry.shutdown();
    }

    /**
+2 −1
Original line number Diff line number Diff line
@@ -106,7 +106,8 @@ public class DiscreteOpsMigrationAndRollbackTest {
                    opEvent.getDuration(), opEvent.getAttributionFlags(),
                    (int) opEvent.getChainId(), DiscreteOpsRegistry.ACCESS_TYPE_NOTE_OP);
        }
        sqlRegistry.writeAndClearOldAccessHistory();
        // flush records from cache to the database.
        sqlRegistry.shutdown();
        assertThat(sqlRegistry.getAllDiscreteOps().size()).isEqualTo(RECORD_COUNT);
        assertThat(sqlRegistry.getLargestAttributionChainId()).isEqualTo(RECORD_COUNT);