1 /* 2 * Copyright 2020 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.sqlite.inspection; 18 19 import static androidx.sqlite.inspection.DatabaseExtensions.isInMemoryDatabase; 20 import static androidx.sqlite.inspection.DatabaseExtensions.pathForDatabase; 21 22 import android.database.sqlite.SQLiteDatabase; 23 import android.util.ArraySet; 24 25 import androidx.annotation.GuardedBy; 26 27 import org.jspecify.annotations.NonNull; 28 import org.jspecify.annotations.Nullable; 29 30 import java.util.HashMap; 31 import java.util.Iterator; 32 import java.util.Map; 33 import java.util.Set; 34 35 /** 36 * The class keeps track of databases under inspection, and can keep database connections open if 37 * such option is enabled. 38 * <p>Signals expected to be provided to the class: 39 * <ul> 40 * <li>{@link #notifyDatabaseOpened} - should be called when the inspection code detects a 41 * database open operation. 42 * <li>{@link #notifyAllDatabaseReferencesReleased} - should be called when the inspection code 43 * detects that the last database connection reference has been released (effectively a connection 44 * closed event). 45 * <li>{@link #notifyKeepOpenToggle} - should be called when the inspection code detects a 46 * request to change the keep-database-connection-open setting (enabled|disabled). 47 * </ul></p> 48 * <p>Callbacks exposed by the class: 49 * <ul> 50 * <li>Detected a database that is now open, and previously was either closed or not tracked. 51 * <li>Detected a database that is now closed, and previously was reported as open. 52 * </ul></p> 53 */ 54 @SuppressWarnings({"DanglingJavadoc", "SyntheticAccessor"}) 55 class DatabaseRegistry { 56 private static final int NOT_TRACKED = -1; 57 58 // Called when tracking state changes (notTracked|closed)->open 59 private final Callback mOnOpenedCallback; 60 // Called when tracking state changes open->closed 61 private final Callback mOnClosedCallback; 62 63 // True if keep-database-connection-open functionality is enabled. 64 private boolean mKeepDatabasesOpen = false; 65 66 private final Object mLock = new Object(); 67 68 // Starting from '1' to distinguish from '0' which could stand for an unset parameter. 69 @GuardedBy("mLock") private int mNextId = 1; 70 71 // TODO: decide if use weak-references to database objects 72 73 /** 74 * Database connection id -> a list of database references pointing to the same database. The 75 * collection is meant to only contain open connections (eventually consistent after all 76 * callbacks queued behind {@link #mLock} are processed). 77 */ 78 @GuardedBy("mLock") private final Map<Integer, Set<SQLiteDatabase>> mDatabases = 79 new HashMap<>(); 80 81 // Database connection id -> extra database reference used to facilitate the 82 // keep-database-connection-open functionality. 83 @GuardedBy("mLock") private final Map<Integer, KeepOpenReference> mKeepOpenReferences = 84 new HashMap<>(); 85 86 // Database path -> database connection id - allowing to report a consistent id for all 87 // references pointing to the same path. 88 @GuardedBy("mLock") private final Map<String, Integer> mPathToId = new HashMap<>(); 89 90 /** 91 * @param onOpenedCallback called when tracking state changes (notTracked|closed)->open 92 * @param onClosedCallback called when tracking state changes open->closed 93 */ DatabaseRegistry(Callback onOpenedCallback, Callback onClosedCallback)94 DatabaseRegistry(Callback onOpenedCallback, Callback onClosedCallback) { 95 mOnOpenedCallback = onOpenedCallback; 96 mOnClosedCallback = onClosedCallback; 97 } 98 99 /** 100 * Should be called when the inspection code detects a database being open operation. 101 * <p> Note that the method should be called before any code has a chance to close the 102 * database, so e.g. in an {@link androidx.inspection.ArtTooling.ExitHook#onExit} 103 * before the return value is released. 104 * Thread-safe. 105 */ notifyDatabaseOpened(@onNull SQLiteDatabase database)106 void notifyDatabaseOpened(@NonNull SQLiteDatabase database) { 107 handleDatabaseSignal(database); 108 } 109 notifyReleaseReference(SQLiteDatabase database)110 void notifyReleaseReference(SQLiteDatabase database) { 111 synchronized (mLock) { 112 /* Prevent all other methods from releasing a reference if a 113 {@link KeepOpenReference} is present */ 114 for (KeepOpenReference reference : mKeepOpenReferences.values()) { 115 if (reference.mDatabase == database) { 116 /* The below will always succeed as {@link mKeepOpenReferences} only 117 * contains active references: 118 * - we only insert active references into {@link mKeepOpenReferences} 119 * - {@link KeepOpenReference#releaseAllReferences} is the only place where we 120 * allow references to be released 121 * - {@link KeepOpenReference#releaseAllReferences} is private an can only be 122 * called from this class; and before it is called, it must be removed from 123 * from {@link mKeepOpenReferences} 124 */ 125 reference.acquireReference(); 126 } 127 } 128 } 129 } 130 131 /** 132 * Should be called when the inspection code detects that the last database connection 133 * reference has been released (effectively a connection closed event). 134 * Thread-safe. 135 */ notifyAllDatabaseReferencesReleased(@onNull SQLiteDatabase database)136 void notifyAllDatabaseReferencesReleased(@NonNull SQLiteDatabase database) { 137 handleDatabaseSignal(database); 138 } 139 140 /** 141 * Should be called when the inspection code detects a request to change the 142 * keep-database-connection-open setting (enabled|disabled). 143 * Thread-safe. 144 */ notifyKeepOpenToggle(boolean setEnabled)145 void notifyKeepOpenToggle(boolean setEnabled) { 146 synchronized (mLock) { 147 if (mKeepDatabasesOpen == setEnabled) { 148 return; // no change 149 } 150 151 if (setEnabled) { // allowClose -> keepOpen 152 mKeepDatabasesOpen = true; 153 154 for (int id : mDatabases.keySet()) { 155 secureKeepOpenReference(id); 156 } 157 } else { // keepOpen -> allowClose 158 mKeepDatabasesOpen = false; 159 160 Iterator<Map.Entry<Integer, KeepOpenReference>> iterator = 161 mKeepOpenReferences.entrySet().iterator(); 162 while (iterator.hasNext()) { 163 KeepOpenReference reference = iterator.next().getValue(); 164 iterator.remove(); // first remove so it doesn't get in its own way 165 reference.releaseAllReferences(); // then release its references 166 } 167 } 168 } 169 } 170 171 /** 172 * Should be called at the start of inspection to pre-populate the list of databases with 173 * ones on disk. 174 */ notifyOnDiskDatabase(@onNull String path)175 void notifyOnDiskDatabase(@NonNull String path) { 176 synchronized (mLock) { 177 Integer currentId = mPathToId.get(path); 178 if (currentId == null) { 179 int id = mNextId++; 180 mPathToId.put(path, id); 181 mOnClosedCallback.onPostEvent(id, path); 182 } 183 } 184 } 185 186 /** Thread-safe */ handleDatabaseSignal(@onNull SQLiteDatabase database)187 private void handleDatabaseSignal(@NonNull SQLiteDatabase database) { 188 Integer notifyOpenedId = null; 189 Integer notifyClosedId = null; 190 191 synchronized (mLock) { 192 int id = getIdForDatabase(database); 193 194 // TODO: revisit the text below since now we're synchronized on the same lock (mLock) 195 // as releaseReference() calls -- which most likely allows for simplifying invariants 196 // Guaranteed up to date: 197 // - either called in a secure context (e.g. before the newly created connection is 198 // returned from the creation; or with an already acquiredReference on it), 199 // - or called after the last reference was released which cannot be undone. 200 final boolean isOpen = database.isOpen(); 201 202 if (id == NOT_TRACKED) { // handling a transition: not tracked -> tracked 203 id = mNextId++; 204 registerReference(id, database); 205 if (isOpen) { 206 notifyOpenedId = id; 207 } else { 208 notifyClosedId = id; 209 } 210 } else if (isOpen) { // handling a transition: tracked(closed) -> tracked(open) 211 // There are two scenarios here: 212 // - hasReferences is up to date and there is an open reference already, so we 213 // don't need to announce a new one 214 // - hasReferences is stale, and references in it are queued up to be 215 // announced as closing, in this case the outside world thinks that the 216 // connection is open (close ones not processed yet), so we don't need to 217 // announce anything; later, when processing the queued up closed events nothing 218 // will be announced as the currently processed database will keep at least one open 219 // connection. 220 if (!hasReferences(id)) { 221 notifyOpenedId = id; 222 } 223 registerReference(id, database); 224 } else { // handling a transition: tracked(open) -> tracked(closed) 225 // There are two scenarios here: 226 // - hasReferences is up to date and we can use it 227 // - hasReferences is stale, and references in it are queued up to be 228 // announced as closed; in this case there is no harm not announcing a closed 229 // event now as the subsequent calls will do it if appropriate 230 final boolean hasReferencesPre = hasReferences(id); 231 unregisterReference(id, database); 232 final boolean hasReferencesPost = hasReferences(id); 233 if (hasReferencesPre && !hasReferencesPost) { 234 notifyClosedId = id; 235 } 236 } 237 238 secureKeepOpenReference(id); 239 240 // notify of changes if any 241 if (notifyOpenedId != null) { 242 mOnOpenedCallback.onPostEvent(notifyOpenedId, pathForDatabase(database)); 243 } else if (notifyClosedId != null) { 244 mOnClosedCallback.onPostEvent(notifyClosedId, pathForDatabase(database)); 245 } 246 } 247 } 248 249 /** 250 * Returns a currently active database reference if one is available. Null otherwise. 251 * Consumer of this method must release the reference when done using it. 252 * Thread-safe 253 */ getConnection(int databaseId)254 @Nullable SQLiteDatabase getConnection(int databaseId) { 255 synchronized (mLock) { 256 return getConnectionImpl(databaseId); 257 } 258 } 259 260 @GuardedBy("mLock") getConnectionImpl(int databaseId)261 private SQLiteDatabase getConnectionImpl(int databaseId) { 262 KeepOpenReference keepOpenReference = mKeepOpenReferences.get(databaseId); 263 if (keepOpenReference != null) { 264 return keepOpenReference.mDatabase; 265 } 266 267 final Set<SQLiteDatabase> references = mDatabases.get(databaseId); 268 if (references == null) return null; 269 270 // tries to find an open reference preferring write-enabled over read-only 271 SQLiteDatabase readOnlyReference = null; 272 for (SQLiteDatabase reference : references) { 273 if (reference.isOpen()) { 274 if (!reference.isReadOnly()) return reference; // write-enabled was found: return it 275 readOnlyReference = reference; // remember the read-only reference but keep looking 276 } 277 } 278 return readOnlyReference; // or null if we did not find an open reference 279 } 280 281 @GuardedBy("mLock") registerReference(int id, @NonNull SQLiteDatabase database)282 private void registerReference(int id, @NonNull SQLiteDatabase database) { 283 Set<SQLiteDatabase> references = mDatabases.get(id); 284 if (references == null) { 285 references = new ArraySet<>(1); 286 mDatabases.put(id, references); 287 if (!isInMemoryDatabase(database)) { 288 mPathToId.put(pathForDatabase(database), id); 289 } 290 } 291 // mDatabases only tracks open instances 292 if (database.isOpen()) { 293 references.add(database); 294 } 295 } 296 297 @GuardedBy("mLock") unregisterReference(int id, @NonNull SQLiteDatabase database)298 private void unregisterReference(int id, @NonNull SQLiteDatabase database) { 299 Set<SQLiteDatabase> references = mDatabases.get(id); 300 if (references == null) { 301 return; 302 } 303 references.remove(database); 304 } 305 306 @GuardedBy("mLock") secureKeepOpenReference(int id)307 private void secureKeepOpenReference(int id) { 308 if (!mKeepDatabasesOpen || mKeepOpenReferences.containsKey(id)) { 309 // Keep-open is disabled or we already have a keep-open-reference for that id. 310 return; 311 } 312 313 // Try secure a keep-open reference 314 SQLiteDatabase reference = getConnectionImpl(id); 315 if (reference != null) { 316 mKeepOpenReferences.put(id, new KeepOpenReference(reference)); 317 } 318 } 319 320 @GuardedBy("mLock") getIdForDatabase(SQLiteDatabase database)321 private int getIdForDatabase(SQLiteDatabase database) { 322 String databasePath = pathForDatabase(database); 323 324 Integer previousId = mPathToId.get(databasePath); 325 if (previousId != null) { 326 return previousId; 327 } 328 329 if (isInMemoryDatabase(database)) { 330 for (Map.Entry<Integer, Set<SQLiteDatabase>> entry : mDatabases.entrySet()) { 331 for (SQLiteDatabase entryDb : entry.getValue()) { 332 if (entryDb == database) { 333 return entry.getKey(); 334 } 335 } 336 } 337 } 338 339 return NOT_TRACKED; 340 } 341 342 @GuardedBy("mLock") hasReferences(int databaseId)343 private boolean hasReferences(int databaseId) { 344 final Set<SQLiteDatabase> references = mDatabases.get(databaseId); 345 return references != null && !references.isEmpty(); 346 } 347 348 interface Callback { onPostEvent(int databaseId, String path)349 void onPostEvent(int databaseId, String path); 350 } 351 352 private static final class KeepOpenReference { 353 private final SQLiteDatabase mDatabase; 354 355 private final Object mLock = new Object(); 356 @GuardedBy("mLock") private int mAcquiredReferenceCount = 0; 357 KeepOpenReference(SQLiteDatabase database)358 private KeepOpenReference(SQLiteDatabase database) { 359 mDatabase = database; 360 } 361 acquireReference()362 private void acquireReference() { 363 synchronized (mLock) { 364 if (DatabaseExtensions.tryAcquireReference(mDatabase)) { 365 mAcquiredReferenceCount++; 366 } 367 } 368 } 369 370 /** 371 * This should only be called after removing the object from 372 * {@link DatabaseRegistry#mKeepOpenReferences}. Otherwise, the object will get in its 373 * own way or releasing its references. 374 */ releaseAllReferences()375 private void releaseAllReferences() { 376 synchronized (mLock) { 377 for (; mAcquiredReferenceCount > 0; mAcquiredReferenceCount--) { 378 mDatabase.releaseReference(); 379 } 380 } 381 } 382 } 383 } 384