|
54 | 54 | import java.net.URL; |
55 | 55 | import java.util.ArrayList; |
56 | 56 | import java.util.Collections; |
| 57 | +import java.util.Date; |
57 | 58 | import java.util.HashMap; |
58 | 59 | import java.util.HashSet; |
59 | 60 | import java.util.List; |
60 | 61 | import java.util.Locale; |
61 | 62 | import java.util.Map; |
62 | 63 | import java.util.Set; |
| 64 | +import java.util.Timer; |
| 65 | +import java.util.TimerTask; |
63 | 66 | import java.util.concurrent.CopyOnWriteArraySet; |
64 | 67 | import java.util.concurrent.Future; |
65 | 68 | import java.util.concurrent.atomic.AtomicBoolean; |
|
70 | 73 | public class Database implements StoreDelegate { |
71 | 74 |
|
72 | 75 | public static final String TAG = Log.TAG_DATABASE; |
| 76 | + |
73 | 77 | // When this many changes pile up in _changesToNotify, start removing their bodies to save RAM |
74 | 78 | private static final int MANY_CHANGES_TO_NOTIFY = 5000; |
75 | 79 |
|
| 80 | + // How long to wait after a database opens before expiring docs |
| 81 | + private static final long kHousekeepingDelayAfterOpening = 3; |
| 82 | + |
76 | 83 | private static final String DEFAULT_PBKDF2_KEY_SALT = "Salty McNaCl"; |
77 | 84 | private static final int DEFAULT_PBKDF2_KEY_ROUNDS = 64000; |
78 | 85 |
|
@@ -120,6 +127,7 @@ public class Database implements StoreDelegate { |
120 | 127 | private boolean postingChangeNotifications; |
121 | 128 | private final Object lockPostingChangeNotifications = new Object(); |
122 | 129 | private final long startTime; |
| 130 | + private Timer purgeTimer; |
123 | 131 |
|
124 | 132 | /** |
125 | 133 | * Each database can have an associated PersistentCookieStore, |
@@ -715,8 +723,11 @@ public void storageExitedTransaction(boolean committed) { |
715 | 723 | */ |
716 | 724 | @InterfaceAudience.Private |
717 | 725 | public void databaseStorageChanged(DocumentChange change) { |
718 | | - Log.v(Log.TAG, "---> Added: %s as seq %d", |
| 726 | + if (change.getRevisionId() != null) |
| 727 | + Log.v(Log.TAG, "---> Added: %s as seq %d", |
719 | 728 | change.getAddedRevision(), change.getAddedRevision().getSequence()); |
| 729 | + else |
| 730 | + Log.v(Log.TAG, "---> Purged: docID=%s", change.getDocumentId()); |
720 | 731 |
|
721 | 732 | changesToNotify.add(change); |
722 | 733 | if (!postChangeNotifications()) { |
@@ -1308,6 +1319,8 @@ public synchronized void open(DatabaseOptions options) throws CouchbaseLiteExcep |
1308 | 1319 | upgrader.deleteSQLiteFiles(); |
1309 | 1320 | } |
1310 | 1321 | } |
| 1322 | + |
| 1323 | + scheduleDocumentExpiration(kHousekeepingDelayAfterOpening); |
1311 | 1324 | } |
1312 | 1325 |
|
1313 | 1326 | /** |
@@ -1391,6 +1404,9 @@ public boolean close() { |
1391 | 1404 | // Clear all replicators: |
1392 | 1405 | allReplicators.clear(); |
1393 | 1406 |
|
| 1407 | + // cancel purge timer |
| 1408 | + cancelPurgeTimer(); |
| 1409 | + |
1394 | 1410 | // Close Store: |
1395 | 1411 | if (store != null) |
1396 | 1412 | store.close(); |
@@ -2250,6 +2266,48 @@ private void installAttachment(AttachmentInternal attachment) throws CouchbaseLi |
2250 | 2266 | } |
2251 | 2267 | } |
2252 | 2268 |
|
| 2269 | + // #pragma mark - EXPIRATION: |
| 2270 | + |
| 2271 | + /* package */void setExpirationDate(Date date, String docID) { |
| 2272 | + long unixTime = date != null ? date.getTime() / 1000 : 0; |
| 2273 | + store.setExpirationOfDocument(unixTime, docID); |
| 2274 | + scheduleDocumentExpiration(0); |
| 2275 | + } |
| 2276 | + |
| 2277 | + private void scheduleDocumentExpiration(long minimumDelay) { |
| 2278 | + if (store == null) return; |
| 2279 | + |
| 2280 | + long nextExpiration = store.nextDocumentExpiry(); |
| 2281 | + if (nextExpiration > 0) { |
| 2282 | + long delay = Math.max((nextExpiration - System.currentTimeMillis()) / 1000 + 1, minimumDelay); |
| 2283 | + Log.v(TAG, "Scheduling next doc expiration in %d sec", delay); |
| 2284 | + cancelPurgeTimer(); |
| 2285 | + purgeTimer = new Timer(); |
| 2286 | + purgeTimer.schedule(new TimerTask() { |
| 2287 | + @Override |
| 2288 | + public void run() { |
| 2289 | + if (isOpen()) |
| 2290 | + purgeExpiredDocuments(); |
| 2291 | + } |
| 2292 | + }, delay * 1000); |
| 2293 | + } else |
| 2294 | + Log.v(TAG, "No pending doc expirations"); |
| 2295 | + } |
| 2296 | + |
| 2297 | + private void purgeExpiredDocuments() { |
| 2298 | + if (store == null) return; |
| 2299 | + int nPurged = store.purgeExpiredDocuments(); |
| 2300 | + Log.v(TAG, "Purged %d expired documents", nPurged); |
| 2301 | + scheduleDocumentExpiration(1); |
| 2302 | + } |
| 2303 | + |
| 2304 | + private void cancelPurgeTimer() { |
| 2305 | + if (purgeTimer != null) { |
| 2306 | + purgeTimer.cancel(); |
| 2307 | + purgeTimer = null; |
| 2308 | + } |
| 2309 | + } |
| 2310 | + |
2253 | 2311 | // #pragma mark - LOOKING UP ATTACHMENTS: |
2254 | 2312 |
|
2255 | 2313 | /** |
@@ -2302,7 +2360,7 @@ private boolean postChangeNotifications() { |
2302 | 2360 | // This is a 'while' instead of an 'if' because when we finish posting notifications, there |
2303 | 2361 | // might be new ones that have arrived as a result of notification handlers making document |
2304 | 2362 | // changes of their own (the replicator manager will do this.) So we need to check again. |
2305 | | - while (!store.inTransaction() && open.get() && changesToNotify.size() > 0) { |
| 2363 | + while (store != null && !store.inTransaction() && open.get() && changesToNotify.size() > 0) { |
2306 | 2364 | List<DocumentChange> outgoingChanges = new ArrayList<DocumentChange>(); |
2307 | 2365 | synchronized (changesToNotify) { |
2308 | 2366 | outgoingChanges.addAll(changesToNotify); |
|
0 commit comments