From 0a68a2300dccf26a0132e147d532089fadab992e Mon Sep 17 00:00:00 2001 From: Jim Borden Date: Sat, 19 Aug 2017 14:23:04 +0900 Subject: [PATCH] Implement database copy API and concurrency tests --- .../Couchbase.Lite.Tests.NetCore.sln | 13 +- src/Couchbase.Lite.Tests.Shared/ArrayTest.cs | 3 +- .../ConcurrencyTests.cs | 396 ++++++++++++++++++ .../Couchbase.Lite.Tests.Shared.projitems | 1 + .../DatabaseTest.cs | 52 +++ .../Util/WaitAssert.cs | 39 ++ .../Couchbase.Lite.Tests.UWP.sln | 21 +- src/Couchbase.Lite/API/Database/Database.cs | 42 +- src/Couchbase.Lite/API/Document/Document.cs | 16 +- src/Couchbase.Lite/Query/XQuery.cs | 4 + vendor/couchbase-lite-core | 2 +- 11 files changed, 564 insertions(+), 25 deletions(-) create mode 100644 src/Couchbase.Lite.Tests.Shared/ConcurrencyTests.cs diff --git a/src/Couchbase.Lite.Tests.NetCore/Couchbase.Lite.Tests.NetCore.sln b/src/Couchbase.Lite.Tests.NetCore/Couchbase.Lite.Tests.NetCore.sln index a240fd1a1..080e679f3 100644 --- a/src/Couchbase.Lite.Tests.NetCore/Couchbase.Lite.Tests.NetCore.sln +++ b/src/Couchbase.Lite.Tests.NetCore/Couchbase.Lite.Tests.NetCore.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26430.16 +VisualStudioVersion = 15.0.26730.3 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Couchbase.Lite.Tests.NetCore", "Couchbase.Lite.Tests.NetCore.csproj", "{1FE20D63-B747-4937-947B-C7C314ACB048}" EndProject @@ -37,14 +37,14 @@ Global {1FE20D63-B747-4937-947B-C7C314ACB048}.Release|Any CPU.Build.0 = Release|Any CPU {3C662351-B969-4875-94F6-326097A5260B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3C662351-B969-4875-94F6-326097A5260B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3C662351-B969-4875-94F6-326097A5260B}.Packaging|Any CPU.ActiveCfg = Packaging|Any CPU - {3C662351-B969-4875-94F6-326097A5260B}.Packaging|Any CPU.Build.0 = Packaging|Any CPU + {3C662351-B969-4875-94F6-326097A5260B}.Packaging|Any CPU.ActiveCfg = Release|Any CPU + {3C662351-B969-4875-94F6-326097A5260B}.Packaging|Any CPU.Build.0 = Release|Any CPU {3C662351-B969-4875-94F6-326097A5260B}.Release|Any CPU.ActiveCfg = Release|Any CPU {3C662351-B969-4875-94F6-326097A5260B}.Release|Any CPU.Build.0 = Release|Any CPU {9F2A660E-0DBF-4435-A926-48D956EB9898}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9F2A660E-0DBF-4435-A926-48D956EB9898}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9F2A660E-0DBF-4435-A926-48D956EB9898}.Packaging|Any CPU.ActiveCfg = Packaging|Any CPU - {9F2A660E-0DBF-4435-A926-48D956EB9898}.Packaging|Any CPU.Build.0 = Packaging|Any CPU + {9F2A660E-0DBF-4435-A926-48D956EB9898}.Packaging|Any CPU.ActiveCfg = Release|Any CPU + {9F2A660E-0DBF-4435-A926-48D956EB9898}.Packaging|Any CPU.Build.0 = Release|Any CPU {9F2A660E-0DBF-4435-A926-48D956EB9898}.Release|Any CPU.ActiveCfg = Release|Any CPU {9F2A660E-0DBF-4435-A926-48D956EB9898}.Release|Any CPU.Build.0 = Release|Any CPU {EE67782D-519F-40F5-A922-7BB95D9328B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -57,4 +57,7 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {DADF991A-D014-456D-9725-E28C284EACA9} + EndGlobalSection EndGlobal diff --git a/src/Couchbase.Lite.Tests.Shared/ArrayTest.cs b/src/Couchbase.Lite.Tests.Shared/ArrayTest.cs index 5cc42be0e..aa58a9e9a 100644 --- a/src/Couchbase.Lite.Tests.Shared/ArrayTest.cs +++ b/src/Couchbase.Lite.Tests.Shared/ArrayTest.cs @@ -16,7 +16,8 @@ // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and -// l +// limitations under the License. +// using System; using System.Collections.Generic; diff --git a/src/Couchbase.Lite.Tests.Shared/ConcurrencyTests.cs b/src/Couchbase.Lite.Tests.Shared/ConcurrencyTests.cs new file mode 100644 index 000000000..875f7a7ef --- /dev/null +++ b/src/Couchbase.Lite.Tests.Shared/ConcurrencyTests.cs @@ -0,0 +1,396 @@ +// +// ConcurrencyTests.cs +// +// Author: +// Jim Borden +// +// Copyright (c) 2017 Couchbase, Inc All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Couchbase.Lite; +using Couchbase.Lite.Internal.Query; +using Couchbase.Lite.Query; +using FluentAssertions; +using LiteCore; +using LiteCore.Interop; +#if !WINDOWS_UWP +using Xunit; +using Xunit.Abstractions; +#else +using Fact = Microsoft.VisualStudio.TestTools.UnitTesting.TestMethodAttribute; +#endif + +namespace Test +{ +#if WINDOWS_UWP + [Microsoft.VisualStudio.TestTools.UnitTesting.TestClass] +#endif + public sealed class ConcurrencyTests : TestCase + { +#if !WINDOWS_UWP + public ConcurrencyTests(ITestOutputHelper output) : base(output) + { + + } +#endif + + [Fact] + public void TestConcurrentCreate() + { + const int nDocs = 1000; + const uint nConcurrent = 10; + + ConcurrentRuns(nConcurrent, (index) => + { + var tag = $"Create{index}"; + CreateDocs(nDocs, tag).Should().HaveCount(nDocs); + }); + + for (uint i = 0; i < nConcurrent; i++) { + var tag = $"Create{i}"; + VerifyByTagName(tag, nDocs); + } + } + + [Fact] + public void TestConcurrentUpdate() + { + const uint nDocs = 10; + const uint nRounds = 10; + const uint nConcurrent = 10; + + var docs = CreateDocs(nDocs, "Create"); + var docIDs = docs.Select(x => x.Id).ToList(); + + ConcurrentRuns(nConcurrent, (index) => + { + var tag = $"Update{index}"; + UpdateDocs(docIDs, nRounds, tag); + }); + + uint count = 0; + + for (uint i = 0; i < nConcurrent; i++) { + var tag = $"Update{i}"; + VerifyByTagName(tag, (n, row) => + { + count++; + }); + } + + count.Should().Be(nDocs, "because only the last in the previous loop should return results"); + } + + [Fact] + public void TestConcurrentRead() + { + const uint nDocs = 10; + const uint nRounds = 100; + const uint nConcurrent = 10; + + var docs = CreateDocs(nDocs, "Create"); + var docIDs = docs.Select(x => x.Id).ToList(); + + ConcurrentRuns(nConcurrent, (index) => + { + ReadDocIDs(docIDs, nRounds); + }); + } + + [Fact] + public void TestConcurrentDelete() + { + const int nDocs = 1000; + var docs = CreateDocs(nDocs, "Create").ToList(); + docs.Count.Should().Be(nDocs); + + var delete1 = new WaitAssert(); + delete1.RunAssertAsync(() => + { + foreach (var doc in docs) { + Db.Delete(doc); + } + }); + + var delete2 = new WaitAssert(); + delete2.RunAssertAsync(() => + { + foreach (var doc in docs) { + Db.Delete(doc); + } + }); + + WaitAssert.WaitFor(TimeSpan.FromSeconds(60), delete1, delete2); + Db.Count.Should().Be(0, "because all documents were deleted"); + } + + [Fact] + public void TestConcurrentInBatch() + { + const int nDocs = 1000; + const uint nConcurrent = 10; + + ConcurrentRuns(nConcurrent, (index) => + { + Db.InBatch(() => + { + var tag = $"Create{index}"; + CreateDocs(nDocs, tag).Should().HaveCount(nDocs); // Force evaluation, not a needed assert + }); + }); + + for (uint i = 0; i < nConcurrent; i++) { + var tag = $"Create{i}"; + VerifyByTagName(tag, nDocs); + } + } + + [Fact] + public void TestConcurrentPurge() + { + const int nDocs = 1000; + const uint nConcurrent = 10; + + var docs = CreateDocs(nDocs, "Create").ToList(); + docs.Count.Should().Be(nDocs); + + ConcurrentRuns(nConcurrent, index => + { + foreach (var doc in docs) { + try { + Db.Purge(doc); + } catch (CouchbaseLiteException e) { + if (e.Status != StatusCode.NotFound) { + throw; + } + } + } + }); + + Db.Count.Should().Be(0, "because all documents were purged"); + } + + [Fact] + public void TestConcurrentCompact() + { + const int nDocs = 1000; + const uint nRounds = 10; + const uint nConcurrent = 10; + + CreateDocs(nDocs, "Create").Should().HaveCount(nDocs); + + ConcurrentRuns(nConcurrent, index => + { + for (uint i = 0; i < nRounds; i++) { + Db.Compact(); + } + }); + } + + [Fact] + public void TestConcurrentCreateAndCloseDB() + { + const int nDocs = 1000; + + var tag1 = "Create"; + var exp1 = new WaitAssert(); + exp1.RunAssertAsync(() => + { + Action a = () => CreateDocs(nDocs, "Create").ToList(); + a.ShouldThrow(); + }); + + Db.Close(); + exp1.WaitForResult(TimeSpan.FromSeconds(60)); + } + + [Fact] + public void TestConcurrentCreateAndDeleteDB() + { + const int nDocs = 1000; + + var tag1 = "Create"; + var exp1 = new WaitAssert(); + exp1.RunAssertAsync(() => + { + Action a = () => CreateDocs(nDocs, "Create").ToList(); + a.ShouldThrow(); + }); + + Db.Delete(); + exp1.WaitForResult(TimeSpan.FromSeconds(60)); + } + + [Fact] + public void TestDatabaseChange() + { + var exp1 = new WaitAssert(); + var exp2 = new WaitAssert(); + Db.Changed += (sender, args) => + { + exp2.RunAssert(() => + { + exp1.WaitForResult(TimeSpan.FromSeconds(20)); // Test deadlock + }); + }; + + exp1.RunAssertAsync(() => + { + Db.Save(new Document("doc1")); + }); + + exp1.WaitForResult(TimeSpan.FromSeconds(10)); + } + + [Fact] + public void TestDocumentChange() + { + var exp1 = new WaitAssert(); + var exp2 = new WaitAssert(); + Db.AddDocumentChangedListener("doc1", (sender, args) => + { + exp2.RunAssert(() => + { + exp1.WaitForResult(TimeSpan.FromSeconds(20)); // Test deadlock + }); + }); + + exp1.RunAssertAsync(() => + { + Db.Save(new Document("doc1")); + }); + + exp1.WaitForResult(TimeSpan.FromSeconds(10)); + } + + private void ReadDocIDs(IEnumerable docIDs, uint rounds) + { + for (uint r = 1; r <= rounds; r++) { + foreach (var docID in docIDs) { + var doc = Db.GetDocument(docID); + doc.Should().NotBeNull(); + doc.Id.Should().Be(docID); + } + } + } + + private void UpdateDocs(IEnumerable docIDs, uint rounds, string tag) + { + uint n = 0; + for (uint r = 1; r <= rounds; r++) { + foreach (var docID in docIDs) { + var doc = Db.GetDocument(docID); + doc.Set("tag", tag); + + var address = doc.GetDictionary("address"); + address.Should().NotBeNull(); + var street = $"{n} street."; + address.Set("street", street); + + var phones = doc.GetArray("phones"); + phones.Should().NotBeNull().And.HaveCount(2); + var phone = $"650-000-{n}"; + phones.Set(0, phone); + + doc.Set("updated", DateTimeOffset.UtcNow); + + WriteLine($"[{tag}] rounds: {r} updating {doc.Id}"); + Db.Save(doc); + } + } + } + + private void VerifyByTagName(string name, Action test) + { + var TAG = Expression.Property("tag"); + var DOCID = SelectResult.Expression(Expression.Meta().ID); + using (var q = Query.Select(DOCID).From(DataSource.Database(Db)).Where(TAG.EqualTo(name))) { + WriteLine((q as XQuery).Explain()); + + using (var e = q.Run()) { + ulong n = 0; + foreach (var row in e) { + test(++n, row); + } + } + } + } + + private void VerifyByTagName(string name, uint numRows) + { + uint count = 0; + VerifyByTagName(name, (n, row) => + { + count++; + }); + + count.Should().Be(numRows); + } + + private Document CreateDocument(string tag) + { + var doc = new Document(); + + doc.Set("tag", tag); + + doc.Set("firstName", "Daniel"); + doc.Set("lastName", "Tiger"); + + var address = new DictionaryObject(); + address.Set("street", "1 Main street"); + address.Set("city", "Mountain View"); + address.Set("state", "CA"); + doc.Set("address", address); + + var phones = new ArrayObject { + "650-123-0001", + "650-123-0002" + }; + doc.Set("phones", phones); + + doc.Set("updated", DateTimeOffset.UtcNow); + + return doc; + } + + private IEnumerable CreateDocs(uint count, string tag) + { + for (uint i = 1; i <= count; i++) { + var doc = CreateDocument(tag); + WriteLine($"[{tag}] rounds: {i} saving {doc.Id}"); + Db.Save(doc); + yield return doc; + } + } + + private void ConcurrentRuns(uint nRuns, Action block) + { + var expectations = new WaitAssert[nRuns]; + for (uint i = 0; i < nRuns; i++) { + expectations[i] = new WaitAssert(); + expectations[i].RunAssertAsync(block, i); + } + + foreach (var exp in expectations) { + exp.WaitForResult(TimeSpan.FromSeconds(60)); + } + } + } +} diff --git a/src/Couchbase.Lite.Tests.Shared/Couchbase.Lite.Tests.Shared.projitems b/src/Couchbase.Lite.Tests.Shared/Couchbase.Lite.Tests.Shared.projitems index bbc7c88cc..34127c356 100644 --- a/src/Couchbase.Lite.Tests.Shared/Couchbase.Lite.Tests.Shared.projitems +++ b/src/Couchbase.Lite.Tests.Shared/Couchbase.Lite.Tests.Shared.projitems @@ -10,6 +10,7 @@ + diff --git a/src/Couchbase.Lite.Tests.Shared/DatabaseTest.cs b/src/Couchbase.Lite.Tests.Shared/DatabaseTest.cs index 9c3a8bc7b..255e6757f 100644 --- a/src/Couchbase.Lite.Tests.Shared/DatabaseTest.cs +++ b/src/Couchbase.Lite.Tests.Shared/DatabaseTest.cs @@ -30,6 +30,7 @@ using LiteCore; using LiteCore.Interop; using System.Linq; +using Couchbase.Lite.Query; #if !WINDOWS_UWP using Xunit; using Xunit.Abstractions; @@ -867,6 +868,57 @@ public void TestConfigurationIsCopiedWhenGetSet() } } + [Fact] + public void TestCopy() + { + for (int i = 0; i < 10; i++) { + var docID = $"doc{i}"; + using (var doc = new Document(docID)) { + doc.Set("name", docID); + + var data = Encoding.UTF8.GetBytes(docID); + var blob = new Blob("text/plain", data); + doc.Set("data", blob); + + SaveDocument(doc); + } + } + + var dbName = "nudb"; + var config = Db.Config; + var dir = config.Directory; + + Database.Delete(dbName, dir); + Database.Copy(Db.Path, dbName, config); + + Database.Exists(dbName, dir).Should().BeTrue(); + using (var nudb = new Database(dbName, config)) { + nudb.Count.Should().Be(10, "because it is a copy of another database with 10 items"); + var DOCID = Expression.Meta().ID; + var S_DOCID = SelectResult.Expression(DOCID); + using (var q = Query.Select(S_DOCID).From(DataSource.Database(nudb))) { + using (var rs = q.Run()) { + foreach (var r in rs) { + var docID = r.GetString(0); + docID.Should().NotBeNull(); + + var doc = nudb.GetDocument(docID); + doc.Should().NotBeNull(); + doc.GetString("name").Should().Be(docID); + + var blob = doc.GetBlob("data"); + blob.Should().NotBeNull(); + + var data = Encoding.UTF8.GetString(blob.Content); + data.Should().Be(docID); + } + } + } + } + + Database.Delete(dbName, dir); + } + private void DeleteDB(Database db) { var path = db.Path; diff --git a/src/Couchbase.Lite.Tests.Shared/Util/WaitAssert.cs b/src/Couchbase.Lite.Tests.Shared/Util/WaitAssert.cs index 6c26bd788..41ae133eb 100644 --- a/src/Couchbase.Lite.Tests.Shared/Util/WaitAssert.cs +++ b/src/Couchbase.Lite.Tests.Shared/Util/WaitAssert.cs @@ -21,6 +21,7 @@ using System; using System.Collections.Generic; using System.Threading; +using System.Threading.Tasks; namespace Couchbase.Lite { @@ -31,6 +32,13 @@ internal sealed class WaitAssert public IList CaughtExceptions { get; } = new List(); + public static void WaitFor(TimeSpan timeout, params WaitAssert[] asserts) + { + foreach (var assert in asserts) { + assert.WaitForResult(timeout); + } + } + public void RunAssert(Action assertAction) { try { @@ -43,6 +51,32 @@ public void RunAssert(Action assertAction) } } + public async Task RunAssertAsync(Action assertAction) + { + try { + await Task.Factory.StartNew(assertAction); + } + catch (Exception e) { + _caughtException = e; + CaughtExceptions.Add(e); + } + finally { + _mre.Set(); + } + } + + public async Task RunAssertAsync(Action assertAction, T arg) + { + try { + await Task.Factory.StartNew(() => assertAction(arg)); + } catch (Exception e) { + _caughtException = e; + CaughtExceptions.Add(e); + } finally { + _mre.Set(); + } + } + public void RunConditionalAssert(Func assertAction) { var done = false; @@ -57,6 +91,11 @@ public void RunConditionalAssert(Func assertAction) } } + public void Fulfill() + { + _mre.Set(); + } + public void WaitForResult(TimeSpan timeout) { if (!_mre.WaitOne(timeout)) { diff --git a/src/Couchbase.Lite.Tests.UWP/Couchbase.Lite.Tests.UWP.sln b/src/Couchbase.Lite.Tests.UWP/Couchbase.Lite.Tests.UWP.sln index 1580d2a69..f4cd77150 100644 --- a/src/Couchbase.Lite.Tests.UWP/Couchbase.Lite.Tests.UWP.sln +++ b/src/Couchbase.Lite.Tests.UWP/Couchbase.Lite.Tests.UWP.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26430.15 +VisualStudioVersion = 15.0.26730.3 MinimumVisualStudioVersion = 10.0.40219.1 Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Couchbase.Lite.Tests.Shared", "..\Couchbase.Lite.Tests.Shared\Couchbase.Lite.Tests.Shared.shproj", "{11F38891-8397-49B4-9146-6DAF0A9D7866}" EndProject @@ -49,14 +49,14 @@ Global {DD9CB298-F3A1-4653-883B-352E69C16F93}.Debug|x64.Build.0 = Debug|Any CPU {DD9CB298-F3A1-4653-883B-352E69C16F93}.Debug|x86.ActiveCfg = Debug|Any CPU {DD9CB298-F3A1-4653-883B-352E69C16F93}.Debug|x86.Build.0 = Debug|Any CPU - {DD9CB298-F3A1-4653-883B-352E69C16F93}.Packaging|Any CPU.ActiveCfg = Packaging|Any CPU - {DD9CB298-F3A1-4653-883B-352E69C16F93}.Packaging|Any CPU.Build.0 = Packaging|Any CPU - {DD9CB298-F3A1-4653-883B-352E69C16F93}.Packaging|ARM.ActiveCfg = Packaging|Any CPU - {DD9CB298-F3A1-4653-883B-352E69C16F93}.Packaging|ARM.Build.0 = Packaging|Any CPU - {DD9CB298-F3A1-4653-883B-352E69C16F93}.Packaging|x64.ActiveCfg = Packaging|Any CPU - {DD9CB298-F3A1-4653-883B-352E69C16F93}.Packaging|x64.Build.0 = Packaging|Any CPU - {DD9CB298-F3A1-4653-883B-352E69C16F93}.Packaging|x86.ActiveCfg = Packaging|Any CPU - {DD9CB298-F3A1-4653-883B-352E69C16F93}.Packaging|x86.Build.0 = Packaging|Any CPU + {DD9CB298-F3A1-4653-883B-352E69C16F93}.Packaging|Any CPU.ActiveCfg = Release|Any CPU + {DD9CB298-F3A1-4653-883B-352E69C16F93}.Packaging|Any CPU.Build.0 = Release|Any CPU + {DD9CB298-F3A1-4653-883B-352E69C16F93}.Packaging|ARM.ActiveCfg = Release|Any CPU + {DD9CB298-F3A1-4653-883B-352E69C16F93}.Packaging|ARM.Build.0 = Release|Any CPU + {DD9CB298-F3A1-4653-883B-352E69C16F93}.Packaging|x64.ActiveCfg = Release|Any CPU + {DD9CB298-F3A1-4653-883B-352E69C16F93}.Packaging|x64.Build.0 = Release|Any CPU + {DD9CB298-F3A1-4653-883B-352E69C16F93}.Packaging|x86.ActiveCfg = Release|Any CPU + {DD9CB298-F3A1-4653-883B-352E69C16F93}.Packaging|x86.Build.0 = Release|Any CPU {DD9CB298-F3A1-4653-883B-352E69C16F93}.Release|Any CPU.ActiveCfg = Release|Any CPU {DD9CB298-F3A1-4653-883B-352E69C16F93}.Release|Any CPU.Build.0 = Release|Any CPU {DD9CB298-F3A1-4653-883B-352E69C16F93}.Release|ARM.ActiveCfg = Release|Any CPU @@ -157,4 +157,7 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {761D58C7-AD0C-46D1-BC96-A570C0551FE7} + EndGlobalSection EndGlobal diff --git a/src/Couchbase.Lite/API/Database/Database.cs b/src/Couchbase.Lite/API/Database/Database.cs index 0a9ee6d07..2dd9e913b 100644 --- a/src/Couchbase.Lite/API/Database/Database.cs +++ b/src/Couchbase.Lite/API/Database/Database.cs @@ -241,6 +241,30 @@ public Database(Database other) #region Public Methods + public static void Copy(string path, string name, DatabaseConfiguration config) + { + var destPath = DatabasePath(name, config.Directory); + var nativeConfig = DBConfig; + if (config.EncryptionKey != null) { +#if true + throw new NotImplementedException("Encryption is not yet supported"); +#else + var key = config.EncryptionKey; + int i = 0; + nativeConfig.encryptionKey.algorithm = C4EncryptionAlgorithm.AES256; + foreach(var b in key.KeyData) { + nativeConfig.encryptionKey.bytes[i++] = b; + } +#endif + } + + LiteCoreBridge.Check(err => + { + var nativeConfigCopy = nativeConfig; + return Native.c4db_copy(path, destPath, &nativeConfigCopy, err); + }); + } + /// /// Deletes the contents of a database with the given name in the /// given directory @@ -509,11 +533,21 @@ public void Save(Document document) #region Internal Methods + internal void BeginTransaction() + { + LiteCoreBridge.Check(err => Native.c4db_beginTransaction(_c4db, err)); + } + internal void ChangeEncryptionKey(object key) { throw new NotImplementedException(); } + internal void EndTransaction(bool commit) + { + LiteCoreBridge.Check(err => Native.c4db_endTransaction(_c4db, commit, err)); + } + internal void ResolveConflict(string docID, IConflictResolver resolver) { InBatch(() => @@ -697,6 +731,7 @@ private void Open() private void PostDatabaseChanged() { + var allChanges = new List(); _threadSafety.DoLocked(() => { if (_obs == null || _c4db == null || Native.c4db_isInTransaction(_c4db)) { @@ -716,7 +751,7 @@ private void PostDatabaseChanged() if (docIDs.Count > 0) { // Only notify if there are actually changes to send var args = new DatabaseChangedEventArgs(this, docIDs, external); - Changed?.Invoke(this, args); + allChanges.Add(args); docIDs = new List(); } } @@ -727,6 +762,10 @@ private void PostDatabaseChanged() } } while (nChanges > 0); }); + + foreach (var args in allChanges) { + Changed?.Invoke(this, args); + } } private void PostDocChanged(string documentID) @@ -762,6 +801,7 @@ private Document VerifyDB(Document document) /// public void Dispose() { + Console.WriteLine($"DATABASE CLOSED {Environment.StackTrace}"); GC.SuppressFinalize(this); _threadSafety.DoLocked(() => Dispose(true)); } diff --git a/src/Couchbase.Lite/API/Document/Document.cs b/src/Couchbase.Lite/API/Document/Document.cs index f26c7317c..87b52c006 100644 --- a/src/Couchbase.Lite/API/Document/Document.cs +++ b/src/Couchbase.Lite/API/Document/Document.cs @@ -233,15 +233,14 @@ private void Save(IConflictResolver resolver, bool deletion) } C4Document* newDoc = null; - var endedEarly = false; - Database.InBatch(() => - { + var success = true; + Database.BeginTransaction(); + try { var tmp = default(C4Document*); SaveInto(&tmp, deletion); if (tmp == null) { Merge(resolver, deletion); if (!_dict.HasChanges) { - endedEarly = true; return; } @@ -252,10 +251,11 @@ private void Save(IConflictResolver resolver, bool deletion) } newDoc = tmp; - }); - - if (endedEarly) { - return; + } catch (Exception) { + success = false; + throw; + } finally { + Database.EndTransaction(success); } c4Doc = newDoc; diff --git a/src/Couchbase.Lite/Query/XQuery.cs b/src/Couchbase.Lite/Query/XQuery.cs index cba428831..1edbd287b 100644 --- a/src/Couchbase.Lite/Query/XQuery.cs +++ b/src/Couchbase.Lite/Query/XQuery.cs @@ -111,6 +111,10 @@ protected virtual void Dispose(bool finalizing) internal string Explain() { // Used for debugging + if (_c4Query == null) { + Check(); + } + return Native.c4query_explain(_c4Query); } diff --git a/vendor/couchbase-lite-core b/vendor/couchbase-lite-core index ffa37436c..15200e247 160000 --- a/vendor/couchbase-lite-core +++ b/vendor/couchbase-lite-core @@ -1 +1 @@ -Subproject commit ffa37436c34f32fe4eb4f9f0a80570fd140c8f7e +Subproject commit 15200e24786dff767bef34e37667a2fcf4f7c73e