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