Skip to content

Commit

Permalink
Merge pull request #1084 from couchbase/feature/ttl_update
Browse files Browse the repository at this point in the history
#1082 #1083 purge documents when docs expiration is set and reached, allow deleted doc to be purged. close #1082 close #1083
  • Loading branch information
Sandychuang8 authored Dec 7, 2018
2 parents ff55145 + ed8d0bf commit 8bffcd2
Show file tree
Hide file tree
Showing 4 changed files with 256 additions and 32 deletions.
65 changes: 56 additions & 9 deletions src/Couchbase.Lite.Shared/API/Database/Database.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -94,6 +95,8 @@ public sealed unsafe class Database : IDisposable

#region Variables

private static readonly TimeSpan HousekeepingDelayAfterOpen = TimeSpan.FromSeconds(3);

[NotNull]
private readonly Dictionary<string, Tuple<IntPtr, GCHandle>> _docObs = new Dictionary<string, Tuple<IntPtr, GCHandle>>();

Expand All @@ -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;
Expand Down Expand Up @@ -745,8 +749,7 @@ 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);
InBatch(() => PurgeDocById(docId));
}

/// <summary>
Expand All @@ -762,9 +765,6 @@ public void Purge(string docId)
/// doesn't exist</exception>
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 =>
{
Expand All @@ -774,6 +774,7 @@ public bool SetDocumentExpiration(string docId, DateTimeOffset? timestamp)
var millisSinceEpoch = timestamp.Value.ToUnixTimeMilliseconds();
succeed = Native.c4doc_setExpiration(_c4db, docId, millisSinceEpoch, err);
}
SchedulePurgeExpired(TimeSpan.Zero);
return succeed;
});
return succeed;
Expand All @@ -790,8 +791,8 @@ public bool SetDocumentExpiration(string docId, DateTimeOffset? timestamp)
/// doesn't exist</exception>
public DateTimeOffset? GetDocumentExpiration(string docId)
{
if (GetDocument(docId) == null) {
throw new CouchbaseLiteException(C4ErrorCode.NotFound, "Cannot find the document.");
if (LiteCoreBridge.Check(err => Native.c4doc_get(_c4db, docId, true, err)) == null) {
throw new CouchbaseLiteException(C4ErrorCode.NotFound);
}
var res = (long)Native.c4doc_getExpiration(_c4db, docId);
if (res == 0) {
Expand Down Expand Up @@ -951,6 +952,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;
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.TotalMilliseconds <= 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
Expand Down Expand Up @@ -1089,6 +1113,8 @@ private void Open()
return Native.c4db_open(path, &localConfig2, err);
});
});

_expirePurgeTimer = new Timer(PurgeExpired, null, HousekeepingDelayAfterOpen, TimeSpan.FromMilliseconds(-1));
}

private void PostDatabaseChanged()
Expand Down Expand Up @@ -1365,6 +1391,27 @@ 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;
LiteCoreBridge.Check(err =>
{
CheckOpen();
cnt = Native.c4db_purgeExpiredDocs(_c4db, err);
Log.To.Database.I(Tag, "{0} purged {1} expired documents", this, cnt);
return err->code == 0;
});
SchedulePurgeExpired(TimeSpan.FromSeconds(1));
}

#endregion

/// <inheritdoc />
Expand Down
6 changes: 0 additions & 6 deletions src/Couchbase.Lite.Shared/API/Document/Document.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,12 +122,6 @@ internal C4DocumentWrapper c4Doc
/// </summary>
public ulong Sequence => ThreadSafety.DoLocked(() => c4Doc?.HasValue == true ? c4Doc.RawDoc->selectedRev.sequence : 0UL);

/// <summary>
/// Gets the expiration time of the document. <c>null</c> will be returned
/// if there is no expiration time set
/// </summary>
public DateTimeOffset? Expiration => Database?.GetDocumentExpiration(Id);

[NotNull]
internal ThreadSafety ThreadSafety { get; } = new ThreadSafety();

Expand Down
150 changes: 145 additions & 5 deletions src/Couchbase.Lite.Tests.Shared/DocumentTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1443,6 +1443,26 @@ public void TestPurgeDocument()
Db.Purge(doc);
}

[Fact]
public void TestPurgeDocumentById()
{
var doc = new MutableDocument("doc1");
doc.SetString("type", "profile");
doc.SetString("name", "Scott");
doc.IsDeleted.Should().BeFalse("beacuse the document is not deleted");

Db.Invoking(db => db.Purge("doc1")).ShouldThrow<CouchbaseLiteException>().Where(e =>
e.Error == CouchbaseLiteError.NotFound && e.Domain == CouchbaseLiteErrorType.CouchbaseLite);

// Save:
SaveDocument(doc);

// Purge should not throw:
Db.Purge("doc1");

Db.GetDocument("doc1").Should().BeNull();
}

[Fact]
public void TestReopenDB()
{
Expand Down Expand Up @@ -1808,21 +1828,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);

Expand All @@ -1836,6 +1856,55 @@ public void TestSetAndGetExpirationFromDoc()
Db.GetDocumentExpiration("doc3").Should().Be(null);
}

[Fact]
public void TestSetExpirationOnDoc()
{
var dto3 = DateTimeOffset.UtcNow.AddSeconds(3);
using (var doc1a = new MutableDocument("doc_to_expired")) {
doc1a.SetInt("answer", 12);
doc1a.SetValue("options", new[] { 1, 2, 3 });
Db.Save(doc1a);

Db.SetDocumentExpiration("doc_to_expired", dto3).Should().Be(true);

}
Thread.Sleep(5000);
var doc = Db.GetDocument("doc_to_expired").Should().BeNull();
}

[Fact]
public void TestSetExpirationOnDeletedDoc()
{
var dto3 = DateTimeOffset.Now.AddSeconds(3);
using (var doc1a = new MutableDocument("deleted_doc1")) {
doc1a.SetInt("answer", 12);
doc1a.SetValue("options", new[] { 1, 2, 3 });
Db.Save(doc1a);
Db.Delete(doc1a);

Db.SetDocumentExpiration("deleted_doc1", dto3).Should().BeTrue();
Thread.Sleep(4000);

Action badAction = (() => Db.SetDocumentExpiration("deleted_doc1", dto3));
badAction.ShouldThrow<CouchbaseLiteException>("Cannot find the document.");
}
}

[Fact]
public void TestGetExpirationFromDeletedDoc()
{
DateTimeOffset dto3 = DateTimeOffset.UtcNow.AddSeconds(3);
using (var doc1a = new MutableDocument("deleted_doc")) {
doc1a.SetInt("answer", 12);
doc1a.SetValue("options", new[] { 1, 2, 3 });
Db.Save(doc1a);
Db.SetDocumentExpiration("deleted_doc", dto3).Should().Be(true);
Db.Delete(doc1a);
}
var exp = Db.GetDocumentExpiration("deleted_doc");
exp.Should().BeSameDateAs(dto3);
}

[Fact]
public void TestSetExpirationOnNoneExistDoc()
{
Expand All @@ -1852,6 +1921,77 @@ public void TestGetExpirationFromNoneExistDoc()
badAction.ShouldThrow<CouchbaseLiteException>("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", DateTimeOffset.UtcNow.AddDays(60));

var exp = Db.GetDocumentExpiration("doc");
exp.Should().NotBeNull();
(Math.Abs((exp.Value - now).TotalDays - 60.0) < 1.0).Should().BeTrue();
}
}

[Fact]
public void TestSetAndUnsetExpirationOnDoc()
{
var dto3 = DateTimeOffset.UtcNow.AddSeconds(3);
using (var doc1a = new MutableDocument("doc_to_expired")) {
doc1a.SetInt("answer", 12);
doc1a.SetValue("options", new[] { 1, 2, 3 });
Db.Save(doc1a);

Db.SetDocumentExpiration("doc_to_expired", dto3).Should().Be(true);

}
Db.SetDocumentExpiration("doc_to_expired", null).Should().Be(true);

Thread.Sleep(5000);
var doc = Db.GetDocument("doc_to_expired").Should().NotBeNull();
}

[Fact]
public void TestDocumentExpirationAfterDocsExpired()
{
var dto2 = DateTimeOffset.Now.AddSeconds(2);
var dto3 = DateTimeOffset.Now.AddSeconds(3);
var dto4 = DateTimeOffset.Now.AddSeconds(4);
var dto60InMS = DateTimeOffset.Now.AddSeconds(60).ToUnixTimeMilliseconds();

using (var doc1a = new MutableDocument("doc1"))
using (var doc1b = new MutableDocument("doc2"))
using (var doc1c = new MutableDocument("doc3")) {
doc1a.SetInt("answer", 42);
doc1a.SetString("a", "string");
Db.Save(doc1a);

doc1b.SetInt("answer", 42);
doc1b.SetString("b", "string");
Db.Save(doc1b);

doc1c.SetInt("answer", 42);
doc1c.SetString("c", "string");
Db.Save(doc1c);

Db.SetDocumentExpiration("doc1", dto2).Should().Be(true);
Db.SetDocumentExpiration("doc2", dto3).Should().Be(true);
Db.SetDocumentExpiration("doc3", dto4).Should().Be(true);
}

Thread.Sleep(6000);

Db.GetDocument("doc1").Should().BeNull();
Db.GetDocument("doc2").Should().BeNull();
Db.GetDocument("doc3").Should().BeNull();
}

private void PopulateData(MutableDocument doc)
{
var date = DateTimeOffset.Now;
Expand Down
Loading

0 comments on commit 8bffcd2

Please sign in to comment.