1 /* 2 * Copyright 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.room.integration.testapp.test; 18 19 import static org.hamcrest.CoreMatchers.is; 20 import static org.hamcrest.MatcherAssert.assertThat; 21 22 import androidx.room.Dao; 23 import androidx.room.Database; 24 import androidx.room.Entity; 25 import androidx.room.Insert; 26 import androidx.room.InvalidationTracker; 27 import androidx.room.PrimaryKey; 28 import androidx.room.Room; 29 import androidx.room.RoomDatabase; 30 import androidx.test.core.app.ApplicationProvider; 31 import androidx.test.ext.junit.runners.AndroidJUnit4; 32 import androidx.test.filters.FlakyTest; 33 import androidx.test.filters.LargeTest; 34 35 import org.jspecify.annotations.NonNull; 36 import org.jspecify.annotations.Nullable; 37 import org.junit.After; 38 import org.junit.Before; 39 import org.junit.Test; 40 import org.junit.runner.RunWith; 41 42 import java.util.Set; 43 import java.util.concurrent.CountDownLatch; 44 import java.util.concurrent.ExecutionException; 45 import java.util.concurrent.ExecutorService; 46 import java.util.concurrent.Executors; 47 import java.util.concurrent.TimeUnit; 48 import java.util.concurrent.atomic.AtomicInteger; 49 50 /** 51 * Regression test for a situation where an InvalidationTracker callback may intermittently be 52 * invoked too early, too late, or not at all, due to missing transactionality in tracking table 53 * code, when distinct database updates occur in close temporal proximity. 54 */ 55 @LargeTest 56 @FlakyTest( 57 bugId = 154040286, 58 detail = "Behavioral test for potentially intermittent InvalidationTracker problems" 59 ) 60 @RunWith(AndroidJUnit4.class) 61 public class InvalidationTrackerBehavioralTest { 62 private ExecutorService mExecutorService; 63 64 @Before setup()65 public void setup() { 66 mExecutorService = Executors.newSingleThreadExecutor(); 67 } 68 69 @After tearDown()70 public void tearDown() { 71 mExecutorService.shutdown(); 72 } 73 74 @Test testInserts_JournalModeTruncate()75 public void testInserts_JournalModeTruncate() throws ExecutionException, InterruptedException { 76 testInserts(RoomDatabase.JournalMode.TRUNCATE); 77 } 78 79 @Test testInserts_JournalModeWAL()80 public void testInserts_JournalModeWAL() throws ExecutionException, InterruptedException { 81 testInserts(RoomDatabase.JournalMode.WRITE_AHEAD_LOGGING); 82 } 83 testInserts(RoomDatabase.JournalMode journalMode)84 private void testInserts(RoomDatabase.JournalMode journalMode) 85 throws ExecutionException, InterruptedException { 86 testInserts(journalMode, true); 87 testInserts(journalMode, false); 88 } 89 testInserts(RoomDatabase.JournalMode journalMode, boolean multiInstance)90 private void testInserts(RoomDatabase.JournalMode journalMode, boolean multiInstance) 91 throws ExecutionException, InterruptedException { 92 final RoomDatabase.Builder<DB> dbBuilder = Room 93 // We need a physical DB to more easily reproduce invalidation callback errors, 94 // and to support enableMultiInstanceInvalidation, which in turn helps reproduce 95 // missed invalidation callbacks 96 .databaseBuilder(ApplicationProvider.getApplicationContext(), DB.class, DB.NAME) 97 .setJournalMode(journalMode); 98 if (multiInstance) { 99 // Helps reproduce missed invalidation callbacks 100 dbBuilder.enableMultiInstanceInvalidation(); 101 } 102 103 DB db = dbBuilder.build(); 104 105 try { 106 testInserts(db, 30, 0L, 0); 107 testInserts(db, 30, 0L, 10); 108 testInserts(db, 30, 0L, 100); 109 testInserts(db, 30, 0L, 1_000); 110 testInserts(db, 30, 0L, 10_000); 111 testInserts(db, 30, 0L, 100_000); 112 testInserts(db, 30, 1L, 0); 113 } finally { 114 db.close(); 115 ApplicationProvider.getApplicationContext().deleteDatabase(DB.NAME); 116 } 117 } 118 119 /** 120 * Uses repetitions within the test to better approximate real-life behavior, rather than 121 * scheduling the whole test for repeated runs from the outside. 122 */ testInserts( final DB db, final int iterations, final long delayMillis, final int delayNanos )123 private void testInserts( 124 final DB db, final int iterations, final long delayMillis, final int delayNanos 125 ) throws ExecutionException, InterruptedException { 126 final AtomicInteger missedInvalidations = new AtomicInteger(); 127 final AtomicInteger spuriousInvalidations = new AtomicInteger(); 128 129 // Does not terminate execution as soon as a problem is detected, for simplicity. 130 // Usually there should not be a problem; termination is delayed only when there is a 131 // problem. 132 mExecutorService.submit(new Runnable() { 133 volatile @Nullable CountDownLatch mLatch = null; 134 135 // Releases latch when change notification received, increments 136 // spuriousInvalidations when notification received without a recent change 137 final InvalidationTracker.Observer mInvalidationObserver = 138 new InvalidationTracker.Observer(Counter2.TABLE_NAME) { 139 @Override 140 public void onInvalidated(@NonNull Set<String> tables) { 141 if (tables.contains(Counter2.TABLE_NAME)) { 142 // Reading the latch field value is a bit racy, 143 // but it does not matter: 144 // 145 // If we see null here then we're either too early or too late; 146 // too late means that our long delay was too short, so we'd 147 // need to adjust it because now the test failed. 148 // Too early means that we received a spurious invalidation, so 149 // we need to fail the test. 150 // 151 // If we see non-null here instead of null due to a race then 152 // our long delay was just too short and we'll need to adjust it 153 // because the test will have failed. latch.countDown() happens 154 // too late in this case but it has no particular effect. 155 final CountDownLatch latch = mLatch; 156 if (latch == null) { 157 // Spurious invalidation callback; this might occur due to a 158 // large delay beyond the provisioned margin, or due to a 159 // bug in the code under test 160 spuriousInvalidations.incrementAndGet(); 161 } else { 162 latch.countDown(); 163 } 164 } 165 } 166 }; 167 168 @Override 169 public void run() { 170 // Ulterior use of this background thread to add the observer, which is not 171 // legal to do from main thread. 172 // To be close to a real use case we only register the observer once, 173 // we do not re-register for each loop iteration. 174 db.getInvalidationTracker().addObserver(mInvalidationObserver); 175 176 try { 177 // Resets latch and updates missedInvalidations when change notification failed 178 for (int i = 0; i < iterations; ++i) { 179 // The Counter1 table exists just to make InvalidationTracker's life more 180 // difficult, we are not interested in notifications from this one; 181 // inserts may trigger undefined invalidation callback behavior, 182 // depending on table update timing 183 db.counterDao().insert(new Counter1()); 184 185 // Use variable delay to detect different kinds of timing-related problems 186 try { 187 Thread.sleep(delayMillis, delayNanos); 188 } catch (InterruptedException e) { 189 throw new RuntimeException(e); 190 } 191 192 final CountDownLatch latch = new CountDownLatch(1); 193 194 db.runInTransaction(() -> { 195 db.counterDao().insert(new Counter2()); 196 197 // Flag that we have inserted a new value, expect invalidation callback; 198 // do this as late as possible prior to the end of the transaction; 199 // this might cause an occasional false negative due to a race, 200 // where a buggy InvalidationTracker could log successful tracking 201 // even though the transaction is not completed yet, but it does not 202 // matter much, as this is an intentionally flaky test; on another run 203 // it should become apparent that InvalidationTracker is buggy. 204 mLatch = latch; 205 }); 206 207 // Use sufficient delay to give invalidation tracker ample time to catch up; 208 // this would need to be increased if the test had false positives. 209 try { 210 if (!latch.await(10L, TimeUnit.SECONDS)) { 211 // The tracker still has not been called, log an error 212 missedInvalidations.incrementAndGet(); 213 } 214 } catch (InterruptedException e) { 215 throw new RuntimeException(e); 216 } 217 218 mLatch = null; 219 } 220 } finally { 221 db.getInvalidationTracker().removeObserver(mInvalidationObserver); 222 } 223 } 224 }).get(); 225 226 assertThat("Missed invalidations on " + iterations + " iterations with delay of " + 227 delayMillis + " ms, " + delayNanos + " ns", missedInvalidations.get(), is(0)); 228 assertThat("Spurious invalidations on " + iterations + " iterations with delay of " + 229 delayMillis + " ms, " + delayNanos + " ns", spuriousInvalidations.get(), is(0)); 230 } 231 232 @Database(entities = { Counter1.class, Counter2.class }, version = 1, exportSchema = false) 233 abstract static class DB extends RoomDatabase { 234 static final String NAME = "invalidationtrackerbehavioraltest"; 235 counterDao()236 abstract CounterDao counterDao(); 237 } 238 239 @Entity(tableName = Counter1.TABLE_NAME) 240 static final class Counter1 { 241 static final String TABLE_NAME = "counter1"; 242 243 @PrimaryKey(autoGenerate = true) 244 long value; 245 } 246 247 @Entity(tableName = Counter2.TABLE_NAME) 248 static final class Counter2 { 249 static final String TABLE_NAME = "counter2"; 250 251 @PrimaryKey(autoGenerate = true) 252 long value; 253 } 254 255 @Dao 256 abstract static class CounterDao { 257 @Insert insert(Counter1 entity)258 abstract void insert(Counter1 entity); 259 260 @Insert insert(Counter2 entity)261 abstract void insert(Counter2 entity); 262 } 263 } 264