1 /* 2 * Copyright (C) 2011 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 android.database.sqlite; 18 19 import android.database.sqlite.SQLiteDebug.DbStats; 20 import android.os.CancellationSignal; 21 import android.os.Handler; 22 import android.os.Looper; 23 import android.os.Message; 24 import android.os.OperationCanceledException; 25 import android.os.SystemClock; 26 import android.text.TextUtils; 27 import android.util.ArraySet; 28 import android.util.Log; 29 import android.util.PrefixPrinter; 30 import android.util.Printer; 31 32 import com.android.internal.annotations.GuardedBy; 33 import com.android.internal.annotations.VisibleForTesting; 34 35 import dalvik.system.CloseGuard; 36 37 import java.io.Closeable; 38 import java.io.File; 39 import java.util.ArrayList; 40 import java.util.Map; 41 import java.util.WeakHashMap; 42 import java.util.concurrent.atomic.AtomicBoolean; 43 import java.util.concurrent.atomic.AtomicLong; 44 import java.util.concurrent.locks.LockSupport; 45 46 /** 47 * Maintains a pool of active SQLite database connections. 48 * <p> 49 * At any given time, a connection is either owned by the pool, or it has been 50 * acquired by a {@link SQLiteSession}. When the {@link SQLiteSession} is 51 * finished with the connection it is using, it must return the connection 52 * back to the pool. 53 * </p><p> 54 * The pool holds strong references to the connections it owns. However, 55 * it only holds <em>weak references</em> to the connections that sessions 56 * have acquired from it. Using weak references in the latter case ensures 57 * that the connection pool can detect when connections have been improperly 58 * abandoned so that it can create new connections to replace them if needed. 59 * </p><p> 60 * The connection pool is thread-safe (but the connections themselves are not). 61 * </p> 62 * 63 * <h2>Exception safety</h2> 64 * <p> 65 * This code attempts to maintain the invariant that opened connections are 66 * always owned. Unfortunately that means it needs to handle exceptions 67 * all over to ensure that broken connections get cleaned up. Most 68 * operations invokving SQLite can throw {@link SQLiteException} or other 69 * runtime exceptions. This is a bit of a pain to deal with because the compiler 70 * cannot help us catch missing exception handling code. 71 * </p><p> 72 * The general rule for this file: If we are making calls out to 73 * {@link SQLiteConnection} then we must be prepared to handle any 74 * runtime exceptions it might throw at us. Note that out-of-memory 75 * is an {@link Error}, not a {@link RuntimeException}. We don't trouble ourselves 76 * handling out of memory because it is hard to do anything at all sensible then 77 * and most likely the VM is about to crash. 78 * </p> 79 * 80 * @hide 81 */ 82 public final class SQLiteConnectionPool implements Closeable { 83 private static final String TAG = "SQLiteConnectionPool"; 84 85 // Amount of time to wait in milliseconds before unblocking acquireConnection 86 // and logging a message about the connection pool being busy. 87 private static final long CONNECTION_POOL_BUSY_MILLIS = 30 * 1000; // 30 seconds 88 89 private final CloseGuard mCloseGuard = CloseGuard.get(); 90 91 private final Object mLock = new Object(); 92 private final AtomicBoolean mConnectionLeaked = new AtomicBoolean(); 93 private final SQLiteDatabaseConfiguration mConfiguration; 94 private int mMaxConnectionPoolSize; 95 private boolean mIsOpen; 96 private int mNextConnectionId; 97 98 private ConnectionWaiter mConnectionWaiterPool; 99 private ConnectionWaiter mConnectionWaiterQueue; 100 101 // Strong references to all available connections. 102 private final ArrayList<SQLiteConnection> mAvailableNonPrimaryConnections = 103 new ArrayList<SQLiteConnection>(); 104 private SQLiteConnection mAvailablePrimaryConnection; 105 106 @GuardedBy("mLock") 107 private IdleConnectionHandler mIdleConnectionHandler; 108 109 private final AtomicLong mTotalExecutionTimeCounter = new AtomicLong(0); 110 111 // Describes what should happen to an acquired connection when it is returned to the pool. 112 enum AcquiredConnectionStatus { 113 // The connection should be returned to the pool as usual. 114 NORMAL, 115 116 // The connection must be reconfigured before being returned. 117 RECONFIGURE, 118 119 // The connection must be closed and discarded. 120 DISCARD, 121 } 122 123 // Weak references to all acquired connections. The associated value 124 // indicates whether the connection must be reconfigured before being 125 // returned to the available connection list or discarded. 126 // For example, the prepared statement cache size may have changed and 127 // need to be updated in preparation for the next client. 128 private final WeakHashMap<SQLiteConnection, AcquiredConnectionStatus> mAcquiredConnections = 129 new WeakHashMap<SQLiteConnection, AcquiredConnectionStatus>(); 130 131 /** 132 * Connection flag: Read-only. 133 * <p> 134 * This flag indicates that the connection will only be used to 135 * perform read-only operations. 136 * </p> 137 */ 138 public static final int CONNECTION_FLAG_READ_ONLY = 1 << 0; 139 140 /** 141 * Connection flag: Primary connection affinity. 142 * <p> 143 * This flag indicates that the primary connection is required. 144 * This flag helps support legacy applications that expect most data modifying 145 * operations to be serialized by locking the primary database connection. 146 * Setting this flag essentially implements the old "db lock" concept by preventing 147 * an operation from being performed until it can obtain exclusive access to 148 * the primary connection. 149 * </p> 150 */ 151 public static final int CONNECTION_FLAG_PRIMARY_CONNECTION_AFFINITY = 1 << 1; 152 153 /** 154 * Connection flag: Connection is being used interactively. 155 * <p> 156 * This flag indicates that the connection is needed by the UI thread. 157 * The connection pool can use this flag to elevate the priority 158 * of the database connection request. 159 * </p> 160 */ 161 public static final int CONNECTION_FLAG_INTERACTIVE = 1 << 2; 162 SQLiteConnectionPool(SQLiteDatabaseConfiguration configuration)163 private SQLiteConnectionPool(SQLiteDatabaseConfiguration configuration) { 164 mConfiguration = new SQLiteDatabaseConfiguration(configuration); 165 setMaxConnectionPoolSizeLocked(); 166 // If timeout is set, setup idle connection handler 167 // In case of MAX_VALUE - idle connections are never closed 168 if (mConfiguration.idleConnectionTimeoutMs != Long.MAX_VALUE) { 169 setupIdleConnectionHandler(Looper.getMainLooper(), 170 mConfiguration.idleConnectionTimeoutMs); 171 } 172 } 173 174 @Override finalize()175 protected void finalize() throws Throwable { 176 try { 177 dispose(true); 178 } finally { 179 super.finalize(); 180 } 181 } 182 183 /** 184 * Opens a connection pool for the specified database. 185 * 186 * @param configuration The database configuration. 187 * @return The connection pool. 188 * 189 * @throws SQLiteException if a database error occurs. 190 */ open(SQLiteDatabaseConfiguration configuration)191 public static SQLiteConnectionPool open(SQLiteDatabaseConfiguration configuration) { 192 if (configuration == null) { 193 throw new IllegalArgumentException("configuration must not be null."); 194 } 195 196 // Create the pool. 197 SQLiteConnectionPool pool = new SQLiteConnectionPool(configuration); 198 pool.open(); // might throw 199 return pool; 200 } 201 202 // Might throw open()203 private void open() { 204 // Open the primary connection. 205 // This might throw if the database is corrupt. 206 mAvailablePrimaryConnection = openConnectionLocked(mConfiguration, 207 true /*primaryConnection*/); // might throw 208 // Mark it released so it can be closed after idle timeout 209 synchronized (mLock) { 210 if (mIdleConnectionHandler != null) { 211 mIdleConnectionHandler.connectionReleased(mAvailablePrimaryConnection); 212 } 213 } 214 215 // Mark the pool as being open for business. 216 mIsOpen = true; 217 mCloseGuard.open("close"); 218 } 219 220 /** 221 * Closes the connection pool. 222 * <p> 223 * When the connection pool is closed, it will refuse all further requests 224 * to acquire connections. All connections that are currently available in 225 * the pool are closed immediately. Any connections that are still in use 226 * will be closed as soon as they are returned to the pool. 227 * </p> 228 * 229 * @throws IllegalStateException if the pool has been closed. 230 */ close()231 public void close() { 232 dispose(false); 233 } 234 dispose(boolean finalized)235 private void dispose(boolean finalized) { 236 if (mCloseGuard != null) { 237 if (finalized) { 238 mCloseGuard.warnIfOpen(); 239 } 240 mCloseGuard.close(); 241 } 242 243 if (!finalized) { 244 // Close all connections. We don't need (or want) to do this 245 // when finalized because we don't know what state the connections 246 // themselves will be in. The finalizer is really just here for CloseGuard. 247 // The connections will take care of themselves when their own finalizers run. 248 synchronized (mLock) { 249 throwIfClosedLocked(); 250 251 mIsOpen = false; 252 253 closeAvailableConnectionsAndLogExceptionsLocked(); 254 255 final int pendingCount = mAcquiredConnections.size(); 256 if (pendingCount != 0) { 257 Log.i(TAG, "The connection pool for " + mConfiguration.label 258 + " has been closed but there are still " 259 + pendingCount + " connections in use. They will be closed " 260 + "as they are released back to the pool."); 261 } 262 263 wakeConnectionWaitersLocked(); 264 } 265 } 266 } 267 268 /** 269 * Reconfigures the database configuration of the connection pool and all of its 270 * connections. 271 * <p> 272 * Configuration changes are propagated down to connections immediately if 273 * they are available or as soon as they are released. This includes changes 274 * that affect the size of the pool. 275 * </p> 276 * 277 * @param configuration The new configuration. 278 * 279 * @throws IllegalStateException if the pool has been closed. 280 */ reconfigure(SQLiteDatabaseConfiguration configuration)281 public void reconfigure(SQLiteDatabaseConfiguration configuration) { 282 if (configuration == null) { 283 throw new IllegalArgumentException("configuration must not be null."); 284 } 285 286 synchronized (mLock) { 287 throwIfClosedLocked(); 288 289 boolean walModeChanged = ((configuration.openFlags ^ mConfiguration.openFlags) 290 & SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING) != 0; 291 if (walModeChanged) { 292 // WAL mode can only be changed if there are no acquired connections 293 // because we need to close all but the primary connection first. 294 if (!mAcquiredConnections.isEmpty()) { 295 throw new IllegalStateException("Write Ahead Logging (WAL) mode cannot " 296 + "be enabled or disabled while there are transactions in " 297 + "progress. Finish all transactions and release all active " 298 + "database connections first."); 299 } 300 301 // Close all non-primary connections. This should happen immediately 302 // because none of them are in use. 303 closeAvailableNonPrimaryConnectionsAndLogExceptionsLocked(); 304 assert mAvailableNonPrimaryConnections.isEmpty(); 305 } 306 307 boolean foreignKeyModeChanged = configuration.foreignKeyConstraintsEnabled 308 != mConfiguration.foreignKeyConstraintsEnabled; 309 if (foreignKeyModeChanged) { 310 // Foreign key constraints can only be changed if there are no transactions 311 // in progress. To make this clear, we throw an exception if there are 312 // any acquired connections. 313 if (!mAcquiredConnections.isEmpty()) { 314 throw new IllegalStateException("Foreign Key Constraints cannot " 315 + "be enabled or disabled while there are transactions in " 316 + "progress. Finish all transactions and release all active " 317 + "database connections first."); 318 } 319 } 320 321 // We should do in-place switching when transitioning from compatibility WAL 322 // to rollback journal. Otherwise transient connection state will be lost 323 boolean onlyCompatWalChanged = (mConfiguration.openFlags ^ configuration.openFlags) 324 == SQLiteDatabase.ENABLE_LEGACY_COMPATIBILITY_WAL; 325 326 if (!onlyCompatWalChanged && mConfiguration.openFlags != configuration.openFlags) { 327 // If we are changing open flags and WAL mode at the same time, then 328 // we have no choice but to close the primary connection beforehand 329 // because there can only be one connection open when we change WAL mode. 330 if (walModeChanged) { 331 closeAvailableConnectionsAndLogExceptionsLocked(); 332 } 333 334 // Try to reopen the primary connection using the new open flags then 335 // close and discard all existing connections. 336 // This might throw if the database is corrupt or cannot be opened in 337 // the new mode in which case existing connections will remain untouched. 338 SQLiteConnection newPrimaryConnection = openConnectionLocked(configuration, 339 true /*primaryConnection*/); // might throw 340 341 closeAvailableConnectionsAndLogExceptionsLocked(); 342 discardAcquiredConnectionsLocked(); 343 344 mAvailablePrimaryConnection = newPrimaryConnection; 345 mConfiguration.updateParametersFrom(configuration); 346 setMaxConnectionPoolSizeLocked(); 347 } else { 348 // Reconfigure the database connections in place. 349 mConfiguration.updateParametersFrom(configuration); 350 setMaxConnectionPoolSizeLocked(); 351 352 closeExcessConnectionsAndLogExceptionsLocked(); 353 reconfigureAllConnectionsLocked(); 354 } 355 356 wakeConnectionWaitersLocked(); 357 } 358 } 359 360 /** 361 * Acquires a connection from the pool. 362 * <p> 363 * The caller must call {@link #releaseConnection} to release the connection 364 * back to the pool when it is finished. Failure to do so will result 365 * in much unpleasantness. 366 * </p> 367 * 368 * @param sql If not null, try to find a connection that already has 369 * the specified SQL statement in its prepared statement cache. 370 * @param connectionFlags The connection request flags. 371 * @param cancellationSignal A signal to cancel the operation in progress, or null if none. 372 * @return The connection that was acquired, never null. 373 * 374 * @throws IllegalStateException if the pool has been closed. 375 * @throws SQLiteException if a database error occurs. 376 * @throws OperationCanceledException if the operation was canceled. 377 */ acquireConnection(String sql, int connectionFlags, CancellationSignal cancellationSignal)378 public SQLiteConnection acquireConnection(String sql, int connectionFlags, 379 CancellationSignal cancellationSignal) { 380 SQLiteConnection con = waitForConnection(sql, connectionFlags, cancellationSignal); 381 synchronized (mLock) { 382 if (mIdleConnectionHandler != null) { 383 mIdleConnectionHandler.connectionAcquired(con); 384 } 385 } 386 return con; 387 } 388 389 /** 390 * Releases a connection back to the pool. 391 * <p> 392 * It is ok to call this method after the pool has closed, to release 393 * connections that were still in use at the time of closure. 394 * </p> 395 * 396 * @param connection The connection to release. Must not be null. 397 * 398 * @throws IllegalStateException if the connection was not acquired 399 * from this pool or if it has already been released. 400 */ releaseConnection(SQLiteConnection connection)401 public void releaseConnection(SQLiteConnection connection) { 402 synchronized (mLock) { 403 if (mIdleConnectionHandler != null) { 404 mIdleConnectionHandler.connectionReleased(connection); 405 } 406 AcquiredConnectionStatus status = mAcquiredConnections.remove(connection); 407 if (status == null) { 408 throw new IllegalStateException("Cannot perform this operation " 409 + "because the specified connection was not acquired " 410 + "from this pool or has already been released."); 411 } 412 413 if (!mIsOpen) { 414 closeConnectionAndLogExceptionsLocked(connection); 415 } else if (connection.isPrimaryConnection()) { 416 if (recycleConnectionLocked(connection, status)) { 417 assert mAvailablePrimaryConnection == null; 418 mAvailablePrimaryConnection = connection; 419 } 420 wakeConnectionWaitersLocked(); 421 } else if (mAvailableNonPrimaryConnections.size() >= mMaxConnectionPoolSize - 1) { 422 closeConnectionAndLogExceptionsLocked(connection); 423 } else { 424 if (recycleConnectionLocked(connection, status)) { 425 mAvailableNonPrimaryConnections.add(connection); 426 } 427 wakeConnectionWaitersLocked(); 428 } 429 } 430 } 431 432 // Can't throw. 433 @GuardedBy("mLock") recycleConnectionLocked(SQLiteConnection connection, AcquiredConnectionStatus status)434 private boolean recycleConnectionLocked(SQLiteConnection connection, 435 AcquiredConnectionStatus status) { 436 if (status == AcquiredConnectionStatus.RECONFIGURE) { 437 try { 438 connection.reconfigure(mConfiguration); // might throw 439 } catch (RuntimeException ex) { 440 Log.e(TAG, "Failed to reconfigure released connection, closing it: " 441 + connection, ex); 442 status = AcquiredConnectionStatus.DISCARD; 443 } 444 } 445 if (status == AcquiredConnectionStatus.DISCARD) { 446 closeConnectionAndLogExceptionsLocked(connection); 447 return false; 448 } 449 return true; 450 } 451 452 /** 453 * Returns true if the session should yield the connection due to 454 * contention over available database connections. 455 * 456 * @param connection The connection owned by the session. 457 * @param connectionFlags The connection request flags. 458 * @return True if the session should yield its connection. 459 * 460 * @throws IllegalStateException if the connection was not acquired 461 * from this pool or if it has already been released. 462 */ shouldYieldConnection(SQLiteConnection connection, int connectionFlags)463 public boolean shouldYieldConnection(SQLiteConnection connection, int connectionFlags) { 464 synchronized (mLock) { 465 if (!mAcquiredConnections.containsKey(connection)) { 466 throw new IllegalStateException("Cannot perform this operation " 467 + "because the specified connection was not acquired " 468 + "from this pool or has already been released."); 469 } 470 471 if (!mIsOpen) { 472 return false; 473 } 474 475 return isSessionBlockingImportantConnectionWaitersLocked( 476 connection.isPrimaryConnection(), connectionFlags); 477 } 478 } 479 480 /** 481 * Collects statistics about database connection memory usage. 482 * 483 * @param dbStatsList The list to populate. 484 */ collectDbStats(ArrayList<DbStats> dbStatsList)485 public void collectDbStats(ArrayList<DbStats> dbStatsList) { 486 synchronized (mLock) { 487 if (mAvailablePrimaryConnection != null) { 488 mAvailablePrimaryConnection.collectDbStats(dbStatsList); 489 } 490 491 for (SQLiteConnection connection : mAvailableNonPrimaryConnections) { 492 connection.collectDbStats(dbStatsList); 493 } 494 495 for (SQLiteConnection connection : mAcquiredConnections.keySet()) { 496 connection.collectDbStatsUnsafe(dbStatsList); 497 } 498 } 499 } 500 501 // Might throw. openConnectionLocked(SQLiteDatabaseConfiguration configuration, boolean primaryConnection)502 private SQLiteConnection openConnectionLocked(SQLiteDatabaseConfiguration configuration, 503 boolean primaryConnection) { 504 final int connectionId = mNextConnectionId++; 505 return SQLiteConnection.open(this, configuration, 506 connectionId, primaryConnection); // might throw 507 } 508 onConnectionLeaked()509 void onConnectionLeaked() { 510 // This code is running inside of the SQLiteConnection finalizer. 511 // 512 // We don't know whether it is just the connection that has been finalized (and leaked) 513 // or whether the connection pool has also been or is about to be finalized. 514 // Consequently, it would be a bad idea to try to grab any locks or to 515 // do any significant work here. So we do the simplest possible thing and 516 // set a flag. waitForConnection() periodically checks this flag (when it 517 // times out) so that it can recover from leaked connections and wake 518 // itself or other threads up if necessary. 519 // 520 // You might still wonder why we don't try to do more to wake up the waiters 521 // immediately. First, as explained above, it would be hard to do safely 522 // unless we started an extra Thread to function as a reference queue. Second, 523 // this is never supposed to happen in normal operation. Third, there is no 524 // guarantee that the GC will actually detect the leak in a timely manner so 525 // it's not all that important that we recover from the leak in a timely manner 526 // either. Fourth, if a badly behaved application finds itself hung waiting for 527 // several seconds while waiting for a leaked connection to be detected and recreated, 528 // then perhaps its authors will have added incentive to fix the problem! 529 530 Log.w(TAG, "A SQLiteConnection object for database '" 531 + mConfiguration.label + "' was leaked! Please fix your application " 532 + "to end transactions in progress properly and to close the database " 533 + "when it is no longer needed."); 534 535 mConnectionLeaked.set(true); 536 } 537 onStatementExecuted(long executionTimeMs)538 void onStatementExecuted(long executionTimeMs) { 539 mTotalExecutionTimeCounter.addAndGet(executionTimeMs); 540 } 541 542 // Can't throw. 543 @GuardedBy("mLock") closeAvailableConnectionsAndLogExceptionsLocked()544 private void closeAvailableConnectionsAndLogExceptionsLocked() { 545 closeAvailableNonPrimaryConnectionsAndLogExceptionsLocked(); 546 547 if (mAvailablePrimaryConnection != null) { 548 closeConnectionAndLogExceptionsLocked(mAvailablePrimaryConnection); 549 mAvailablePrimaryConnection = null; 550 } 551 } 552 553 // Can't throw. 554 @GuardedBy("mLock") closeAvailableConnectionLocked(int connectionId)555 private boolean closeAvailableConnectionLocked(int connectionId) { 556 final int count = mAvailableNonPrimaryConnections.size(); 557 for (int i = count - 1; i >= 0; i--) { 558 SQLiteConnection c = mAvailableNonPrimaryConnections.get(i); 559 if (c.getConnectionId() == connectionId) { 560 closeConnectionAndLogExceptionsLocked(c); 561 mAvailableNonPrimaryConnections.remove(i); 562 return true; 563 } 564 } 565 566 if (mAvailablePrimaryConnection != null 567 && mAvailablePrimaryConnection.getConnectionId() == connectionId) { 568 closeConnectionAndLogExceptionsLocked(mAvailablePrimaryConnection); 569 mAvailablePrimaryConnection = null; 570 return true; 571 } 572 return false; 573 } 574 575 // Can't throw. 576 @GuardedBy("mLock") closeAvailableNonPrimaryConnectionsAndLogExceptionsLocked()577 private void closeAvailableNonPrimaryConnectionsAndLogExceptionsLocked() { 578 final int count = mAvailableNonPrimaryConnections.size(); 579 for (int i = 0; i < count; i++) { 580 closeConnectionAndLogExceptionsLocked(mAvailableNonPrimaryConnections.get(i)); 581 } 582 mAvailableNonPrimaryConnections.clear(); 583 } 584 585 /** 586 * Close non-primary connections that are not currently in use. This method is safe to use 587 * in finalize block as it doesn't throw RuntimeExceptions. 588 */ closeAvailableNonPrimaryConnectionsAndLogExceptions()589 void closeAvailableNonPrimaryConnectionsAndLogExceptions() { 590 synchronized (mLock) { 591 closeAvailableNonPrimaryConnectionsAndLogExceptionsLocked(); 592 } 593 } 594 595 // Can't throw. 596 @GuardedBy("mLock") closeExcessConnectionsAndLogExceptionsLocked()597 private void closeExcessConnectionsAndLogExceptionsLocked() { 598 int availableCount = mAvailableNonPrimaryConnections.size(); 599 while (availableCount-- > mMaxConnectionPoolSize - 1) { 600 SQLiteConnection connection = 601 mAvailableNonPrimaryConnections.remove(availableCount); 602 closeConnectionAndLogExceptionsLocked(connection); 603 } 604 } 605 606 // Can't throw. 607 @GuardedBy("mLock") closeConnectionAndLogExceptionsLocked(SQLiteConnection connection)608 private void closeConnectionAndLogExceptionsLocked(SQLiteConnection connection) { 609 try { 610 connection.close(); // might throw 611 if (mIdleConnectionHandler != null) { 612 mIdleConnectionHandler.connectionClosed(connection); 613 } 614 } catch (RuntimeException ex) { 615 Log.e(TAG, "Failed to close connection, its fate is now in the hands " 616 + "of the merciful GC: " + connection, ex); 617 } 618 } 619 620 // Can't throw. discardAcquiredConnectionsLocked()621 private void discardAcquiredConnectionsLocked() { 622 markAcquiredConnectionsLocked(AcquiredConnectionStatus.DISCARD); 623 } 624 625 // Can't throw. 626 @GuardedBy("mLock") reconfigureAllConnectionsLocked()627 private void reconfigureAllConnectionsLocked() { 628 if (mAvailablePrimaryConnection != null) { 629 try { 630 mAvailablePrimaryConnection.reconfigure(mConfiguration); // might throw 631 } catch (RuntimeException ex) { 632 Log.e(TAG, "Failed to reconfigure available primary connection, closing it: " 633 + mAvailablePrimaryConnection, ex); 634 closeConnectionAndLogExceptionsLocked(mAvailablePrimaryConnection); 635 mAvailablePrimaryConnection = null; 636 } 637 } 638 639 int count = mAvailableNonPrimaryConnections.size(); 640 for (int i = 0; i < count; i++) { 641 final SQLiteConnection connection = mAvailableNonPrimaryConnections.get(i); 642 try { 643 connection.reconfigure(mConfiguration); // might throw 644 } catch (RuntimeException ex) { 645 Log.e(TAG, "Failed to reconfigure available non-primary connection, closing it: " 646 + connection, ex); 647 closeConnectionAndLogExceptionsLocked(connection); 648 mAvailableNonPrimaryConnections.remove(i--); 649 count -= 1; 650 } 651 } 652 653 markAcquiredConnectionsLocked(AcquiredConnectionStatus.RECONFIGURE); 654 } 655 656 // Can't throw. markAcquiredConnectionsLocked(AcquiredConnectionStatus status)657 private void markAcquiredConnectionsLocked(AcquiredConnectionStatus status) { 658 if (!mAcquiredConnections.isEmpty()) { 659 ArrayList<SQLiteConnection> keysToUpdate = new ArrayList<SQLiteConnection>( 660 mAcquiredConnections.size()); 661 for (Map.Entry<SQLiteConnection, AcquiredConnectionStatus> entry 662 : mAcquiredConnections.entrySet()) { 663 AcquiredConnectionStatus oldStatus = entry.getValue(); 664 if (status != oldStatus 665 && oldStatus != AcquiredConnectionStatus.DISCARD) { 666 keysToUpdate.add(entry.getKey()); 667 } 668 } 669 final int updateCount = keysToUpdate.size(); 670 for (int i = 0; i < updateCount; i++) { 671 mAcquiredConnections.put(keysToUpdate.get(i), status); 672 } 673 } 674 } 675 676 // Might throw. waitForConnection(String sql, int connectionFlags, CancellationSignal cancellationSignal)677 private SQLiteConnection waitForConnection(String sql, int connectionFlags, 678 CancellationSignal cancellationSignal) { 679 final boolean wantPrimaryConnection = 680 (connectionFlags & CONNECTION_FLAG_PRIMARY_CONNECTION_AFFINITY) != 0; 681 682 final ConnectionWaiter waiter; 683 final int nonce; 684 synchronized (mLock) { 685 throwIfClosedLocked(); 686 687 // Abort if canceled. 688 if (cancellationSignal != null) { 689 cancellationSignal.throwIfCanceled(); 690 } 691 692 // Try to acquire a connection. 693 SQLiteConnection connection = null; 694 if (!wantPrimaryConnection) { 695 connection = tryAcquireNonPrimaryConnectionLocked( 696 sql, connectionFlags); // might throw 697 } 698 if (connection == null) { 699 connection = tryAcquirePrimaryConnectionLocked(connectionFlags); // might throw 700 } 701 if (connection != null) { 702 return connection; 703 } 704 705 // No connections available. Enqueue a waiter in priority order. 706 final int priority = getPriority(connectionFlags); 707 final long startTime = SystemClock.uptimeMillis(); 708 waiter = obtainConnectionWaiterLocked(Thread.currentThread(), startTime, 709 priority, wantPrimaryConnection, sql, connectionFlags); 710 ConnectionWaiter predecessor = null; 711 ConnectionWaiter successor = mConnectionWaiterQueue; 712 while (successor != null) { 713 if (priority > successor.mPriority) { 714 waiter.mNext = successor; 715 break; 716 } 717 predecessor = successor; 718 successor = successor.mNext; 719 } 720 if (predecessor != null) { 721 predecessor.mNext = waiter; 722 } else { 723 mConnectionWaiterQueue = waiter; 724 } 725 726 nonce = waiter.mNonce; 727 } 728 729 // Set up the cancellation listener. 730 if (cancellationSignal != null) { 731 cancellationSignal.setOnCancelListener(new CancellationSignal.OnCancelListener() { 732 @Override 733 public void onCancel() { 734 synchronized (mLock) { 735 if (waiter.mNonce == nonce) { 736 cancelConnectionWaiterLocked(waiter); 737 } 738 } 739 } 740 }); 741 } 742 try { 743 // Park the thread until a connection is assigned or the pool is closed. 744 // Rethrow an exception from the wait, if we got one. 745 long busyTimeoutMillis = CONNECTION_POOL_BUSY_MILLIS; 746 long nextBusyTimeoutTime = waiter.mStartTime + busyTimeoutMillis; 747 for (;;) { 748 // Detect and recover from connection leaks. 749 if (mConnectionLeaked.compareAndSet(true, false)) { 750 synchronized (mLock) { 751 wakeConnectionWaitersLocked(); 752 } 753 } 754 755 // Wait to be unparked (may already have happened), a timeout, or interruption. 756 LockSupport.parkNanos(this, busyTimeoutMillis * 1000000L); 757 758 // Clear the interrupted flag, just in case. 759 Thread.interrupted(); 760 761 // Check whether we are done waiting yet. 762 synchronized (mLock) { 763 throwIfClosedLocked(); 764 765 final SQLiteConnection connection = waiter.mAssignedConnection; 766 final RuntimeException ex = waiter.mException; 767 if (connection != null || ex != null) { 768 recycleConnectionWaiterLocked(waiter); 769 if (connection != null) { 770 return connection; 771 } 772 throw ex; // rethrow! 773 } 774 775 final long now = SystemClock.uptimeMillis(); 776 if (now < nextBusyTimeoutTime) { 777 busyTimeoutMillis = now - nextBusyTimeoutTime; 778 } else { 779 logConnectionPoolBusyLocked(now - waiter.mStartTime, connectionFlags); 780 busyTimeoutMillis = CONNECTION_POOL_BUSY_MILLIS; 781 nextBusyTimeoutTime = now + busyTimeoutMillis; 782 } 783 } 784 } 785 } finally { 786 // Remove the cancellation listener. 787 if (cancellationSignal != null) { 788 cancellationSignal.setOnCancelListener(null); 789 } 790 } 791 } 792 793 // Can't throw. 794 @GuardedBy("mLock") cancelConnectionWaiterLocked(ConnectionWaiter waiter)795 private void cancelConnectionWaiterLocked(ConnectionWaiter waiter) { 796 if (waiter.mAssignedConnection != null || waiter.mException != null) { 797 // Waiter is done waiting but has not woken up yet. 798 return; 799 } 800 801 // Waiter must still be waiting. Dequeue it. 802 ConnectionWaiter predecessor = null; 803 ConnectionWaiter current = mConnectionWaiterQueue; 804 while (current != waiter) { 805 assert current != null; 806 predecessor = current; 807 current = current.mNext; 808 } 809 if (predecessor != null) { 810 predecessor.mNext = waiter.mNext; 811 } else { 812 mConnectionWaiterQueue = waiter.mNext; 813 } 814 815 // Send the waiter an exception and unpark it. 816 waiter.mException = new OperationCanceledException(); 817 LockSupport.unpark(waiter.mThread); 818 819 // Check whether removing this waiter will enable other waiters to make progress. 820 wakeConnectionWaitersLocked(); 821 } 822 823 // Can't throw. logConnectionPoolBusyLocked(long waitMillis, int connectionFlags)824 private void logConnectionPoolBusyLocked(long waitMillis, int connectionFlags) { 825 final Thread thread = Thread.currentThread(); 826 StringBuilder msg = new StringBuilder(); 827 msg.append("The connection pool for database '").append(mConfiguration.label); 828 msg.append("' has been unable to grant a connection to thread "); 829 msg.append(thread.getId()).append(" (").append(thread.getName()).append(") "); 830 msg.append("with flags 0x").append(Integer.toHexString(connectionFlags)); 831 msg.append(" for ").append(waitMillis * 0.001f).append(" seconds.\n"); 832 833 ArrayList<String> requests = new ArrayList<String>(); 834 int activeConnections = 0; 835 int idleConnections = 0; 836 if (!mAcquiredConnections.isEmpty()) { 837 for (SQLiteConnection connection : mAcquiredConnections.keySet()) { 838 String description = connection.describeCurrentOperationUnsafe(); 839 if (description != null) { 840 requests.add(description); 841 activeConnections += 1; 842 } else { 843 idleConnections += 1; 844 } 845 } 846 } 847 int availableConnections = mAvailableNonPrimaryConnections.size(); 848 if (mAvailablePrimaryConnection != null) { 849 availableConnections += 1; 850 } 851 852 msg.append("Connections: ").append(activeConnections).append(" active, "); 853 msg.append(idleConnections).append(" idle, "); 854 msg.append(availableConnections).append(" available.\n"); 855 856 if (!requests.isEmpty()) { 857 msg.append("\nRequests in progress:\n"); 858 for (String request : requests) { 859 msg.append(" ").append(request).append("\n"); 860 } 861 } 862 863 Log.w(TAG, msg.toString()); 864 } 865 866 // Can't throw. 867 @GuardedBy("mLock") wakeConnectionWaitersLocked()868 private void wakeConnectionWaitersLocked() { 869 // Unpark all waiters that have requests that we can fulfill. 870 // This method is designed to not throw runtime exceptions, although we might send 871 // a waiter an exception for it to rethrow. 872 ConnectionWaiter predecessor = null; 873 ConnectionWaiter waiter = mConnectionWaiterQueue; 874 boolean primaryConnectionNotAvailable = false; 875 boolean nonPrimaryConnectionNotAvailable = false; 876 while (waiter != null) { 877 boolean unpark = false; 878 if (!mIsOpen) { 879 unpark = true; 880 } else { 881 try { 882 SQLiteConnection connection = null; 883 if (!waiter.mWantPrimaryConnection && !nonPrimaryConnectionNotAvailable) { 884 connection = tryAcquireNonPrimaryConnectionLocked( 885 waiter.mSql, waiter.mConnectionFlags); // might throw 886 if (connection == null) { 887 nonPrimaryConnectionNotAvailable = true; 888 } 889 } 890 if (connection == null && !primaryConnectionNotAvailable) { 891 connection = tryAcquirePrimaryConnectionLocked( 892 waiter.mConnectionFlags); // might throw 893 if (connection == null) { 894 primaryConnectionNotAvailable = true; 895 } 896 } 897 if (connection != null) { 898 waiter.mAssignedConnection = connection; 899 unpark = true; 900 } else if (nonPrimaryConnectionNotAvailable && primaryConnectionNotAvailable) { 901 // There are no connections available and the pool is still open. 902 // We cannot fulfill any more connection requests, so stop here. 903 break; 904 } 905 } catch (RuntimeException ex) { 906 // Let the waiter handle the exception from acquiring a connection. 907 waiter.mException = ex; 908 unpark = true; 909 } 910 } 911 912 final ConnectionWaiter successor = waiter.mNext; 913 if (unpark) { 914 if (predecessor != null) { 915 predecessor.mNext = successor; 916 } else { 917 mConnectionWaiterQueue = successor; 918 } 919 waiter.mNext = null; 920 921 LockSupport.unpark(waiter.mThread); 922 } else { 923 predecessor = waiter; 924 } 925 waiter = successor; 926 } 927 } 928 929 // Might throw. 930 @GuardedBy("mLock") tryAcquirePrimaryConnectionLocked(int connectionFlags)931 private SQLiteConnection tryAcquirePrimaryConnectionLocked(int connectionFlags) { 932 // If the primary connection is available, acquire it now. 933 SQLiteConnection connection = mAvailablePrimaryConnection; 934 if (connection != null) { 935 mAvailablePrimaryConnection = null; 936 finishAcquireConnectionLocked(connection, connectionFlags); // might throw 937 return connection; 938 } 939 940 // Make sure that the primary connection actually exists and has just been acquired. 941 for (SQLiteConnection acquiredConnection : mAcquiredConnections.keySet()) { 942 if (acquiredConnection.isPrimaryConnection()) { 943 return null; 944 } 945 } 946 947 // Uhoh. No primary connection! Either this is the first time we asked 948 // for it, or maybe it leaked? 949 connection = openConnectionLocked(mConfiguration, 950 true /*primaryConnection*/); // might throw 951 finishAcquireConnectionLocked(connection, connectionFlags); // might throw 952 return connection; 953 } 954 955 // Might throw. 956 @GuardedBy("mLock") tryAcquireNonPrimaryConnectionLocked( String sql, int connectionFlags)957 private SQLiteConnection tryAcquireNonPrimaryConnectionLocked( 958 String sql, int connectionFlags) { 959 // Try to acquire the next connection in the queue. 960 SQLiteConnection connection; 961 final int availableCount = mAvailableNonPrimaryConnections.size(); 962 if (availableCount > 1 && sql != null) { 963 // If we have a choice, then prefer a connection that has the 964 // prepared statement in its cache. 965 for (int i = 0; i < availableCount; i++) { 966 connection = mAvailableNonPrimaryConnections.get(i); 967 if (connection.isPreparedStatementInCache(sql)) { 968 mAvailableNonPrimaryConnections.remove(i); 969 finishAcquireConnectionLocked(connection, connectionFlags); // might throw 970 return connection; 971 } 972 } 973 } 974 if (availableCount > 0) { 975 // Otherwise, just grab the next one. 976 connection = mAvailableNonPrimaryConnections.remove(availableCount - 1); 977 finishAcquireConnectionLocked(connection, connectionFlags); // might throw 978 return connection; 979 } 980 981 // Expand the pool if needed. 982 int openConnections = mAcquiredConnections.size(); 983 if (mAvailablePrimaryConnection != null) { 984 openConnections += 1; 985 } 986 if (openConnections >= mMaxConnectionPoolSize) { 987 return null; 988 } 989 connection = openConnectionLocked(mConfiguration, 990 false /*primaryConnection*/); // might throw 991 finishAcquireConnectionLocked(connection, connectionFlags); // might throw 992 return connection; 993 } 994 995 // Might throw. 996 @GuardedBy("mLock") finishAcquireConnectionLocked(SQLiteConnection connection, int connectionFlags)997 private void finishAcquireConnectionLocked(SQLiteConnection connection, int connectionFlags) { 998 try { 999 final boolean readOnly = (connectionFlags & CONNECTION_FLAG_READ_ONLY) != 0; 1000 connection.setOnlyAllowReadOnlyOperations(readOnly); 1001 1002 mAcquiredConnections.put(connection, AcquiredConnectionStatus.NORMAL); 1003 } catch (RuntimeException ex) { 1004 Log.e(TAG, "Failed to prepare acquired connection for session, closing it: " 1005 + connection +", connectionFlags=" + connectionFlags); 1006 closeConnectionAndLogExceptionsLocked(connection); 1007 throw ex; // rethrow! 1008 } 1009 } 1010 isSessionBlockingImportantConnectionWaitersLocked( boolean holdingPrimaryConnection, int connectionFlags)1011 private boolean isSessionBlockingImportantConnectionWaitersLocked( 1012 boolean holdingPrimaryConnection, int connectionFlags) { 1013 ConnectionWaiter waiter = mConnectionWaiterQueue; 1014 if (waiter != null) { 1015 final int priority = getPriority(connectionFlags); 1016 do { 1017 // Only worry about blocked connections that have same or lower priority. 1018 if (priority > waiter.mPriority) { 1019 break; 1020 } 1021 1022 // If we are holding the primary connection then we are blocking the waiter. 1023 // Likewise, if we are holding a non-primary connection and the waiter 1024 // would accept a non-primary connection, then we are blocking the waier. 1025 if (holdingPrimaryConnection || !waiter.mWantPrimaryConnection) { 1026 return true; 1027 } 1028 1029 waiter = waiter.mNext; 1030 } while (waiter != null); 1031 } 1032 return false; 1033 } 1034 getPriority(int connectionFlags)1035 private static int getPriority(int connectionFlags) { 1036 return (connectionFlags & CONNECTION_FLAG_INTERACTIVE) != 0 ? 1 : 0; 1037 } 1038 setMaxConnectionPoolSizeLocked()1039 private void setMaxConnectionPoolSizeLocked() { 1040 if (!mConfiguration.isInMemoryDb() 1041 && (mConfiguration.openFlags & SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING) != 0) { 1042 mMaxConnectionPoolSize = SQLiteGlobal.getWALConnectionPoolSize(); 1043 } else { 1044 // We don't actually need to always restrict the connection pool size to 1 1045 // for non-WAL databases. There might be reasons to use connection pooling 1046 // with other journal modes. However, we should always keep pool size of 1 for in-memory 1047 // databases since every :memory: db is separate from another. 1048 // For now, enabling connection pooling and using WAL are the same thing in the API. 1049 mMaxConnectionPoolSize = 1; 1050 } 1051 } 1052 1053 /** 1054 * Set up the handler based on the provided looper and timeout. 1055 */ 1056 @VisibleForTesting setupIdleConnectionHandler(Looper looper, long timeoutMs)1057 public void setupIdleConnectionHandler(Looper looper, long timeoutMs) { 1058 synchronized (mLock) { 1059 mIdleConnectionHandler = new IdleConnectionHandler(looper, timeoutMs); 1060 } 1061 } 1062 disableIdleConnectionHandler()1063 void disableIdleConnectionHandler() { 1064 synchronized (mLock) { 1065 mIdleConnectionHandler = null; 1066 } 1067 } 1068 throwIfClosedLocked()1069 private void throwIfClosedLocked() { 1070 if (!mIsOpen) { 1071 throw new IllegalStateException("Cannot perform this operation " 1072 + "because the connection pool has been closed."); 1073 } 1074 } 1075 obtainConnectionWaiterLocked(Thread thread, long startTime, int priority, boolean wantPrimaryConnection, String sql, int connectionFlags)1076 private ConnectionWaiter obtainConnectionWaiterLocked(Thread thread, long startTime, 1077 int priority, boolean wantPrimaryConnection, String sql, int connectionFlags) { 1078 ConnectionWaiter waiter = mConnectionWaiterPool; 1079 if (waiter != null) { 1080 mConnectionWaiterPool = waiter.mNext; 1081 waiter.mNext = null; 1082 } else { 1083 waiter = new ConnectionWaiter(); 1084 } 1085 waiter.mThread = thread; 1086 waiter.mStartTime = startTime; 1087 waiter.mPriority = priority; 1088 waiter.mWantPrimaryConnection = wantPrimaryConnection; 1089 waiter.mSql = sql; 1090 waiter.mConnectionFlags = connectionFlags; 1091 return waiter; 1092 } 1093 recycleConnectionWaiterLocked(ConnectionWaiter waiter)1094 private void recycleConnectionWaiterLocked(ConnectionWaiter waiter) { 1095 waiter.mNext = mConnectionWaiterPool; 1096 waiter.mThread = null; 1097 waiter.mSql = null; 1098 waiter.mAssignedConnection = null; 1099 waiter.mException = null; 1100 waiter.mNonce += 1; 1101 mConnectionWaiterPool = waiter; 1102 } 1103 1104 /** 1105 * Dumps debugging information about this connection pool. 1106 * 1107 * @param printer The printer to receive the dump, not null. 1108 * @param verbose True to dump more verbose information. 1109 */ dump(Printer printer, boolean verbose, ArraySet<String> directories)1110 public void dump(Printer printer, boolean verbose, ArraySet<String> directories) { 1111 Printer indentedPrinter = PrefixPrinter.create(printer, " "); 1112 synchronized (mLock) { 1113 if (directories != null) { 1114 directories.add(new File(mConfiguration.path).getParent()); 1115 } 1116 boolean isCompatibilityWalEnabled = mConfiguration.isLegacyCompatibilityWalEnabled(); 1117 printer.println("Connection pool for " + mConfiguration.path + ":"); 1118 printer.println(" Open: " + mIsOpen); 1119 printer.println(" Max connections: " + mMaxConnectionPoolSize); 1120 printer.println(" Total execution time: " + mTotalExecutionTimeCounter); 1121 printer.println(" Configuration: openFlags=" + mConfiguration.openFlags 1122 + ", isLegacyCompatibilityWalEnabled=" + isCompatibilityWalEnabled 1123 + ", journalMode=" + TextUtils.emptyIfNull(mConfiguration.journalMode) 1124 + ", syncMode=" + TextUtils.emptyIfNull(mConfiguration.syncMode)); 1125 1126 if (isCompatibilityWalEnabled) { 1127 printer.println(" Compatibility WAL enabled: wal_syncmode=" 1128 + SQLiteCompatibilityWalFlags.getWALSyncMode()); 1129 } 1130 if (mConfiguration.isLookasideConfigSet()) { 1131 printer.println(" Lookaside config: sz=" + mConfiguration.lookasideSlotSize 1132 + " cnt=" + mConfiguration.lookasideSlotCount); 1133 } 1134 if (mConfiguration.idleConnectionTimeoutMs != Long.MAX_VALUE) { 1135 printer.println( 1136 " Idle connection timeout: " + mConfiguration.idleConnectionTimeoutMs); 1137 } 1138 printer.println(" Available primary connection:"); 1139 if (mAvailablePrimaryConnection != null) { 1140 mAvailablePrimaryConnection.dump(indentedPrinter, verbose); 1141 } else { 1142 indentedPrinter.println("<none>"); 1143 } 1144 1145 printer.println(" Available non-primary connections:"); 1146 if (!mAvailableNonPrimaryConnections.isEmpty()) { 1147 final int count = mAvailableNonPrimaryConnections.size(); 1148 for (int i = 0; i < count; i++) { 1149 mAvailableNonPrimaryConnections.get(i).dump(indentedPrinter, verbose); 1150 } 1151 } else { 1152 indentedPrinter.println("<none>"); 1153 } 1154 1155 printer.println(" Acquired connections:"); 1156 if (!mAcquiredConnections.isEmpty()) { 1157 for (Map.Entry<SQLiteConnection, AcquiredConnectionStatus> entry : 1158 mAcquiredConnections.entrySet()) { 1159 final SQLiteConnection connection = entry.getKey(); 1160 connection.dumpUnsafe(indentedPrinter, verbose); 1161 indentedPrinter.println(" Status: " + entry.getValue()); 1162 } 1163 } else { 1164 indentedPrinter.println("<none>"); 1165 } 1166 1167 printer.println(" Connection waiters:"); 1168 if (mConnectionWaiterQueue != null) { 1169 int i = 0; 1170 final long now = SystemClock.uptimeMillis(); 1171 for (ConnectionWaiter waiter = mConnectionWaiterQueue; waiter != null; 1172 waiter = waiter.mNext, i++) { 1173 indentedPrinter.println(i + ": waited for " 1174 + ((now - waiter.mStartTime) * 0.001f) 1175 + " ms - thread=" + waiter.mThread 1176 + ", priority=" + waiter.mPriority 1177 + ", sql='" + waiter.mSql + "'"); 1178 } 1179 } else { 1180 indentedPrinter.println("<none>"); 1181 } 1182 } 1183 } 1184 1185 @Override toString()1186 public String toString() { 1187 return "SQLiteConnectionPool: " + mConfiguration.path; 1188 } 1189 getPath()1190 public String getPath() { 1191 return mConfiguration.path; 1192 } 1193 1194 private static final class ConnectionWaiter { 1195 public ConnectionWaiter mNext; 1196 public Thread mThread; 1197 public long mStartTime; 1198 public int mPriority; 1199 public boolean mWantPrimaryConnection; 1200 public String mSql; 1201 public int mConnectionFlags; 1202 public SQLiteConnection mAssignedConnection; 1203 public RuntimeException mException; 1204 public int mNonce; 1205 } 1206 1207 private class IdleConnectionHandler extends Handler { 1208 private final long mTimeout; 1209 IdleConnectionHandler(Looper looper, long timeout)1210 IdleConnectionHandler(Looper looper, long timeout) { 1211 super(looper); 1212 mTimeout = timeout; 1213 } 1214 1215 @Override handleMessage(Message msg)1216 public void handleMessage(Message msg) { 1217 // Skip the (obsolete) message if the handler has changed 1218 synchronized (mLock) { 1219 if (this != mIdleConnectionHandler) { 1220 return; 1221 } 1222 if (closeAvailableConnectionLocked(msg.what)) { 1223 if (Log.isLoggable(TAG, Log.DEBUG)) { 1224 Log.d(TAG, "Closed idle connection " + mConfiguration.label + " " + msg.what 1225 + " after " + mTimeout); 1226 } 1227 } 1228 } 1229 } 1230 connectionReleased(SQLiteConnection con)1231 void connectionReleased(SQLiteConnection con) { 1232 sendEmptyMessageDelayed(con.getConnectionId(), mTimeout); 1233 } 1234 connectionAcquired(SQLiteConnection con)1235 void connectionAcquired(SQLiteConnection con) { 1236 // Remove any pending close operations 1237 removeMessages(con.getConnectionId()); 1238 } 1239 connectionClosed(SQLiteConnection con)1240 void connectionClosed(SQLiteConnection con) { 1241 removeMessages(con.getConnectionId()); 1242 } 1243 } 1244 } 1245