diff --git a/src/Couchbase.Lite.Shared/API/Database/Database.cs b/src/Couchbase.Lite.Shared/API/Database/Database.cs index 75e50687b5..65b693d01e 100644 --- a/src/Couchbase.Lite.Shared/API/Database/Database.cs +++ b/src/Couchbase.Lite.Shared/API/Database/Database.cs @@ -23,6 +23,7 @@ using System.Linq; using System.Runtime.ExceptionServices; using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; using Couchbase.Lite.DI; @@ -94,6 +95,8 @@ public sealed unsafe class Database : IDisposable #region Variables + private static readonly TimeSpan HousekeepingDelayAfterOpen = TimeSpan.FromSeconds(3); + [NotNull] private readonly Dictionary> _docObs = new Dictionary>(); @@ -111,10 +114,11 @@ public sealed unsafe class Database : IDisposable [NotNull] private readonly TaskFactory _callbackFactory = new TaskFactory(new QueueTaskScheduler()); - #if false +#if false private IJsonSerializer _jsonSerializer; - #endif +#endif + private Timer _expirePurgeTimer; private C4DatabaseObserver* _obs; private GCHandle _obsContext; private C4Database* _c4db; @@ -745,8 +749,13 @@ public void Purge(Document document) public void Purge(string docId) { CBDebug.MustNotBeNull(Log.To.Database, Tag, nameof(docId), docId); - var document = GetDocument(docId); - Purge(document); + try { + LiteCoreBridge.Check(err => Native.c4db_beginTransaction(_c4db, err)); + PurgeDocById(docId); + LiteCoreBridge.Check(e => Native.c4db_endTransaction(_c4db, true, e)); + } catch (Exception) { + LiteCoreBridge.Check(e => Native.c4db_endTransaction(_c4db, false, e)); + } } /// @@ -762,9 +771,6 @@ public void Purge(string docId) /// doesn't exist public bool SetDocumentExpiration(string docId, DateTimeOffset? timestamp) { - if(GetDocument(docId) == null) { - throw new CouchbaseLiteException(C4ErrorCode.NotFound, "Cannot find the document."); - } var succeed = false; ThreadSafety.DoLockedBridge(err => { @@ -774,6 +780,7 @@ public bool SetDocumentExpiration(string docId, DateTimeOffset? timestamp) var Timestamp = timestamp?.ToUnixTimeMilliseconds(); succeed = Native.c4doc_setExpiration(_c4db, docId, (ulong)Timestamp, err); } + SchedulePurgeExpired(TimeSpan.Zero); return succeed; }); return succeed; @@ -951,6 +958,29 @@ internal void ResolveConflict([NotNull]string docID) } }); } + + internal void SchedulePurgeExpired(TimeSpan delay) + { + var nextExpiration = Native.c4db_nextDocExpiration(_c4db); + if (nextExpiration > 0) { + var delta = (DateTimeOffset.FromUnixTimeMilliseconds((long)nextExpiration) - DateTimeOffset.UtcNow).Add(TimeSpan.FromSeconds(1)); + var expirationTimeSpan = delta > delay ? delta : delay; + if (expirationTimeSpan.TotalMilliseconds >= UInt32.MaxValue) { + _expirePurgeTimer.Change(TimeSpan.FromMilliseconds(UInt32.MaxValue - 1), TimeSpan.FromMilliseconds(-1)); + Log.To.Database.I(Tag, "{0:F3} seconds is too far in the future to schedule a document expiration," + + " will run again at the maximum value of {0:F3} seconds", expirationTimeSpan.TotalSeconds, (UInt32.MaxValue - 1) / 1000); + } else if (expirationTimeSpan.TotalSeconds <= Double.Epsilon) { + _expirePurgeTimer.Change(Timeout.Infinite, Timeout.Infinite); + PurgeExpired(null); + } else { + _expirePurgeTimer.Change(expirationTimeSpan, TimeSpan.FromMilliseconds(-1)); + Log.To.Database.I(Tag, "Scheduling next doc expiration in {0:F3} seconds", expirationTimeSpan.TotalSeconds); + } + } else { + Log.To.Database.I(Tag, "No pending doc expirations"); + } + } + #endregion #region Private Methods @@ -1089,6 +1119,8 @@ private void Open() return Native.c4db_open(path, &localConfig2, err); }); }); + + _expirePurgeTimer = new Timer(PurgeExpired, null, HousekeepingDelayAfterOpen, TimeSpan.FromMilliseconds(-1)); } private void PostDatabaseChanged() @@ -1365,6 +1397,29 @@ private void VerifyDB([NotNull]Document document) } } + private void PurgeDocById(string id) + { + ThreadSafety.DoLockedBridge(err => + { + return Native.c4db_purgeDoc(_c4db, id, err); + }); + } + + private void PurgeExpired(object state) + { + var cnt = 0L; + ThreadSafety.DoLockedBridge(err => + { + CheckOpen(); + cnt = Native.c4db_purgeExpiredDocs(_c4db, err); + Log.To.Database.I(Tag, "{0} purged {1} expired documents", this, cnt); + SchedulePurgeExpired(TimeSpan.FromSeconds(1)); + if (err->code>0 && err->domain>0) + return false; + return true; + }); + } + #endregion /// diff --git a/src/Couchbase.Lite.Tests.Shared/DocumentTest.cs b/src/Couchbase.Lite.Tests.Shared/DocumentTest.cs index 4a1fb57060..f592ebf5d4 100644 --- a/src/Couchbase.Lite.Tests.Shared/DocumentTest.cs +++ b/src/Couchbase.Lite.Tests.Shared/DocumentTest.cs @@ -1808,21 +1808,21 @@ public void TestDeleteDocAndGetDoc() [Fact] public void TestSetAndGetExpirationFromDoc() { - var dto30 = DateTimeOffset.Now.AddSeconds(30); - var dto0 = DateTimeOffset.Now; + var dto30 = DateTimeOffset.UtcNow.AddSeconds(30); + var dto0 = DateTimeOffset.UtcNow; using (var doc1a = new MutableDocument("doc1")) using (var doc1b = new MutableDocument("doc2")) using (var doc1c = new MutableDocument("doc3")) { - doc1a.SetInt("answer", 42); + doc1a.SetInt("answer", 12); doc1a.SetValue("options", new[] { 1, 2, 3 }); Db.Save(doc1a); - doc1b.SetInt("answer", 42); + doc1b.SetInt("answer", 22); doc1b.SetValue("options", new[] { 1, 2, 3 }); Db.Save(doc1b); - doc1c.SetInt("answer", 42); + doc1c.SetInt("answer", 32); doc1c.SetValue("options", new[] { 1, 2, 3 }); Db.Save(doc1c); @@ -1836,6 +1836,22 @@ public void TestSetAndGetExpirationFromDoc() Db.GetDocumentExpiration("doc3").Should().Be(null); } + [Fact] + public void TestSetExpirationFromDoc() + { + var dto3 = DateTimeOffset.UtcNow.AddSeconds(3); + using (var doc1a = new MutableDocument("doc1")) { + doc1a.SetInt("answer", 12); + doc1a.SetValue("options", new[] { 1, 2, 3 }); + Db.Save(doc1a); + + Db.SetDocumentExpiration("doc1", dto3).Should().Be(true); + + } + Thread.Sleep(4000); + var doc = Db.GetDocument("doc1").Should().BeNull(); + } + [Fact] public void TestSetExpirationOnNoneExistDoc() { @@ -1852,6 +1868,24 @@ public void TestGetExpirationFromNoneExistDoc() badAction.ShouldThrow("Cannot find the document."); } + [Fact] + public void TestLongExpiration() + { + var now = DateTime.UtcNow; + using (var doc = new MutableDocument("doc")) { + doc.SetInt("answer", 42); + doc.SetValue("options", new[] { 1, 2, 3 }); + Db.Save(doc); + + Db.GetDocumentExpiration("doc").Should().BeNull(); + Db.SetDocumentExpiration("doc", new DateTimeOffset(DateTime.UtcNow.AddDays(60))); + + var exp = Db.GetDocumentExpiration("doc"); + exp.Should().NotBeNull(); + (Math.Abs((exp.Value - now).TotalDays - 60.0) < 1.0).Should().BeTrue(); + } + } + private void PopulateData(MutableDocument doc) { var date = DateTimeOffset.Now;