Merge "Avoid holding lock in CarWatchdogService when writing to database" into sc-v2-dev
diff --git a/service/src/com/android/car/watchdog/WatchdogPerfHandler.java b/service/src/com/android/car/watchdog/WatchdogPerfHandler.java
index 850c517..d963bff 100644
--- a/service/src/com/android/car/watchdog/WatchdogPerfHandler.java
+++ b/service/src/com/android/car/watchdog/WatchdogPerfHandler.java
@@ -215,6 +215,10 @@
     private final int mRecurringOveruseTimes;
     private final TimeSource mTimeSource;
     private final Object mLock = new Object();
+    /**
+     * Tracks user packages' resource usage. When cache is updated, call
+     * {@link WatchdogStorage#markDirty()} to notify database is out of sync.
+     */
     @GuardedBy("mLock")
     private final ArrayMap<String, PackageResourceUsage> mUsageByUserPackage = new ArrayMap<>();
     @GuardedBy("mLock")
@@ -223,7 +227,11 @@
     @GuardedBy("mLock")
     private final SparseArray<ArrayList<ResourceOveruseListenerInfo>>
             mOveruseSystemListenerInfosByUid = new SparseArray<>();
-    /** Default killable state for packages. Updated only for {@link UserHandle.ALL} user handle. */
+    /**
+     * Default killable state for packages. Updated only for {@link UserHandle#ALL} user handle.
+     * When cache is updated, call {@link WatchdogStorage#markDirty()} to notify database is out of
+     * sync.
+     */
     @GuardedBy("mLock")
     private final ArraySet<String> mDefaultNotKillableGenericPackages = new ArraySet<>();
     /** Keys in {@link mUsageByUserPackage} for user notification on resource overuse. */
@@ -249,8 +257,6 @@
     @GuardedBy("mLock")
     private boolean mIsConnectedToDaemon;
     @GuardedBy("mLock")
-    private boolean mIsWrittenToDatabase = true;
-    @GuardedBy("mLock")
     private @UxStateType int mCurrentUxState = UX_STATE_NO_DISTRACTION;
     @GuardedBy("mLock")
     private CarUxRestrictions mCurrentUxRestrictions;
@@ -556,16 +562,14 @@
         }
         String key = getUserPackageUniqueId(userId, genericPackageName);
         synchronized (mLock) {
-            /*
-             * When the queried package is not cached in {@link mUsageByUserPackage}, the set API
-             * will update the killable state even when the package should never be killed.
-             * But the get API will return the correct killable state. This behavior is tolerable
-             * because in production the set API should be called only after the get API.
-             * For instance, when this case happens by mistake and the package overuses resource
-             * between the set and the get API calls, the daemon will provide correct killable
-             * state when pushing the latest stats. Ergo, the invalid killable state doesn't have
-             * any effect.
-             */
+            // When the queried package is not cached in {@link mUsageByUserPackage}, the set API
+            // will update the killable state even when the package should never be killed.
+            // But the get API will return the correct killable state. This behavior is tolerable
+            // because in production the set API should be called only after the get API.
+            // For instance, when this case happens by mistake and the package overuses resource
+            // between the set and the get API calls, the daemon will provide correct killable
+            // state when pushing the latest stats. Ergo, the invalid killable state doesn't have
+            // any effect.
             PackageResourceUsage usage = mUsageByUserPackage.get(key);
             if (usage == null) {
                 usage = new PackageResourceUsage(userId, genericPackageName,
@@ -578,6 +582,7 @@
             }
             mUsageByUserPackage.put(key, usage);
         }
+        mWatchdogStorage.markDirty();
         if (DEBUG) {
             Slogf.d(TAG, "Successfully set killable package state for user %d", userId);
         }
@@ -611,6 +616,7 @@
                 } else {
                     mDefaultNotKillableGenericPackages.remove(genericPackageName);
                 }
+                mWatchdogStorage.markDirty();
             }
         }
         if (DEBUG) {
@@ -768,8 +774,10 @@
         SparseArray<String> genericPackageNamesByUid = mPackageInfoHandler.getNamesForUids(uids);
         ArraySet<String> overusingUserPackageKeys = new ArraySet<>();
         checkAndHandleDateChange();
+        if (genericPackageNamesByUid.size() > 0) {
+            mWatchdogStorage.markDirty();
+        }
         synchronized (mLock) {
-            mIsWrittenToDatabase &= genericPackageNamesByUid.size() == 0;
             for (int i = 0; i < packageIoOveruseStats.size(); ++i) {
                 PackageIoOveruseStats stats = packageIoOveruseStats.get(i);
                 String genericPackageName = genericPackageNamesByUid.get(stats.uid);
@@ -793,8 +801,7 @@
                     continue;
                 }
                 overusingUserPackageKeys.add(usage.getUniqueId());
-                int killableState = usage.getKillableState();
-                if (killableState == KILLABLE_STATE_NEVER) {
+                if (usage.getKillableState() == KILLABLE_STATE_NEVER) {
                     continue;
                 }
                 if (usage.ioUsage.getNotForgivenOveruses() > mRecurringOveruseTimes) {
@@ -1119,31 +1126,58 @@
         }
     }
 
-    /** Writes user package settings and stats to database. */
+    /**
+     * Writes user package settings and stats to database. If database is marked as clean,
+     * no writing is executed.
+     */
     public void writeToDatabase() {
-        synchronized (mLock) {
-            if (mIsWrittenToDatabase) {
-                return;
+        if (!mWatchdogStorage.startWrite()) {
+            return;
+        }
+        try {
+            List<WatchdogStorage.UserPackageSettingsEntry> userPackageSettingsEntries =
+                    new ArrayList<>();
+            List<WatchdogStorage.IoUsageStatsEntry> ioUsageStatsEntries = new ArrayList<>();
+            SparseArray<List<String>> forgivePackagesByUserId = new SparseArray<>();
+            synchronized (mLock) {
+                for (int i = 0; i < mUsageByUserPackage.size(); i++) {
+                    PackageResourceUsage usage = mUsageByUserPackage.valueAt(i);
+                    userPackageSettingsEntries.add(new WatchdogStorage.UserPackageSettingsEntry(
+                            usage.userId, usage.genericPackageName, usage.getKillableState()));
+                    if (!usage.ioUsage.hasUsage()) {
+                        continue;
+                    }
+                    if (usage.ioUsage.shouldForgiveHistoricalOveruses()) {
+                        List<String> packagesToForgive = forgivePackagesByUserId.get(usage.userId);
+                        if (packagesToForgive == null) {
+                            packagesToForgive = new ArrayList<>();
+                        }
+                        packagesToForgive.add(usage.genericPackageName);
+                        forgivePackagesByUserId.put(usage.userId, packagesToForgive);
+                    }
+                    ioUsageStatsEntries.add(new WatchdogStorage.IoUsageStatsEntry(usage.userId,
+                            usage.genericPackageName, usage.ioUsage));
+                }
+                for (int i = 0; i < mDefaultNotKillableGenericPackages.size(); i++) {
+                    String packageName = mDefaultNotKillableGenericPackages.valueAt(i);
+                    userPackageSettingsEntries.add(new WatchdogStorage.UserPackageSettingsEntry(
+                            UserHandle.ALL.getIdentifier(), packageName, KILLABLE_STATE_NO));
+                }
             }
-            List<WatchdogStorage.UserPackageSettingsEntry>  entries =
-                    new ArrayList<>(mUsageByUserPackage.size());
-            for (int i = 0; i < mUsageByUserPackage.size(); ++i) {
-                PackageResourceUsage usage = mUsageByUserPackage.valueAt(i);
-                entries.add(new WatchdogStorage.UserPackageSettingsEntry(
-                        usage.userId, usage.genericPackageName, usage.getKillableState()));
-            }
-            for (String packageName : mDefaultNotKillableGenericPackages) {
-                entries.add(new WatchdogStorage.UserPackageSettingsEntry(
-                        UserHandle.USER_ALL, packageName, KILLABLE_STATE_NO));
-            }
-            if (!mWatchdogStorage.saveUserPackageSettings(entries)) {
+            boolean userPackageSettingResult =
+                    mWatchdogStorage.saveUserPackageSettings(userPackageSettingsEntries);
+            if (!userPackageSettingResult) {
                 Slogf.e(TAG, "Failed to write user package settings to database");
             } else {
                 Slogf.i(TAG, "Successfully saved %d user package settings to database",
-                        entries.size());
+                        userPackageSettingsEntries.size());
             }
-            writeStatsLocked();
-            mIsWrittenToDatabase = true;
+            if (writeStats(ioUsageStatsEntries, forgivePackagesByUserId)
+                    && userPackageSettingResult) {
+                mWatchdogStorage.markWriteSuccessful();
+            }
+        } finally {
+            mWatchdogStorage.endWrite();
         }
     }
 
@@ -1153,36 +1187,19 @@
                 ? KILLABLE_STATE_NO : KILLABLE_STATE_YES;
     }
 
-    @GuardedBy("mLock")
-    private void writeStatsLocked() {
-        List<WatchdogStorage.IoUsageStatsEntry> ioUsageStatsEntries =
-                new ArrayList<>(mUsageByUserPackage.size());
-        SparseArray<List<String>> forgivePackagesByUserId = new SparseArray<>();
-        for (int i = 0; i < mUsageByUserPackage.size(); ++i) {
-            PackageResourceUsage usage = mUsageByUserPackage.valueAt(i);
-            if (!usage.ioUsage.hasUsage()) {
-                continue;
-            }
-            if (usage.ioUsage.shouldForgiveHistoricalOveruses()) {
-                List<String> packagesToForgive = forgivePackagesByUserId.get(usage.userId);
-                if (packagesToForgive == null) {
-                    packagesToForgive = new ArrayList<>();
-                }
-                packagesToForgive.add(usage.genericPackageName);
-                forgivePackagesByUserId.put(usage.userId, packagesToForgive);
-            }
-            ioUsageStatsEntries.add(new WatchdogStorage.IoUsageStatsEntry(usage.userId,
-                    usage.genericPackageName, usage.ioUsage));
-        }
+    private boolean writeStats(List<WatchdogStorage.IoUsageStatsEntry> ioUsageStatsEntries,
+            SparseArray<List<String>> forgivePackagesByUserId) {
         // Forgive historical overuses before writing the latest stats to disk to avoid forgiving
         // the latest stats when the write is triggered after date change.
         if (forgivePackagesByUserId.size() != 0) {
             mWatchdogStorage.forgiveHistoricalOveruses(forgivePackagesByUserId,
                     mRecurringOverusePeriodInDays);
-            Slogf.e(TAG, "Attempted to forgive historical overuses for %d users.",
+            Slogf.i(TAG, "Attempted to forgive historical overuses for %d users.",
                     forgivePackagesByUserId.size());
         }
-
+        if (ioUsageStatsEntries.isEmpty()) {
+            return true;
+        }
         int result = mWatchdogStorage.saveIoUsageStats(ioUsageStatsEntries);
         if (result == WatchdogStorage.FAILED_TRANSACTION) {
             Slogf.e(TAG, "Failed to write %d I/O overuse stats to database",
@@ -1191,6 +1208,7 @@
             Slogf.i(TAG, "Successfully saved %d/%d I/O overuse stats to database",
                     result, ioUsageStatsEntries.size());
         }
+        return result != WatchdogStorage.FAILED_TRANSACTION;
     }
 
     @GuardedBy("mLock")
@@ -1219,6 +1237,7 @@
         int killableState = usage.syncAndFetchKillableState(
                 componentType, isSafeToKill, defaultKillableState);
         mUsageByUserPackage.put(key, usage);
+        mWatchdogStorage.markDirty();
         return killableState;
     }
 
@@ -1252,16 +1271,13 @@
             if (currentDate.equals(mLatestStatsReportDate)) {
                 return;
             }
-            // After the first database read or on the first stats sync from the daemon, whichever
-            // happens first, the cached stats would either be empty or initialized from the
-            // database. In either case, don't write to database.
-            if (mLatestStatsReportDate != null && !mIsWrittenToDatabase) {
-                writeToDatabase();
-            }
-            for (int i = 0; i < mUsageByUserPackage.size(); ++i) {
+            mLatestStatsReportDate = currentDate;
+        }
+        writeToDatabase();
+        synchronized (mLock) {
+            for (int i = 0; i < mUsageByUserPackage.size(); i++) {
                 mUsageByUserPackage.valueAt(i).resetStats();
             }
-            mLatestStatsReportDate = currentDate;
         }
         syncHistoricalNotForgivenOveruses();
         if (DEBUG) {
diff --git a/service/src/com/android/car/watchdog/WatchdogStorage.java b/service/src/com/android/car/watchdog/WatchdogStorage.java
index 58c0121..7b4726a 100644
--- a/service/src/com/android/car/watchdog/WatchdogStorage.java
+++ b/service/src/com/android/car/watchdog/WatchdogStorage.java
@@ -18,6 +18,7 @@
 
 import static com.android.car.watchdog.TimeSource.ZONE_OFFSET;
 
+import android.annotation.IntDef;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
 import android.automotive.watchdog.PerStateBytes;
@@ -41,6 +42,8 @@
 import com.android.server.utils.Slogf;
 
 import java.io.File;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.time.Instant;
 import java.time.Period;
 import java.time.ZonedDateTime;
@@ -59,6 +62,38 @@
     private static final String TAG = CarLog.tagFor(WatchdogStorage.class);
     private static final int RETENTION_PERIOD_IN_DAYS = 30;
 
+    /**
+     * The database is clean when it is synchronized with the in-memory cache. Cannot start a
+     * write while in this state.
+     */
+    private static final int DB_STATE_CLEAN = 1;
+
+    /**
+     * The database is dirty when it is not synchronized with the in-memory cache. When the
+     * database is in this state, no write is in progress.
+     */
+    private static final int DB_STATE_DIRTY = 2;
+
+    /**
+     * Database write in progress. Cannot start a new write when the database is in this state.
+     */
+    private static final int DB_STATE_WRITE_IN_PROGRESS = 3;
+
+    /**
+     * The database enters this state when the database is marked dirty while a write is in
+     * progress.
+     */
+    private static final int DB_STATE_WRITE_IN_PROGRESS_DIRTY = 4;
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = {"DB_STATE_"}, value = {
+            DB_STATE_CLEAN,
+            DB_STATE_DIRTY,
+            DB_STATE_WRITE_IN_PROGRESS,
+            DB_STATE_WRITE_IN_PROGRESS_DIRTY
+    })
+    private @interface DatabaseStateType{}
+
     public static final int FAILED_TRANSACTION = -1;
     /* Stats are stored on a daily basis. */
     public static final TemporalUnit STATS_TEMPORAL_UNIT = ChronoUnit.DAYS;
@@ -77,6 +112,8 @@
     // the cache won't change until the next boot, so it is safe to cache the data in memory.
     @GuardedBy("mLock")
     private final List<IoUsageStatsEntry> mTodayIoUsageStatsEntries = new ArrayList<>();
+    @GuardedBy("mLock")
+    private @DatabaseStateType int mCurrentDbState = DB_STATE_CLEAN;
 
     public WatchdogStorage(Context context, TimeSource timeSource) {
         this(context, /* useDataSystemCarDir= */ true, timeSource);
@@ -100,6 +137,54 @@
         }
     }
 
+    /**
+     * Marks the database as dirty. The database is dirty when it is not synchronized with the
+     * memory cache.
+     */
+    public void markDirty() {
+        synchronized (mLock) {
+            mCurrentDbState = mCurrentDbState == DB_STATE_WRITE_IN_PROGRESS
+                    ? DB_STATE_WRITE_IN_PROGRESS_DIRTY : DB_STATE_DIRTY;
+        }
+        Slogf.i(TAG, "Database marked dirty.");
+    }
+
+    /**
+     * Starts write to database only if database is dirty and no writing is in progress.
+     *
+     * @return {@code true} if start was successful, otherwise {@code false}.
+     */
+    public boolean startWrite() {
+        synchronized (mLock) {
+            if (mCurrentDbState != DB_STATE_DIRTY) {
+                Slogf.e(TAG, "Cannot start a new write while the DB state is %s",
+                        toDbStateString(mCurrentDbState));
+                return false;
+            }
+            mCurrentDbState = DB_STATE_WRITE_IN_PROGRESS;
+            return true;
+        }
+    }
+
+    /** Ends write to database if write is in progress. */
+    public void endWrite() {
+        synchronized (mLock) {
+            mCurrentDbState = mCurrentDbState == DB_STATE_CLEAN ? DB_STATE_CLEAN : DB_STATE_DIRTY;
+        }
+    }
+
+    /** Marks the database as clean during an in progress database write. */
+    public void markWriteSuccessful() {
+        synchronized (mLock) {
+            if (mCurrentDbState != DB_STATE_WRITE_IN_PROGRESS) {
+                Slogf.e(TAG, "Failed to mark write successful as the current db state is %s",
+                        toDbStateString(mCurrentDbState));
+                return;
+            }
+            mCurrentDbState = DB_STATE_CLEAN;
+        }
+    }
+
     /** Saves the given user package settings entries and returns whether the change succeeded. */
     public boolean saveUserPackageSettings(List<UserPackageSettingsEntry> entries) {
         ArraySet<Integer> usersWithMissingIds = new ArraySet<>();
@@ -453,6 +538,21 @@
         return rows.size();
     }
 
+    private static String toDbStateString(int dbState) {
+        switch (dbState) {
+            case DB_STATE_CLEAN:
+                return "DB_STATE_CLEAN";
+            case DB_STATE_DIRTY:
+                return "DB_STATE_DIRTY";
+            case DB_STATE_WRITE_IN_PROGRESS:
+                return "DB_STATE_WRITE_IN_PROGRESS";
+            case DB_STATE_WRITE_IN_PROGRESS_DIRTY:
+                return "DB_STATE_WRITE_IN_PROGRESS_DIRTY";
+            default:
+                return "UNKNOWN";
+        }
+    }
+
     /** Defines the user package settings entry stored in the UserPackageSettingsTable. */
     static final class UserPackageSettingsEntry {
         public final @UserIdInt int userId;
diff --git a/tests/carservice_unit_test/src/com/android/car/watchdog/CarWatchdogServiceUnitTest.java b/tests/carservice_unit_test/src/com/android/car/watchdog/CarWatchdogServiceUnitTest.java
index 5e5988b..db9966b 100644
--- a/tests/carservice_unit_test/src/com/android/car/watchdog/CarWatchdogServiceUnitTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/watchdog/CarWatchdogServiceUnitTest.java
@@ -45,6 +45,7 @@
 import static com.android.car.watchdog.TimeSource.ZONE_OFFSET;
 import static com.android.car.watchdog.WatchdogPerfHandler.INTENT_EXTRA_ID;
 import static com.android.car.watchdog.WatchdogStorage.RETENTION_PERIOD;
+import static com.android.car.watchdog.WatchdogStorage.WatchdogDbHelper.DATABASE_NAME;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
@@ -199,7 +200,6 @@
     @Mock private Resources mMockResources;
     @Mock private IBinder mMockBinder;
     @Mock private ICarWatchdog mMockCarWatchdogDaemon;
-    @Mock private WatchdogStorage mMockWatchdogStorage;
     @Mock private UserNotificationHelper mMockUserNotificationHelper;
 
     @Captor private ArgumentCaptor<ICarPowerStateListener> mICarPowerStateListenerCaptor;
@@ -231,6 +231,7 @@
     private CarWatchdogService mCarWatchdogService;
     private ICarWatchdogServiceForSystem mWatchdogServiceForSystemImpl;
     private IBinder.DeathRecipient mCarWatchdogDaemonBinderDeathRecipient;
+    private WatchdogStorage mMockWatchdogStorage;
     private BroadcastReceiver mBroadcastReceiver;
     private boolean mIsDaemonCrashed;
     private ICarPowerStateListener mCarPowerStateListener;
@@ -304,6 +305,13 @@
         mTempSystemCarDir = Files.createTempDirectory("watchdog_test").toFile();
         when(mMockSystemInterface.getSystemCarDir()).thenReturn(mTempSystemCarDir);
 
+        File tempDbFile = new File(mTempSystemCarDir.getPath(), DATABASE_NAME);
+        when(mMockContext.createDeviceProtectedStorageContext()).thenReturn(mMockContext);
+        when(mMockContext.getDatabasePath(DATABASE_NAME)).thenReturn(tempDbFile);
+        mMockWatchdogStorage =
+                spy(new WatchdogStorage(mMockContext, /* useDataSystemCarDir= */ false,
+                        mTimeSource));
+
         setupUsers();
         mockWatchdogDaemon();
         mockWatchdogStorage();
@@ -623,10 +631,10 @@
 
         long startTime = mTimeSource.getCurrentDateTime().minusDays(4).toEpochSecond();
         long duration = mTimeSource.now().getEpochSecond() - startTime;
-        when(mMockWatchdogStorage.getHistoricalIoOveruseStats(
-                UserHandle.getUserId(uid), packageName, 6))
-                .thenReturn(new IoOveruseStats.Builder(startTime, duration).setTotalOveruses(5)
-                        .setTotalTimesKilled(2).setTotalBytesWritten(24_000).build());
+        doReturn(new IoOveruseStats.Builder(startTime, duration).setTotalOveruses(5)
+                .setTotalTimesKilled(2).setTotalBytesWritten(24_000).build())
+                .when(mMockWatchdogStorage)
+                .getHistoricalIoOveruseStats(UserHandle.getUserId(uid), packageName, 6);
 
         injectIoOveruseStatsForPackages(mGenericPackageNameByUid,
                 /* killablePackages= */ Collections.singleton(packageName),
@@ -656,8 +664,8 @@
         injectPackageInfos(Collections.singletonList(constructPackageManagerPackageInfo(
                 packageName, uid, null, ApplicationInfo.FLAG_SYSTEM, 0)));
 
-        when(mMockWatchdogStorage.getHistoricalIoOveruseStats(
-                UserHandle.getUserId(uid), packageName, 6)).thenReturn(null);
+        doReturn(null).when(mMockWatchdogStorage)
+                .getHistoricalIoOveruseStats(UserHandle.getUserId(uid), packageName, 6);
 
         injectIoOveruseStatsForPackages(mGenericPackageNameByUid,
                 /* killablePackages= */ Collections.singleton(packageName),
@@ -688,10 +696,10 @@
 
         long startTime = mTimeSource.getCurrentDateTime().minusDays(4).toEpochSecond();
         long duration = mTimeSource.now().getEpochSecond() - startTime;
-        when(mMockWatchdogStorage.getHistoricalIoOveruseStats(
-                UserHandle.getUserId(uid), packageName, 6))
-                .thenReturn(new IoOveruseStats.Builder(startTime, duration).setTotalOveruses(5)
-                        .setTotalTimesKilled(2).setTotalBytesWritten(24_000).build());
+        doReturn(new IoOveruseStats.Builder(startTime, duration).setTotalOveruses(5)
+                .setTotalTimesKilled(2).setTotalBytesWritten(24_000).build())
+                .when(mMockWatchdogStorage)
+                .getHistoricalIoOveruseStats(UserHandle.getUserId(uid), packageName, 6);
 
         ResourceOveruseStats actualStats = mCarWatchdogService.getResourceOveruseStats(
                 CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO,
@@ -806,16 +814,15 @@
         IoOveruseStats thirdPartyPkgOldStats = new IoOveruseStats.Builder(
                 startTime, now.toEpochSecond() - startTime).setTotalOveruses(5)
                 .setTotalTimesKilled(2).setTotalBytesWritten(24_000).build();
-        when(mMockWatchdogStorage.getHistoricalIoOveruseStats(11, "third_party_package", 6))
-                .thenReturn(thirdPartyPkgOldStats);
+        doReturn(thirdPartyPkgOldStats).when(mMockWatchdogStorage)
+                .getHistoricalIoOveruseStats(11, "third_party_package", 6);
 
         startTime = now.minusDays(6).toEpochSecond();
         IoOveruseStats vendorPkgOldStats = new IoOveruseStats.Builder(
                 startTime, now.toEpochSecond() - startTime).setTotalOveruses(2)
                 .setTotalTimesKilled(0).setTotalBytesWritten(35_000).build();
-        when(mMockWatchdogStorage.getHistoricalIoOveruseStats(12, "vendor_package.critical", 6))
-                .thenReturn(vendorPkgOldStats);
-
+        doReturn(vendorPkgOldStats).when(mMockWatchdogStorage)
+                .getHistoricalIoOveruseStats(12, "vendor_package.critical", 6);
 
         List<ResourceOveruseStats> actualStats = mCarWatchdogService.getAllResourceOveruseStats(
                 CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO, /* minimumStatsFlag= */ 0,
@@ -991,15 +998,15 @@
         IoOveruseStats thirdPartyPkgOldStats = new IoOveruseStats.Builder(
                 startTime, now.toEpochSecond() - startTime).setTotalOveruses(5)
                 .setTotalTimesKilled(2).setTotalBytesWritten(24_000).build();
-        when(mMockWatchdogStorage.getHistoricalIoOveruseStats(11, "third_party_package", 6))
-                .thenReturn(thirdPartyPkgOldStats);
+        doReturn(thirdPartyPkgOldStats).when(mMockWatchdogStorage)
+                .getHistoricalIoOveruseStats(11, "third_party_package", 6);
 
         startTime = now.minusDays(6).toEpochSecond();
         IoOveruseStats vendorPkgOldStats = new IoOveruseStats.Builder(
                 startTime, now.toEpochSecond() - startTime).setTotalOveruses(2)
                 .setTotalTimesKilled(0).setTotalBytesWritten(6_900_000).build();
-        when(mMockWatchdogStorage.getHistoricalIoOveruseStats(12, "vendor_package.critical", 6))
-                .thenReturn(vendorPkgOldStats);
+        doReturn(vendorPkgOldStats).when(mMockWatchdogStorage)
+                .getHistoricalIoOveruseStats(12, "vendor_package.critical", 6);
 
         List<ResourceOveruseStats> actualStats = mCarWatchdogService.getAllResourceOveruseStats(
                 CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO,
@@ -1085,8 +1092,9 @@
         IoOveruseStats vendorPkgOldStats = new IoOveruseStats.Builder(
                 startTime, now.toEpochSecond() - startTime).setTotalOveruses(2)
                 .setTotalTimesKilled(0).setTotalBytesWritten(6_900_000).build();
-        when(mMockWatchdogStorage.getHistoricalIoOveruseStats(12, "vendor_package.critical", 6))
-                .thenReturn(vendorPkgOldStats);
+
+        doReturn(vendorPkgOldStats).when(mMockWatchdogStorage)
+                .getHistoricalIoOveruseStats(12, "vendor_package.critical", 6);
 
         ResourceOveruseStats actualStats =
                 mCarWatchdogService.getResourceOveruseStatsForUserPackage(
@@ -1368,6 +1376,8 @@
                         PackageKillableState.KILLABLE_STATE_NEVER),
                 new PackageKillableState("third_party_package", 13,
                         PackageKillableState.KILLABLE_STATE_YES));
+
+        verify(mMockWatchdogStorage, times(11)).markDirty();
     }
 
     @Test
@@ -1413,6 +1423,8 @@
                         PackageKillableState.KILLABLE_STATE_NO),
                 new PackageKillableState("third_party_package.D", 11,
                         PackageKillableState.KILLABLE_STATE_NO));
+
+        verify(mMockWatchdogStorage, times(7)).markDirty();
     }
 
     @Test
@@ -1460,6 +1472,8 @@
                         PackageKillableState.KILLABLE_STATE_NEVER),
                 new PackageKillableState("third_party_package", 13,
                         PackageKillableState.KILLABLE_STATE_NO));
+
+        verify(mMockWatchdogStorage, times(11)).markDirty();
     }
 
     @Test
@@ -1511,6 +1525,8 @@
                         PackageKillableState.KILLABLE_STATE_NO),
                 new PackageKillableState("third_party_package.B", 13,
                         PackageKillableState.KILLABLE_STATE_NO));
+
+        verify(mMockWatchdogStorage, times(5)).markDirty();
     }
 
     @Test
@@ -1529,6 +1545,8 @@
                                 PackageKillableState.KILLABLE_STATE_YES),
                         new PackageKillableState("vendor_package.critical", 11,
                                 PackageKillableState.KILLABLE_STATE_NEVER));
+
+        verify(mMockWatchdogStorage, times(2)).markDirty();
     }
 
     @Test
@@ -1557,6 +1575,8 @@
                                 PackageKillableState.KILLABLE_STATE_YES),
                         new PackageKillableState("vendor_package.critical.B", 101,
                                 PackageKillableState.KILLABLE_STATE_NEVER));
+
+        verify(mMockWatchdogStorage, times(6)).markDirty();
     }
 
     @Test
@@ -1577,6 +1597,8 @@
         PackageKillableStateSubject.assertThat(killableStates)
                 .containsExactly(new PackageKillableState("some_pkg_as_vendor_pkg", 100,
                         PackageKillableState.KILLABLE_STATE_YES));
+
+        verify(mMockWatchdogStorage).markDirty();
     }
 
     @Test
@@ -1607,6 +1629,8 @@
                                 PackageKillableState.KILLABLE_STATE_YES),
                         new PackageKillableState("third_party_package.D", 11,
                                 PackageKillableState.KILLABLE_STATE_YES));
+
+        verify(mMockWatchdogStorage, times(2)).markDirty();
     }
 
     @Test
@@ -1638,6 +1662,8 @@
                                 PackageKillableState.KILLABLE_STATE_YES),
                         new PackageKillableState("third_party_package.D", 100,
                                 PackageKillableState.KILLABLE_STATE_YES));
+
+        verify(mMockWatchdogStorage, times(2)).markDirty();
     }
 
     @Test
@@ -1662,6 +1688,8 @@
                                 PackageKillableState.KILLABLE_STATE_YES),
                         new PackageKillableState("vendor_package.B", 100,
                                 PackageKillableState.KILLABLE_STATE_YES));
+
+        verify(mMockWatchdogStorage).markDirty();
     }
 
     @Test
@@ -1683,6 +1711,8 @@
                         PackageKillableState.KILLABLE_STATE_YES),
                 new PackageKillableState("vendor_package.critical", 12,
                         PackageKillableState.KILLABLE_STATE_NEVER));
+
+        verify(mMockWatchdogStorage, times(4)).markDirty();
     }
 
     @Test
@@ -1717,6 +1747,8 @@
                                 PackageKillableState.KILLABLE_STATE_NEVER),
                         new PackageKillableState("vendor_package.B", 12,
                                 PackageKillableState.KILLABLE_STATE_NEVER));
+
+        verify(mMockWatchdogStorage, times(3)).markDirty();
     }
 
     @Test
@@ -2284,6 +2316,9 @@
                         new PackageKillableState("third_party_package.A", 12,
                                 PackageKillableState.KILLABLE_STATE_NO));
 
+        // Changing and getting package killable states marks the database as dirty
+        verify(mMockWatchdogStorage, times(5)).markDirty();
+
         ResourceOveruseStatsSubject.assertThat(actualStats)
                 .containsExactlyElementsIn(expectedStats);
 
@@ -2581,10 +2616,10 @@
 
     @Test
     public void testUserNotificationOnHistoricalRecurrentOveruse() throws Exception {
-        when(mMockWatchdogStorage
-                .getNotForgivenHistoricalIoOveruses(RECURRING_OVERUSE_PERIOD_IN_DAYS))
-                .thenReturn(Arrays.asList(new WatchdogStorage.NotForgivenOverusesEntry(100,
-                        "system_package.non_critical", 2)));
+        doReturn(Arrays.asList(new WatchdogStorage.NotForgivenOverusesEntry(100,
+                "system_package.non_critical", 2)))
+                .when(mMockWatchdogStorage)
+                .getNotForgivenHistoricalIoOveruses(RECURRING_OVERUSE_PERIOD_IN_DAYS);
 
         // Force CarWatchdogService to fetch historical not forgiven overuses.
         restartService(/* totalRestarts= */ 1, /* wantedDbWrites= */ 0);
@@ -2904,10 +2939,9 @@
 
     @Test
     public void testDisableHistoricalRecurrentlyOverusingApp() throws Exception {
-        when(mMockWatchdogStorage
-                .getNotForgivenHistoricalIoOveruses(RECURRING_OVERUSE_PERIOD_IN_DAYS))
-                .thenReturn(Arrays.asList(new WatchdogStorage.NotForgivenOverusesEntry(100,
-                        "third_party_package", 2)));
+        doReturn(Arrays.asList(new WatchdogStorage.NotForgivenOverusesEntry(100,
+                "third_party_package", 2))).when(mMockWatchdogStorage)
+                .getNotForgivenHistoricalIoOveruses(RECURRING_OVERUSE_PERIOD_IN_DAYS);
 
         // Force CarWatchdogService to fetch historical not forgiven overuses.
         restartService(/* totalRestarts= */ 1, /* wantedDbWrites= */ 0);
@@ -2968,10 +3002,9 @@
 
     @Test
     public void testDisableHistoricalRecurrentlyOverusingAppAfterDateChange() throws Exception {
-        when(mMockWatchdogStorage.getNotForgivenHistoricalIoOveruses(
-                eq(RECURRING_OVERUSE_PERIOD_IN_DAYS)))
-                .thenReturn(Arrays.asList(new WatchdogStorage.NotForgivenOverusesEntry(100,
-                        "third_party_package", 2)));
+        doReturn(Arrays.asList(new WatchdogStorage.NotForgivenOverusesEntry(100,
+                "third_party_package", 2))).when(mMockWatchdogStorage)
+                .getNotForgivenHistoricalIoOveruses(RECURRING_OVERUSE_PERIOD_IN_DAYS);
 
         mTimeSource.updateNow(/* numDaysAgo= */ 1);
         setRequiresDistractionOptimization(true);
@@ -3611,11 +3644,11 @@
     }
 
     private void mockWatchdogStorage() {
-        when(mMockWatchdogStorage.saveUserPackageSettings(any())).thenAnswer((args) -> {
+        doAnswer((args) -> {
             mUserPackageSettingsEntries.addAll(args.getArgument(0));
             return true;
-        });
-        when(mMockWatchdogStorage.saveIoUsageStats(any())).thenAnswer((args) -> {
+        }).when(mMockWatchdogStorage).saveUserPackageSettings(any());
+        doAnswer((args) -> {
             List<WatchdogStorage.IoUsageStatsEntry> ioUsageStatsEntries = args.getArgument(0);
             for (WatchdogStorage.IoUsageStatsEntry entry : ioUsageStatsEntries) {
                 mIoUsageStatsEntries.add(
@@ -3627,27 +3660,30 @@
                                         entry.ioUsage.getTotalTimesKilled())));
             }
             return ioUsageStatsEntries.size();
-        });
-        when(mMockWatchdogStorage.getUserPackageSettings()).thenReturn(mUserPackageSettingsEntries);
-        when(mMockWatchdogStorage.getTodayIoUsageStats()).thenReturn(mIoUsageStatsEntries);
-        when(mMockWatchdogStorage.getDailySystemIoUsageSummaries(anyLong(), anyLong(), anyLong()))
-                .thenAnswer(args -> sampleDailyIoUsageSummariesForAWeek(args.getArgument(1),
-                        SYSTEM_DAILY_IO_USAGE_SUMMARY_MULTIPLIER));
-        when(mMockWatchdogStorage.getTopUsersDailyIoUsageSummaries(
-                anyInt(), anyLong(), anyLong(), anyLong())).thenAnswer(args -> {
-                    ArrayList<WatchdogStorage.UserPackageDailySummaries> summaries =
-                            new ArrayList<>();
-                    for (int i = 0; i < mGenericPackageNameByUid.size(); ++i) {
-                        int uid = mGenericPackageNameByUid.keyAt(i);
-                        summaries.add(new WatchdogStorage.UserPackageDailySummaries(
-                                UserHandle.getUserId(uid), mGenericPackageNameByUid.valueAt(i),
-                                sampleDailyIoUsageSummariesForAWeek(args.getArgument(2),
-                                        /* sysOrUidMultiplier= */ uid)));
-                    }
-                    summaries.sort(Comparator.comparingLong(WatchdogStorage
-                            .UserPackageDailySummaries::getTotalWrittenBytes).reversed());
-                    return summaries;
-                });
+        }).when(mMockWatchdogStorage).saveIoUsageStats(any());
+        doReturn(mUserPackageSettingsEntries).when(mMockWatchdogStorage).getUserPackageSettings();
+        doReturn(mIoUsageStatsEntries).when(mMockWatchdogStorage).getTodayIoUsageStats();
+        doReturn(List.of()).when(mMockWatchdogStorage)
+                .getNotForgivenHistoricalIoOveruses(RECURRING_OVERUSE_PERIOD_IN_DAYS);
+        doAnswer(args -> sampleDailyIoUsageSummariesForAWeek(args.getArgument(1),
+                SYSTEM_DAILY_IO_USAGE_SUMMARY_MULTIPLIER))
+                .when(mMockWatchdogStorage)
+                .getDailySystemIoUsageSummaries(anyLong(), anyLong(), anyLong());
+        doAnswer(args -> {
+            ArrayList<WatchdogStorage.UserPackageDailySummaries> summaries =
+                    new ArrayList<>();
+            for (int i = 0; i < mGenericPackageNameByUid.size(); ++i) {
+                int uid = mGenericPackageNameByUid.keyAt(i);
+                summaries.add(new WatchdogStorage.UserPackageDailySummaries(
+                        UserHandle.getUserId(uid), mGenericPackageNameByUid.valueAt(i),
+                        sampleDailyIoUsageSummariesForAWeek(args.getArgument(2),
+                                /* sysOrUidMultiplier= */ uid)));
+            }
+            summaries.sort(Comparator.comparingLong(WatchdogStorage
+                    .UserPackageDailySummaries::getTotalWrittenBytes).reversed());
+            return summaries;
+        }).when(mMockWatchdogStorage)
+                .getTopUsersDailyIoUsageSummaries(anyInt(), anyLong(), anyLong(), anyLong());
     }
 
     private void setupUsers() {
@@ -3670,8 +3706,11 @@
         setCarPowerState(CarPowerStateListener.SHUTDOWN_PREPARE);
         setCarPowerState(CarPowerStateListener.SHUTDOWN_ENTER);
         mCarWatchdogService.release();
+        verify(mMockWatchdogStorage, times(totalRestarts)).startWrite();
         verify(mMockWatchdogStorage, times(wantedDbWrites)).saveIoUsageStats(any());
         verify(mMockWatchdogStorage, times(wantedDbWrites)).saveUserPackageSettings(any());
+        verify(mMockWatchdogStorage, times(wantedDbWrites)).markWriteSuccessful();
+        verify(mMockWatchdogStorage, times(wantedDbWrites)).endWrite();
         verify(mMockWatchdogStorage, times(totalRestarts)).release();
         mCarWatchdogService = new CarWatchdogService(mMockContext, mMockWatchdogStorage,
                 mMockUserNotificationHelper, mTimeSource);
@@ -3983,6 +4022,10 @@
         // OVERUSE_HANDLING_DELAY_MILLS delay. Wait until the below message is processed before
         // returning, so the resource overuse handling is completed.
         delayedRunOnMainSync(() -> {}, OVERUSE_HANDLING_DELAY_MILLS * 2);
+
+        if (mGenericPackageNameByUid.size() > 0) {
+            verify(mMockWatchdogStorage, atLeastOnce()).markDirty();
+        }
     }
 
     private void setUpSampleUserAndPackages() {
diff --git a/tests/carservice_unit_test/src/com/android/car/watchdog/WatchdogStorageUnitTest.java b/tests/carservice_unit_test/src/com/android/car/watchdog/WatchdogStorageUnitTest.java
index 8d7cf30..34c3282 100644
--- a/tests/carservice_unit_test/src/com/android/car/watchdog/WatchdogStorageUnitTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/watchdog/WatchdogStorageUnitTest.java
@@ -85,6 +85,55 @@
     }
 
     @Test
+    public void testStartWrite() {
+        assertWithMessage("Start write on clean db").that(mService.startWrite()).isFalse();
+
+        mService.markDirty();
+
+        assertWithMessage("Start write on a dirty DB").that(mService.startWrite()).isTrue();
+
+        mService.markWriteSuccessful();
+        mService.endWrite();
+
+        assertWithMessage("Start write again").that(mService.startWrite()).isFalse();
+    }
+
+    @Test
+    public void testStartWriteAndMarkDirty() {
+        mService.markDirty();
+
+        assertWithMessage("Start database write").that(mService.startWrite()).isTrue();
+
+        mService.markDirty();
+        mService.markWriteSuccessful();
+        mService.endWrite();
+
+        assertWithMessage("Start write again").that(mService.startWrite()).isTrue();
+    }
+
+    @Test
+    public void testStartWriteTwice() {
+        mService.markDirty();
+
+        assertWithMessage("Start database write").that(mService.startWrite()).isTrue();
+        assertWithMessage("Start database write twice").that(mService.startWrite())
+                .isFalse();
+
+        mService.endWrite();
+    }
+
+    @Test
+    public void testMarkSuccessfulWriteWithNoWriteInProgress() {
+        mService.markDirty();
+
+        // Database not marked as clean since, no write in progress
+        mService.markWriteSuccessful();
+
+        // Write successful since database is still dirty.
+        assertWithMessage("Start database write").that(mService.startWrite()).isTrue();
+    }
+
+    @Test
     public void testSaveUserPackageSettings() throws Exception {
         List<WatchdogStorage.UserPackageSettingsEntry> expected = sampleSettings();