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 package androidx.room.support 17 18 import android.os.SystemClock 19 import androidx.annotation.GuardedBy 20 import androidx.room.support.AutoCloser.Watch 21 import androidx.sqlite.db.SupportSQLiteDatabase 22 import androidx.sqlite.db.SupportSQLiteOpenHelper 23 import java.util.concurrent.TimeUnit 24 import java.util.concurrent.atomic.AtomicInteger 25 import java.util.concurrent.atomic.AtomicLong 26 import kotlinx.coroutines.CoroutineScope 27 import kotlinx.coroutines.Job 28 import kotlinx.coroutines.delay 29 import kotlinx.coroutines.launch 30 31 /** 32 * AutoCloser is responsible for automatically opening (using `delegateOpenHelper`) and closing (on 33 * a timer started when there are no remaining references) a [SupportSQLiteDatabase]. 34 * 35 * It is important to ensure that the reference count is incremented when using a returned database. 36 * 37 * @param timeoutAmount time for auto close timer 38 * @param timeUnit time unit for `timeoutAmount` 39 * @param watch A [Watch] implementation to get an increasing timestamp. 40 */ 41 internal class AutoCloser( 42 timeoutAmount: Long, 43 timeUnit: TimeUnit, <lambda>null44 private val watch: Watch = Watch { SystemClock.uptimeMillis() } 45 ) { 46 // The unwrapped SupportSQLiteOpenHelper (i.e. not AutoClosingRoomOpenHelper) 47 private lateinit var delegateOpenHelper: SupportSQLiteOpenHelper 48 49 private lateinit var coroutineScope: CoroutineScope 50 51 private var onAutoCloseCallback: (() -> Unit)? = null 52 53 private val lock = Any() 54 55 private val autoCloseTimeoutInMs = timeUnit.toMillis(timeoutAmount) 56 57 private val referenceCount = AtomicInteger(0) 58 59 private var lastDecrementRefCountTimeStamp = AtomicLong(watch.getMillis()) 60 61 // The unwrapped SupportSqliteDatabase (i.e. not AutoCloseSupportSQLiteDatabase) 62 @GuardedBy("lock") internal var delegateDatabase: SupportSQLiteDatabase? = null 63 64 private var manuallyClosed = false 65 66 private var autoCloseJob: Job? = null 67 autoCloseDatabasenull68 private fun autoCloseDatabase(): Unit = 69 synchronized(lock) { 70 if (watch.getMillis() - lastDecrementRefCountTimeStamp.get() < autoCloseTimeoutInMs) { 71 // An increment + decrement beat us to closing the db. We 72 // will not close the database, and there should be at least 73 // one more auto-close scheduled. 74 return 75 } 76 if (referenceCount.get() != 0) { 77 // An increment beat us to closing the db. We don't close the 78 // db, and another closer will be scheduled once the ref 79 // count is decremented. 80 return 81 } 82 onAutoCloseCallback?.invoke() 83 ?: error( 84 "onAutoCloseCallback is null but it should have been set before use. " + 85 "Please file a bug against Room at: $BUG_LINK" 86 ) 87 delegateDatabase?.let { 88 if (it.isOpen) { 89 it.close() 90 } 91 } 92 delegateDatabase = null 93 } 94 95 /** 96 * Since we need to construct the AutoCloser in the [androidx.room.RoomDatabase.Builder], we 97 * need to set the `delegateOpenHelper` after construction. 98 * 99 * @param delegateOpenHelper the open helper that is used to create new [SupportSQLiteDatabase]. 100 */ initOpenHelpernull101 fun initOpenHelper(delegateOpenHelper: SupportSQLiteOpenHelper) { 102 require(delegateOpenHelper !is AutoClosingRoomOpenHelper) 103 this.delegateOpenHelper = delegateOpenHelper 104 } 105 106 /** 107 * Since we need to construct the AutoCloser in the [androidx.room.RoomDatabase.Builder], we 108 * need to set the `coroutineScope` after construction. 109 * 110 * @param coroutineScope where the auto close will execute. 111 */ initCoroutineScopenull112 fun initCoroutineScope(coroutineScope: CoroutineScope) { 113 this.coroutineScope = coroutineScope 114 } 115 116 /** 117 * Execute a ref counting function. The function will receive an unwrapped open database and 118 * this database will stay open until at least after function returns. If there are no more 119 * references in use for the db once function completes, an auto close operation will be 120 * scheduled. 121 */ executeRefCountingFunctionnull122 fun <V> executeRefCountingFunction(block: (SupportSQLiteDatabase) -> V): V = 123 try { 124 block(incrementCountAndEnsureDbIsOpen()) 125 } finally { 126 decrementCountAndScheduleClose() 127 } 128 129 /** 130 * Confirms that auto-close function is no longer running and confirms that `delegateDatabase` 131 * is set and open. `delegateDatabase` will not be auto closed until 132 * [decrementCountAndScheduleClose] is called. [decrementCountAndScheduleClose] must be called 133 * once for each call to [incrementCountAndEnsureDbIsOpen]. 134 * 135 * If this throws an exception, [decrementCountAndScheduleClose] must still be called! 136 * 137 * @return the *unwrapped* SupportSQLiteDatabase. 138 */ incrementCountAndEnsureDbIsOpennull139 fun incrementCountAndEnsureDbIsOpen(): SupportSQLiteDatabase { 140 // If there is a scheduled auto close operation, cancel it. 141 autoCloseJob?.cancel() 142 autoCloseJob = null 143 144 referenceCount.incrementAndGet() 145 check(!manuallyClosed) { "Attempting to open already closed database." } 146 synchronized(lock) { 147 delegateDatabase?.let { 148 if (it.isOpen) { 149 return it 150 } 151 } 152 return delegateOpenHelper.writableDatabase.also { delegateDatabase = it } 153 } 154 } 155 156 /** 157 * Decrements the ref count and schedules a close if there are no other references to the db. 158 * This must only be called after a corresponding [incrementCountAndEnsureDbIsOpen] call. 159 */ decrementCountAndScheduleClosenull160 fun decrementCountAndScheduleClose() { 161 val newCount = referenceCount.decrementAndGet() 162 check(newCount >= 0) { "Unbalanced reference count." } 163 lastDecrementRefCountTimeStamp.set(watch.getMillis()) 164 if (newCount == 0) { 165 autoCloseJob = 166 coroutineScope.launch { 167 delay(autoCloseTimeoutInMs) 168 autoCloseDatabase() 169 } 170 } 171 } 172 173 /** Close the database if it is still active. */ closeDatabaseIfOpennull174 fun closeDatabaseIfOpen() { 175 synchronized(lock) { 176 manuallyClosed = true 177 autoCloseJob?.cancel() 178 autoCloseJob = null 179 delegateDatabase?.close() 180 delegateDatabase = null 181 } 182 } 183 184 /** 185 * The auto closer is still active if the database has not been closed. This means that whether 186 * or not the underlying database is closed, when active we will re-open it on the next access. 187 * 188 * @return a boolean indicating whether the auto closer is still active 189 */ 190 val isActive: Boolean 191 get() = !manuallyClosed 192 193 /** 194 * Sets a callback that will be run every time the database is auto-closed. This callback needs 195 * to be lightweight since it is run while holding a lock. 196 * 197 * @param onAutoClose the callback to run 198 */ setAutoCloseCallbacknull199 fun setAutoCloseCallback(onAutoClose: () -> Unit) { 200 onAutoCloseCallback = onAutoClose 201 } 202 203 /** Returns the current auto close callback. This is only visible for testing. */ 204 internal val autoCloseCallbackForTest 205 get() = onAutoCloseCallback 206 207 /** Returns the current ref count for this auto closer. This is only visible for testing. */ 208 internal val refCountForTest: Int 209 get() = referenceCount.get() 210 211 /** Represents a counting time tracker function. */ interfacenull212 fun interface Watch { 213 fun getMillis(): Long 214 } 215 216 companion object { 217 const val BUG_LINK = 218 "https://issuetracker.google.com/issues/new?component=413107&template=1096568" 219 } 220 } 221