// Processes a change in the subscribed database
        private void DatabaseChanged(object sender, DatabaseChangeEventArgs args)
        {
            if (ChangesFeedMode == ChangesFeedMode.LongPoll)
            {
                // Only send changes if it is the first VALID time (i.e. has at least one change
                // and hasn't started another write yet)
                var changes = Db.ChangesSince(_since, _options, ChangesFilter, FilterParams);
                if (changes.Count > 0 && Interlocked.CompareExchange(ref _filled, 1, 0) == 0)
                {
                    WriteChanges(changes);
                }

                return;
            }
            else if (Interlocked.CompareExchange(ref _filled, 1, 0) == 0)
            {
                // Backfill potentially missed revisions between the check for subscription need
                // and actual subscription
                WriteChanges(Db.ChangesSince(_since, _options, ChangesFilter, FilterParams));
                return;
            }

            var changesToSend = new RevisionList();

            foreach (var change in args.Changes)
            {
                var rev        = change.AddedRevision;
                var winningRev = change.WinningRevisionId;

                if (!ChangesIncludeConflicts)
                {
                    if (winningRev == null)
                    {
                        continue; // this change doesn't affect the winning rev ID, no need to send it
                    }

                    if (rev.Equals(winningRev))
                    {
                        // This rev made a _different_ rev current, so substitute that one.
                        // We need to emit the current sequence # in the feed, so put it in the rev.
                        // This isn't correct internally (this is an old rev so it has an older sequence)
                        // but consumers of the _changes feed don't care about the internal state.
                        if (ChangesIncludeDocs)
                        {
                            Db.LoadRevisionBody(rev);
                        }
                    }
                }

                if (!Db.RunFilter(ChangesFilter, FilterParams, rev))
                {
                    continue;
                }

                changesToSend.Add(rev);
            }

            WriteChanges(changesToSend);
        }
        // Processes a change in the subscribed database
        private void DatabaseChanged(object sender, DatabaseChangeEventArgs args)
        {
            foreach (var change in args.Changes)
            {
                var rev        = change.AddedRevision;
                var winningRev = change.WinningRevision;

                if (!ChangesIncludeConflicts)
                {
                    if (winningRev == null)
                    {
                        continue; // this change doesn't affect the winning rev ID, no need to send it
                    }

                    if (rev.Equals(winningRev))
                    {
                        // This rev made a _different_ rev current, so substitute that one.
                        // We need to emit the current sequence # in the feed, so put it in the rev.
                        // This isn't correct internally (this is an old rev so it has an older sequence)
                        // but consumers of the _changes feed don't care about the internal state.
                        if (ChangesIncludeDocs)
                        {
                            _db.LoadRevisionBody(rev, DocumentContentOptions.None);
                        }
                    }
                }

                if (!_db.RunFilter(ChangesFilter, null, rev))
                {
                    continue;
                }

                if (ChangesFeedMode == ChangesFeedMode.LongPoll)
                {
                    _changes.Add(rev);
                }
                else
                {
                    Log.D(TAG, "Sending continuous change chunk");
                    var written = Response.SendContinuousLine(DatabaseMethods.ChangesDictForRev(rev, this), ChangesFeedMode);
                    if (!written)
                    {
                        Terminate();
                    }
                }
            }

            if (ChangesFeedMode == ChangesFeedMode.LongPoll && _changes.Count > 0)
            {
                var body = new Body(DatabaseMethods.ResponseBodyForChanges(_changes, 0, this));
                Response.WriteData(body.AsJson(), true);
                CouchbaseLiteRouter.ResponseFinished(this);
            }
        }
        // Processes a change in the subscribed database
        private void DatabaseChanged(object sender, DatabaseChangeEventArgs args)
        {
            if (!_filled)
            {
                _filled = true;
                WriteChanges(Db.ChangesSince(_since, _options, ChangesFilter, FilterParams));
                return;
            }

            var changesToSend = new RevisionList();

            foreach (var change in args.Changes)
            {
                var rev        = change.AddedRevision;
                var winningRev = change.WinningRevisionId;

                if (!ChangesIncludeConflicts)
                {
                    if (winningRev == null)
                    {
                        continue; // this change doesn't affect the winning rev ID, no need to send it
                    }

                    if (rev.Equals(winningRev))
                    {
                        // This rev made a _different_ rev current, so substitute that one.
                        // We need to emit the current sequence # in the feed, so put it in the rev.
                        // This isn't correct internally (this is an old rev so it has an older sequence)
                        // but consumers of the _changes feed don't care about the internal state.
                        if (ChangesIncludeDocs)
                        {
                            Db.LoadRevisionBody(rev);
                        }
                    }
                }

                if (!Db.RunFilter(ChangesFilter, FilterParams, rev))
                {
                    continue;
                }

                changesToSend.Add(rev);
            }

            WriteChanges(changesToSend);
        }
        public static ICouchbaseResponseState RevsDiff(ICouchbaseListenerContext context)
        {
            // Collect all of the input doc/revision IDs as CBL_Revisions:
            var revs = new RevisionList();
            var body = context.BodyAs<Dictionary<string, object>>();
            if (body == null) {
                return context.CreateResponse(StatusCode.BadJson).AsDefaultState();
            }

            foreach (var docPair in body) {
                var revIDs = docPair.Value.AsList<string>();
                if (revIDs == null) {
                    return context.CreateResponse(StatusCode.BadParam).AsDefaultState();
                }

                foreach (var revID in revIDs) {
                    var rev = new RevisionInternal(docPair.Key, revID.AsRevID(), false);
                    revs.Add(rev);
                }
            }

            return PerformLogicWithDatabase(context, true, db =>
            {
                var response = context.CreateResponse();
                // Look them up, removing the existing ones from revs:
                db.Storage.FindMissingRevisions(revs);

                // Return the missing revs in a somewhat different format:
                IDictionary<string, object> diffs = new Dictionary<string, object>();
                foreach(var rev in revs) {
                    var docId = rev.DocID;
                    IList<RevisionID> missingRevs = null;
                    if(!diffs.ContainsKey(docId)) {
                        missingRevs = new List<RevisionID>();
                        diffs[docId] = new Dictionary<string, IList<RevisionID>> { { "missing", missingRevs } };
                    } else {
                        missingRevs = ((Dictionary<string, IList<RevisionID>>)diffs[docId])["missing"];
                    }

                    missingRevs.Add(rev.RevID);
                }

                // Add the possible ancestors for each missing revision:
                foreach(var docPair in diffs) {
                    IDictionary<string, IList<RevisionID>> docInfo = (IDictionary<string, IList<RevisionID>>)docPair.Value;
                    int maxGen = 0;
                    RevisionID maxRevID = null;
                    foreach(var revId in docInfo["missing"]) {
                        if(revId.Generation > maxGen) {
                            maxGen = revId.Generation;
                            maxRevID = revId;
                        }
                    }

                    var rev = new RevisionInternal(docPair.Key, maxRevID, false);
                    var ancestors = db.Storage.GetPossibleAncestors(rev, 0, ValueTypePtr<bool>.NULL)?.ToList();
                    if(ancestors != null && ancestors.Count > 0) {
                        docInfo["possible_ancestors"] = ancestors;
                    }
                }

                response.JsonBody = new Body(diffs);
                return response;
            }).AsDefaultState();

        }
        internal override void ProcessInbox(RevisionList inbox)
        {
            // Generate a set of doc/rev IDs in the JSON format that _revs_diff wants:
            // <http://wiki.apache.org/couchdb/HttpPostRevsDiff>
            var diffs = new Dictionary <String, IList <String> >();

            foreach (var rev in inbox)
            {
                var docID = rev.GetDocId();
                var revs  = diffs.Get(docID);
                if (revs == null)
                {
                    revs         = new List <String>();
                    diffs[docID] = revs;
                }
                revs.AddItem(rev.GetRevId());
                AddPending(rev);
            }

            // Call _revs_diff on the target db:
            Log.D(Tag, "processInbox() calling asyncTaskStarted()");
            Log.D(Tag, "posting to /_revs_diff: {0}", String.Join(Environment.NewLine, Manager.GetObjectMapper().WriteValueAsString(diffs)));

            AsyncTaskStarted();
            SendAsyncRequest(HttpMethod.Post, "/_revs_diff", diffs, (response, e) =>
            {
                try {
                    var results = response.AsDictionary <string, object>();

                    Log.D(Tag, "/_revs_diff response: {0}\r\n{1}", response, results);

                    if (e != null)
                    {
                        SetLastError(e);
                        RevisionFailed();
                    }
                    else
                    {
                        if (results.Count != 0)
                        {
                            // Go through the list of local changes again, selecting the ones the destination server
                            // said were missing and mapping them to a JSON dictionary in the form _bulk_docs wants:
                            var docsToSend = new List <object> ();
                            var revsToSend = new RevisionList();
                            foreach (var rev in inbox)
                            {
                                // Is this revision in the server's 'missing' list?
                                IDictionary <string, object> properties = null;
                                var revResults = results.Get(rev.GetDocId()).AsDictionary <string, object>();

                                if (revResults == null)
                                {
                                    continue;
                                }

                                var revs = ((JArray)revResults.Get("missing")).Values <String>().ToList();
                                if (revs == null || !revs.Contains(rev.GetRevId()))
                                {
                                    RemovePending(rev);
                                    continue;
                                }

                                // Get the revision's properties:
                                var contentOptions = DocumentContentOptions.IncludeAttachments;

                                if (!dontSendMultipart && revisionBodyTransformationFunction == null)
                                {
                                    contentOptions &= DocumentContentOptions.BigAttachmentsFollow;
                                }


                                RevisionInternal loadedRev;
                                try {
                                    loadedRev  = LocalDatabase.LoadRevisionBody(rev, contentOptions);
                                    properties = new Dictionary <string, object>(rev.GetProperties());
                                } catch (CouchbaseLiteException e1) {
                                    Log.W(Tag, string.Format("{0} Couldn't get local contents of {1}", rev, this), e1);
                                    RevisionFailed();
                                    continue;
                                }

                                var populatedRev = TransformRevision(loadedRev);

                                IList <string> possibleAncestors = null;
                                if (revResults.ContainsKey("possible_ancestors"))
                                {
                                    possibleAncestors = revResults["possible_ancestors"].AsList <string>();
                                }

                                properties               = new Dictionary <string, object>(populatedRev.GetProperties());
                                var revisions            = LocalDatabase.GetRevisionHistoryDictStartingFromAnyAncestor(populatedRev, possibleAncestors);
                                properties["_revisions"] = revisions;
                                populatedRev.SetProperties(properties);

                                // Strip any attachments already known to the target db:
                                if (properties.ContainsKey("_attachments"))
                                {
                                    // Look for the latest common ancestor and stuf out older attachments:
                                    var minRevPos = FindCommonAncestor(populatedRev, possibleAncestors);

                                    Database.StubOutAttachmentsInRevBeforeRevPos(populatedRev, minRevPos + 1, false);

                                    properties = populatedRev.GetProperties();

                                    if (!dontSendMultipart && UploadMultipartRevision(populatedRev))
                                    {
                                        continue;
                                    }
                                }

                                if (properties == null || !properties.ContainsKey("_id"))
                                {
                                    throw new InvalidOperationException("properties must contain a document _id");
                                }
                                // Add the _revisions list:
                                revsToSend.Add(rev);

                                //now add it to the docs to send
                                docsToSend.AddItem(properties);
                            }

                            UploadBulkDocs(docsToSend, revsToSend);
                        }
                        else
                        {
                            foreach (var revisionInternal in inbox)
                            {
                                RemovePending(revisionInternal);
                            }
                        }
                    }
                }
                catch (Exception ex)
                {
                    Log.E(Tag, "Unhandled exception in Pusher.ProcessInbox", ex);
                }
                finally
                {
                    Log.D(Tag, "processInbox() calling AsyncTaskFinished()");
                    AsyncTaskFinished(1);
                }
            });
        }
        public RevisionList ChangesSince(Int64 lastSequence, ChangesOptions options, RevisionFilter filter)
        {
            // http://wiki.apache.org/couchdb/HTTP_database_API#Changes
            // Translate options to ForestDB:
            if (options.Descending) {
                // https://github.com/couchbase/couchbase-lite-ios/issues/641
                throw new CouchbaseLiteException(StatusCode.NotImplemented);
            }

            var forestOps = C4EnumeratorOptions.DEFAULT;
            forestOps.flags |= C4EnumeratorFlags.IncludeDeleted | C4EnumeratorFlags.IncludeNonConflicted;
            if (options.IncludeDocs || options.IncludeConflicts || filter != null) {
                forestOps.flags |= C4EnumeratorFlags.IncludeBodies;
            }

            var changes = new RevisionList();
            var e = new CBForestDocEnumerator(Forest, lastSequence, forestOps);
            foreach (var next in e) {
                var doc = next.Document;
                var revs = default(IEnumerable<RevisionInternal>);
                if (options.IncludeConflicts) {
                    using (var enumerator = new CBForestHistoryEnumerator(doc, true, false)) {
                        var includeBody = forestOps.flags.HasFlag(C4EnumeratorFlags.IncludeBodies);
                        revs = enumerator.Select(x => new RevisionInternal(x.Document, includeBody)).ToList();
                    }
                } else {
                    revs = new List<RevisionInternal> { new RevisionInternal(doc, forestOps.flags.HasFlag(C4EnumeratorFlags.IncludeBodies)) };
                }

                foreach (var rev in revs) {
                    Debug.Assert(rev != null);
                    if (filter == null || filter(rev)) {
                        if (!options.IncludeDocs) {
                            rev.SetBody(null);
                        }

                        if(filter == null || filter(rev)) {
                            changes.Add(rev);
                        }
                    }
                }
            }

            if (options.SortBySequence) {
                changes.SortBySequence(!options.Descending);
                changes.Limit(options.Limit);
            }

            return changes;
        }
Example #7
0
        public RevisionList ChangesSince(Int64 lastSequence, ChangesOptions options, RevisionFilter filter)
        {
            // http://wiki.apache.org/couchdb/HTTP_database_API#Changes
            // Translate options to ForestDB:
            if (options.Descending)
            {
                // https://github.com/couchbase/couchbase-lite-ios/issues/641
                throw new CouchbaseLiteException(StatusCode.NotImplemented);
            }

            var forestOps = C4EnumeratorOptions.DEFAULT;

            forestOps.flags |= C4EnumeratorFlags.IncludeDeleted | C4EnumeratorFlags.IncludeNonConflicted;
            if (options.IncludeDocs || options.IncludeConflicts || filter != null)
            {
                forestOps.flags |= C4EnumeratorFlags.IncludeBodies;
            }

            var changes = new RevisionList();
            var e       = new CBForestDocEnumerator(Forest, lastSequence, forestOps);

            foreach (var next in e)
            {
                var revs = default(IEnumerable <RevisionInternal>);
                if (options.IncludeConflicts)
                {
                    using (var enumerator = new CBForestHistoryEnumerator(next.GetDocument(), true, false)) {
                        var includeBody = forestOps.flags.HasFlag(C4EnumeratorFlags.IncludeBodies);
                        revs = enumerator.Select(x => new RevisionInternal(x.GetDocument(), includeBody)).ToList();
                    }
                }
                else
                {
                    revs = new List <RevisionInternal> {
                        new RevisionInternal(next.GetDocument(), forestOps.flags.HasFlag(C4EnumeratorFlags.IncludeBodies))
                    };
                }

                foreach (var rev in revs)
                {
                    Debug.Assert(rev != null);
                    if (filter == null || filter(rev))
                    {
                        if (!options.IncludeDocs)
                        {
                            rev.SetBody(null);
                        }

                        if (filter == null || filter(rev))
                        {
                            changes.Add(rev);
                        }
                    }
                }
            }

            if (options.SortBySequence)
            {
                changes.SortBySequence(!options.Descending);
                changes.Limit(options.Limit);
            }

            return(changes);
        }
Example #8
0
        public static ICouchbaseResponseState RevsDiff(ICouchbaseListenerContext context)
        {
            // Collect all of the input doc/revision IDs as CBL_Revisions:
            var revs = new RevisionList();
            var body = context.BodyAs <Dictionary <string, object> >();

            if (body == null)
            {
                return(context.CreateResponse(StatusCode.BadJson).AsDefaultState());
            }

            foreach (var docPair in body)
            {
                var revIDs = docPair.Value.AsList <string>();
                if (revIDs == null)
                {
                    return(context.CreateResponse(StatusCode.BadParam).AsDefaultState());
                }

                foreach (var revID in revIDs)
                {
                    var rev = new RevisionInternal(docPair.Key, revID, false);
                    revs.Add(rev);
                }
            }

            return(PerformLogicWithDatabase(context, true, db =>
            {
                var response = context.CreateResponse();
                // Look them up, removing the existing ones from revs:
                db.Storage.FindMissingRevisions(revs);

                // Return the missing revs in a somewhat different format:
                IDictionary <string, object> diffs = new Dictionary <string, object>();
                foreach (var rev in revs)
                {
                    var docId = rev.GetDocId();
                    IList <string> missingRevs = null;
                    if (!diffs.ContainsKey(docId))
                    {
                        missingRevs = new List <string>();
                        diffs[docId] = new Dictionary <string, IList <string> > {
                            { "missing", missingRevs }
                        };
                    }
                    else
                    {
                        missingRevs = ((Dictionary <string, IList <string> >)diffs[docId])["missing"];
                    }

                    missingRevs.Add(rev.GetRevId());
                }

                // Add the possible ancestors for each missing revision:
                foreach (var docPair in diffs)
                {
                    IDictionary <string, IList <string> > docInfo = (IDictionary <string, IList <string> >)docPair.Value;
                    int maxGen = 0;
                    string maxRevID = null;
                    foreach (var revId in docInfo["missing"])
                    {
                        var parsed = RevisionInternal.ParseRevId(revId);
                        if (parsed.Item1 > maxGen)
                        {
                            maxGen = parsed.Item1;
                            maxRevID = revId;
                        }
                    }

                    var rev = new RevisionInternal(docPair.Key, maxRevID, false);
                    var ancestors = db.Storage.GetPossibleAncestors(rev, 0, false);
                    var ancestorList = ancestors == null ? null : ancestors.ToList();
                    if (ancestorList != null && ancestorList.Count > 0)
                    {
                        docInfo["possible_ancestors"] = ancestorList;
                    }
                }

                response.JsonBody = new Body(diffs);
                return response;
            }).AsDefaultState());
        }
Example #9
0
        private void UploadChanges(IList <RevisionInternal> changes, IDictionary <string, object> revsDiffResults)
        {
            // Go through the list of local changes again, selecting the ones the destination server
            // said were missing and mapping them to a JSON dictionary in the form _bulk_docs wants:
            var docsToSend = new List <object> ();
            var revsToSend = new RevisionList();
            IDictionary <string, object> revResults = null;

            foreach (var rev in changes)
            {
                // Is this revision in the server's 'missing' list?
                if (revsDiffResults != null)
                {
                    revResults = revsDiffResults.Get(rev.DocID).AsDictionary <string, object>();
                    if (revResults == null)
                    {
                        //SafeIncrementCompletedChangesCount();
                        continue;
                    }

                    var revs = revResults.Get("missing").AsList <string>();
                    if (revs == null || !revs.Any(id => id.Equals(rev.RevID, StringComparison.OrdinalIgnoreCase)))
                    {
                        RemovePending(rev);
                        //SafeIncrementCompletedChangesCount();
                        continue;
                    }
                }

                IDictionary <string, object> properties = null;
                // Get the revision's properties:
                var contentOptions = DocumentContentOptions.IncludeAttachments;
                if (!_dontSendMultipart && RevisionBodyTransformationFunction == null)
                {
                    contentOptions |= DocumentContentOptions.BigAttachmentsFollow;
                }

                RevisionInternal loadedRev;
                try {
                    loadedRev = LocalDatabase.LoadRevisionBody(rev);
                    if (loadedRev == null)
                    {
                        throw Misc.CreateExceptionAndLog(Log.To.Sync, StatusCode.DbError, TAG,
                                                         "Unable to load revision body");
                    }

                    properties = new Dictionary <string, object>(rev.GetProperties());
                } catch (Exception e1) {
                    Log.To.Sync.E(TAG, String.Format("Couldn't get local contents of {0}, marking revision failed",
                                                     rev), e1);
                    RevisionFailed();
                    continue;
                }

                var            populatedRev      = TransformRevision(loadedRev);
                IList <string> possibleAncestors = null;
                if (revResults != null && revResults.ContainsKey("possible_ancestors"))
                {
                    possibleAncestors = revResults["possible_ancestors"].AsList <string>();
                }

                properties = new Dictionary <string, object>(populatedRev.GetProperties());

                try {
                    var history = LocalDatabase.GetRevisionHistory(populatedRev, possibleAncestors);
                    if (history == null)
                    {
                        throw Misc.CreateExceptionAndLog(Log.To.Sync, StatusCode.DbError, TAG,
                                                         "Unable to load revision history");
                    }

                    properties["_revisions"] = Database.MakeRevisionHistoryDict(history);
                } catch (Exception e1) {
                    Log.To.Sync.E(TAG, "Error getting revision history, marking revision failed", e1);
                    RevisionFailed();
                    continue;
                }

                populatedRev.SetProperties(properties);
                if (properties.GetCast <bool>("_removed"))
                {
                    RemovePending(rev);
                    continue;
                }

                // Strip any attachments already known to the target db:
                if (properties.ContainsKey("_attachments"))
                {
                    // Look for the latest common ancestor and stuf out older attachments:
                    var minRevPos = FindCommonAncestor(populatedRev, possibleAncestors);
                    try {
                        LocalDatabase.ExpandAttachments(populatedRev, minRevPos + 1, !_dontSendMultipart, false);
                    } catch (Exception ex) {
                        Log.To.Sync.E(TAG, "Error expanding attachments, marking revision failed", ex);
                        RevisionFailed();
                        continue;
                    }

                    properties = populatedRev.GetProperties();
                    if (!_dontSendMultipart && UploadMultipartRevision(populatedRev))
                    {
                        continue;
                    }
                }

                if (properties == null || !properties.ContainsKey("_id"))
                {
                    throw Misc.CreateExceptionAndLog(Log.To.Sync, StatusCode.BadParam, TAG,
                                                     "properties must contain a document _id");
                }

                // Add the _revisions list:
                revsToSend.Add(rev);

                //now add it to the docs to send
                docsToSend.Add(properties);
            }

            UploadBulkDocs(docsToSend, revsToSend);
        }
        public void TestRevTree()
        {
            var change = default(DocumentChange);
            database.Changed += (sender, args) =>
            {
                Assert.AreEqual(1, args.Changes.Count());
                Assert.IsNull(change, "Multiple notifications posted");
                change = args.Changes.First();
            };

            var rev = new RevisionInternal("MyDocId", "4-4444".AsRevID(), false);
            var revProperties = new Dictionary<string, object>();
            revProperties.SetDocRevID(rev.DocID, rev.RevID);
            revProperties["message"] = "hi";
            rev.SetProperties(revProperties);

            var revHistory = new List<RevisionID>();
            revHistory.Add(rev.RevID);
            revHistory.Add("3-3333".AsRevID());
            revHistory.Add("2-2222".AsRevID());
            revHistory.Add("1-1111".AsRevID());
            database.ForceInsert(rev, revHistory, null);
            Assert.AreEqual(1, database.GetDocumentCount());
            VerifyRev(rev, revHistory);
            Assert.AreEqual(Announcement(database, rev, rev), change);
            Assert.IsFalse(change.IsConflict);

            // No-op ForceInsert of already-existing revision
            var lastSeq = database.GetLastSequenceNumber();
            database.ForceInsert(rev, revHistory, null);
            Assert.AreEqual(lastSeq, database.GetLastSequenceNumber());
            
            var conflict = new RevisionInternal("MyDocId", "5-5555".AsRevID(), false);
            var conflictProperties = new Dictionary<string, object>();
            conflictProperties.SetDocRevID(conflict.DocID, conflict.RevID);
            conflictProperties["message"] = "yo";
            conflict.SetProperties(conflictProperties);
            
            var conflictHistory = new List<RevisionID>();
            conflictHistory.Add(conflict.RevID);
            conflictHistory.Add("4-4545".AsRevID());
            conflictHistory.Add("3-3030".AsRevID());
            conflictHistory.Add("2-2222".AsRevID());
            conflictHistory.Add("1-1111".AsRevID());
            change = null;
            database.ForceInsert(conflict, conflictHistory, null);
            Assert.AreEqual(1, database.GetDocumentCount());
            VerifyRev(conflict, conflictHistory);
            Assert.AreEqual(Announcement(database, conflict, conflict), change);
            Assert.IsTrue(change.IsConflict);

            // Add an unrelated document:
            var other = new RevisionInternal("AnotherDocID", "1-1010".AsRevID(), false);
            var otherProperties = new Dictionary<string, object>();
            otherProperties["language"] = "jp";
            other.SetProperties(otherProperties);
            var otherHistory = new List<RevisionID>();
            otherHistory.Add(other.RevID);
            change = null;
            database.ForceInsert(other, otherHistory, null);
            Assert.AreEqual(Announcement(database, other, other), change);
            Assert.IsFalse(change.IsConflict);

            // Fetch one of those phantom revisions with no body:
            var rev2 = database.GetDocument(rev.DocID, "2-2222".AsRevID(), 
                true);
            Assert.IsTrue(rev2.Missing);
            Assert.IsNull(rev2.GetBody());

            Assert.IsNull(database.GetDocument(rev.DocID, "666-6666".AsRevID(), true));

            // Make sure no duplicate rows were inserted for the common revisions:
            if(_storageType == StorageEngineTypes.SQLite) {
                Assert.AreEqual(8, database.GetLastSequenceNumber());
            } else {
                Assert.AreEqual(3, database.GetLastSequenceNumber());
            }
            // Make sure the revision with the higher revID wins the conflict:
            var current = database.GetDocument(rev.DocID, null, 
                true);
            Assert.AreEqual(conflict, current);

            // Check that the list of conflicts is accurate
            var conflictingRevs = database.Storage.GetAllDocumentRevisions(rev.DocID, true, false);
            CollectionAssert.AreEqual(new[] { conflict, rev }, conflictingRevs);
            
            // Get the _changes feed and verify only the winner is in it:
            var options = new ChangesOptions();
            var changes = database.ChangesSince(0, options, null, null);
            CollectionAssert.AreEqual(new[] { conflict, other }, changes);
            options.IncludeConflicts = true;
            changes = database.ChangesSince(0, options, null, null);
            var expectedChanges = new RevisionList();
            expectedChanges.Add(rev);
            expectedChanges.Add(conflict);
            expectedChanges.Add(other);
            var expectedChangesAlt = new RevisionList();
            expectedChangesAlt.Add(conflict);
            expectedChangesAlt.Add(rev);
            expectedChangesAlt.Add(other);
            Assert.IsTrue(expectedChanges.SequenceEqual(changes) || expectedChangesAlt.SequenceEqual(changes));
        }
        internal override void ProcessInbox(RevisionList inbox)
        {
            if (Status == ReplicationStatus.Offline) {
                Log.V(TAG, "Offline, so skipping inbox process");
                return;
            }

            if(_requests.Count > ManagerOptions.Default.MaxOpenHttpConnections) {
                Task.Delay(1000).ContinueWith(t => ProcessInbox(inbox), CancellationToken.None, TaskContinuationOptions.None, WorkExecutor.Scheduler);
                return;
            }

            // Generate a set of doc/rev IDs in the JSON format that _revs_diff wants:
            // <http://wiki.apache.org/couchdb/HttpPostRevsDiff>
            var diffs = new Dictionary<String, IList<String>>();
            foreach (var rev in inbox) {
                var docID = rev.GetDocId();
                var revs = diffs.Get(docID);
                if (revs == null) {
                    revs = new List<String>();
                    diffs[docID] = revs;
                }
                revs.Add(rev.GetRevId());
                AddPending(rev);
            }

            // Call _revs_diff on the target db:
            Log.D(TAG, "posting to /_revs_diff: {0}", String.Join(Environment.NewLine, new[] { Manager.GetObjectMapper().WriteValueAsString(diffs) }));
            SendAsyncRequest(HttpMethod.Post, "/_revs_diff", diffs, (response, e) =>
            {
                try {
                    if(!LocalDatabase.IsOpen) {
                        return;
                    }

                    var results = response.AsDictionary<string, object>();

                    Log.D(TAG, "/_revs_diff response: {0}\r\n{1}", response, results);

                    if (e != null) {
                        LastError = e;
                        RevisionFailed();
                    } else {
                        if (results.Count != 0)  {
                            // Go through the list of local changes again, selecting the ones the destination server
                            // said were missing and mapping them to a JSON dictionary in the form _bulk_docs wants:
                            var docsToSend = new List<object> ();
                            var revsToSend = new RevisionList();
                            foreach (var rev in inbox) {
                                // Is this revision in the server's 'missing' list?
                                IDictionary<string, object> properties = null;
                                var revResults = results.Get(rev.GetDocId()).AsDictionary<string, object>(); 
                                if (revResults == null) {
                                    //SafeIncrementCompletedChangesCount();
                                    continue;
                                }

                                var revs = revResults.Get("missing").AsList<string>();
                                if (revs == null || !revs.Any( id => id.Equals(rev.GetRevId(), StringComparison.OrdinalIgnoreCase))) {
                                    RemovePending(rev);
                                    //SafeIncrementCompletedChangesCount();
                                    continue;
                                }

                                // Get the revision's properties:
                                var contentOptions = DocumentContentOptions.IncludeAttachments;
                                if (!_dontSendMultipart && RevisionBodyTransformationFunction == null)
                                {
                                    contentOptions |= DocumentContentOptions.BigAttachmentsFollow;
                                }

                                RevisionInternal loadedRev;
                                try {
                                    loadedRev = LocalDatabase.LoadRevisionBody (rev);
                                    if(loadedRev == null) {
                                        throw new CouchbaseLiteException("DB is closed", StatusCode.DbError);
                                    }

                                    properties = new Dictionary<string, object>(rev.GetProperties());
                                } catch (Exception e1) {
                                    Log.W(TAG, String.Format("{0} Couldn't get local contents of", rev), e);
                                    RevisionFailed();
                                    continue;
                                }

                                var populatedRev = TransformRevision(loadedRev);
                                IList<string> possibleAncestors = null;
                                if (revResults.ContainsKey("possible_ancestors")) {
                                    possibleAncestors = revResults["possible_ancestors"].AsList<string>();
                                }

                                properties = new Dictionary<string, object>(populatedRev.GetProperties());

                                try {
                                    var history = LocalDatabase.GetRevisionHistory(populatedRev, possibleAncestors);
                                    if(history == null) {
                                        throw new CouchbaseLiteException("DB closed", StatusCode.DbError);
                                    }

                                    properties["_revisions"] = Database.MakeRevisionHistoryDict(history);
                                } catch(Exception e1) {
                                    Log.W(TAG, "Error getting revision history", e1);
                                    RevisionFailed();
                                    continue;
                                }

                                populatedRev.SetProperties(properties);
                                if(properties.GetCast<bool>("_removed")) {
                                    RemovePending(rev);
                                    continue;
                                }

                                // Strip any attachments already known to the target db:
                                if (properties.ContainsKey("_attachments")) {
                                    // Look for the latest common ancestor and stuf out older attachments:
                                    var minRevPos = FindCommonAncestor(populatedRev, possibleAncestors);
                                    try {
                                        LocalDatabase.ExpandAttachments(populatedRev, minRevPos + 1, !_dontSendMultipart, false);
                                    } catch(Exception ex) {
                                        Log.W(TAG, "Error expanding attachments!", ex);
                                        RevisionFailed();
                                        continue;
                                    }

                                    properties = populatedRev.GetProperties();
                                    if (!_dontSendMultipart && UploadMultipartRevision(populatedRev)) {
                                        continue;
                                    }
                                }

                                if (properties == null || !properties.ContainsKey("_id")) {
                                    throw new InvalidOperationException("properties must contain a document _id");
                                }

                                // Add the _revisions list:
                                revsToSend.Add(rev);

                                //now add it to the docs to send
                                docsToSend.Add (properties);
                            }

                            UploadBulkDocs(docsToSend, revsToSend);
                        } else {
                            foreach (var revisionInternal in inbox) {
                                RemovePending(revisionInternal);
                            }

                            SafeAddToCompletedChangesCount(inbox.Count);
                        }
                    }
                } catch (Exception ex) {
                    Log.E(TAG, "Unhandled exception in Pusher.ProcessInbox", ex);
                }
            });
        }
Example #12
0
        internal override void ProcessInbox(RevisionList inbox)
        {
            if (Status == ReplicationStatus.Offline)
            {
                Log.To.Sync.I(TAG, "Offline, so skipping inbox process");
                return;
            }

            if (_requests.Count > ReplicationOptions.MaxOpenHttpConnections)
            {
                Task.Delay(1000).ContinueWith(t => ProcessInbox(inbox), CancellationToken.None, TaskContinuationOptions.None, WorkExecutor.Scheduler);
                return;
            }

            // Generate a set of doc/rev IDs in the JSON format that _revs_diff wants:
            // <http://wiki.apache.org/couchdb/HttpPostRevsDiff>
            var diffs      = new Dictionary <String, IList <String> >();
            var inboxCount = inbox.Count;

            foreach (var rev in inbox)
            {
                var docID = rev.DocID;
                var revs  = diffs.Get(docID);
                if (revs == null)
                {
                    revs         = new List <String>();
                    diffs[docID] = revs;
                }
                revs.Add(rev.RevID);
                AddPending(rev);
            }

            // Call _revs_diff on the target db:
            Log.To.Sync.D(TAG, "posting to /_revs_diff: {0}", String.Join(Environment.NewLine, new[] { Manager.GetObjectMapper().WriteValueAsString(diffs) }));
            SendAsyncRequest(HttpMethod.Post, "/_revs_diff", diffs, (response, e) =>
            {
                try {
                    if (!LocalDatabase.IsOpen)
                    {
                        return;
                    }

                    var results = response.AsDictionary <string, object>();

                    Log.To.Sync.D(TAG, "/_revs_diff response: {0}\r\n{1}", response, results);

                    if (e != null)
                    {
                        LastError = e;
                        for (int i = 0; i < inboxCount; i++)
                        {
                            RevisionFailed();
                        }

                        if (Continuous)
                        {
                            FireTrigger(ReplicationTrigger.WaitingForChanges);
                        }
                        else
                        {
                            FireTrigger(ReplicationTrigger.StopImmediate);
                        }
                    }
                    else
                    {
                        if (results.Count != 0)
                        {
                            // Go through the list of local changes again, selecting the ones the destination server
                            // said were missing and mapping them to a JSON dictionary in the form _bulk_docs wants:
                            var docsToSend = new List <object> ();
                            var revsToSend = new RevisionList();
                            foreach (var rev in inbox)
                            {
                                // Is this revision in the server's 'missing' list?
                                IDictionary <string, object> properties = null;
                                var revResults = results.Get(rev.DocID).AsDictionary <string, object>();
                                if (revResults == null)
                                {
                                    //SafeIncrementCompletedChangesCount();
                                    continue;
                                }

                                var revs = revResults.Get("missing").AsList <string>();
                                if (revs == null || !revs.Any(id => id.Equals(rev.RevID, StringComparison.OrdinalIgnoreCase)))
                                {
                                    RemovePending(rev);
                                    //SafeIncrementCompletedChangesCount();
                                    continue;
                                }

                                // Get the revision's properties:
                                var contentOptions = DocumentContentOptions.IncludeAttachments;
                                if (!_dontSendMultipart && RevisionBodyTransformationFunction == null)
                                {
                                    contentOptions |= DocumentContentOptions.BigAttachmentsFollow;
                                }

                                RevisionInternal loadedRev;
                                try {
                                    loadedRev = LocalDatabase.LoadRevisionBody(rev);
                                    if (loadedRev == null)
                                    {
                                        throw Misc.CreateExceptionAndLog(Log.To.Sync, StatusCode.DbError, TAG,
                                                                         "Unable to load revision body");
                                    }

                                    properties = new Dictionary <string, object>(rev.GetProperties());
                                } catch (Exception e1) {
                                    Log.To.Sync.E(TAG, String.Format("Couldn't get local contents of {0}, marking revision failed",
                                                                     rev), e1);
                                    RevisionFailed();
                                    continue;
                                }

                                var populatedRev = TransformRevision(loadedRev);
                                IList <string> possibleAncestors = null;
                                if (revResults.ContainsKey("possible_ancestors"))
                                {
                                    possibleAncestors = revResults["possible_ancestors"].AsList <string>();
                                }

                                properties = new Dictionary <string, object>(populatedRev.GetProperties());

                                try {
                                    var history = LocalDatabase.GetRevisionHistory(populatedRev, possibleAncestors);
                                    if (history == null)
                                    {
                                        throw Misc.CreateExceptionAndLog(Log.To.Sync, StatusCode.DbError, TAG,
                                                                         "Unable to load revision history");
                                    }

                                    properties["_revisions"] = Database.MakeRevisionHistoryDict(history);
                                } catch (Exception e1) {
                                    Log.To.Sync.E(TAG, "Error getting revision history, marking revision failed", e1);
                                    RevisionFailed();
                                    continue;
                                }

                                populatedRev.SetProperties(properties);
                                if (properties.GetCast <bool>("_removed"))
                                {
                                    RemovePending(rev);
                                    continue;
                                }

                                // Strip any attachments already known to the target db:
                                if (properties.ContainsKey("_attachments"))
                                {
                                    // Look for the latest common ancestor and stuf out older attachments:
                                    var minRevPos = FindCommonAncestor(populatedRev, possibleAncestors);
                                    try {
                                        LocalDatabase.ExpandAttachments(populatedRev, minRevPos + 1, !_dontSendMultipart, false);
                                    } catch (Exception ex) {
                                        Log.To.Sync.E(TAG, "Error expanding attachments, marking revision failed", ex);
                                        RevisionFailed();
                                        continue;
                                    }

                                    properties = populatedRev.GetProperties();
                                    if (!_dontSendMultipart && UploadMultipartRevision(populatedRev))
                                    {
                                        continue;
                                    }
                                }

                                if (properties == null || !properties.ContainsKey("_id"))
                                {
                                    throw Misc.CreateExceptionAndLog(Log.To.Sync, StatusCode.BadParam, TAG,
                                                                     "properties must contain a document _id");
                                }

                                // Add the _revisions list:
                                revsToSend.Add(rev);

                                //now add it to the docs to send
                                docsToSend.Add(properties);
                            }

                            UploadBulkDocs(docsToSend, revsToSend);
                        }
                        else
                        {
                            foreach (var revisionInternal in inbox)
                            {
                                RemovePending(revisionInternal);
                            }

                            //SafeAddToCompletedChangesCount(inbox.Count);
                        }
                    }
                } catch (Exception ex) {
                    Log.To.Sync.E(TAG, "Unhandled exception in Pusher.ProcessInbox, continuing...", ex);
                }
            });
        }
Example #13
0
        internal override void ProcessInbox(RevisionList inbox)
        {
            // Generate a set of doc/rev IDs in the JSON format that _revs_diff wants:
            // <http://wiki.apache.org/couchdb/HttpPostRevsDiff>
            var diffs = new Dictionary<String, IList<String>>();
            foreach (var rev in inbox)
            {
                var docID = rev.GetDocId();
                var revs = diffs.Get(docID);
                if (revs == null)
                {
                    revs = new List<String>();
                    diffs[docID] = revs;
                }
                revs.AddItem(rev.GetRevId());
                AddPending(rev);
            }

            // Call _revs_diff on the target db:
            Log.D(Tag, "processInbox() calling asyncTaskStarted()");
            Log.D(Tag, "posting to /_revs_diff: {0}", String.Join(Environment.NewLine, Manager.GetObjectMapper().WriteValueAsString(diffs)));

            AsyncTaskStarted();
            SendAsyncRequest(HttpMethod.Post, "/_revs_diff", diffs, (response, e) =>
            {
                try {
                    var results = response.AsDictionary<string, object>();

                    Log.D(Tag, "/_revs_diff response: {0}\r\n{1}", response, results);

                    if (e != null) 
                    {
                        SetLastError(e);
                        RevisionFailed();
                    } else {
                        if (results.Count != 0) 
                        {
                            // Go through the list of local changes again, selecting the ones the destination server
                            // said were missing and mapping them to a JSON dictionary in the form _bulk_docs wants:
                            var docsToSend = new List<object> ();
                            var revsToSend = new RevisionList();
                            foreach (var rev in inbox)
                            {
                                // Is this revision in the server's 'missing' list?
                                IDictionary<string, object> properties = null;
                                var revResults = results.Get(rev.GetDocId()).AsDictionary<string, object>();

                                if (revResults == null)
                                {
                                    continue;
                                }

                                var revs = ((JArray)revResults.Get("missing")).Values<String>().ToList();
                                if (revs == null || !revs.Contains(rev.GetRevId()))
                                {
                                    RemovePending(rev);
                                    continue;
                                }

                                // Get the revision's properties:
                                var contentOptions = DocumentContentOptions.IncludeAttachments;

                                if (!dontSendMultipart && revisionBodyTransformationFunction == null)
                                {
                                    contentOptions &= DocumentContentOptions.BigAttachmentsFollow;
                                }


                                RevisionInternal loadedRev;
                                try {
                                    loadedRev = LocalDatabase.LoadRevisionBody (rev, contentOptions);
                                    properties = new Dictionary<string, object>(rev.GetProperties());
                                } catch (CouchbaseLiteException e1) {
                                    Log.W(Tag, string.Format("{0} Couldn't get local contents of {1}", rev, this), e1);
                                    RevisionFailed();
                                    continue;
                                }

                                var populatedRev = TransformRevision(loadedRev);

                                IList<string> possibleAncestors = null;
                                if (revResults.ContainsKey("possible_ancestors"))
                                {
                                    possibleAncestors = revResults["possible_ancestors"].AsList<string>();
                                }

                                properties = new Dictionary<string, object>(populatedRev.GetProperties());
                                var revisions = LocalDatabase.GetRevisionHistoryDictStartingFromAnyAncestor(populatedRev, possibleAncestors);
                                properties["_revisions"] = revisions;
                                populatedRev.SetProperties(properties);

                                // Strip any attachments already known to the target db:
                                if (properties.ContainsKey("_attachments")) 
                                {
                                    // Look for the latest common ancestor and stuf out older attachments:
                                    var minRevPos = FindCommonAncestor(populatedRev, possibleAncestors);

                                    Database.StubOutAttachmentsInRevBeforeRevPos(populatedRev, minRevPos + 1, false);

                                    properties = populatedRev.GetProperties();

                                    if (!dontSendMultipart && UploadMultipartRevision(populatedRev)) 
                                    {
                                        continue;
                                    }
                                }

                                if (properties == null || !properties.ContainsKey("_id"))
                                {
                                    throw new InvalidOperationException("properties must contain a document _id");
                                }
                                // Add the _revisions list:
                                revsToSend.Add(rev);

                                //now add it to the docs to send
                                docsToSend.AddItem (properties);
                            }

                            UploadBulkDocs(docsToSend, revsToSend);
                        } 
                        else 
                        {
                            foreach (var revisionInternal in inbox)
                            {
                                RemovePending(revisionInternal);
                            }
                        }
                    }
                }
                catch (Exception ex)
                {
                    Log.E(Tag, "Unhandled exception in Pusher.ProcessInbox", ex);
                }
                finally
                {
                    Log.D(Tag, "processInbox() calling AsyncTaskFinished()");
                    AsyncTaskFinished(1);
                }
            });
        }
        private void UploadChanges(IList<RevisionInternal> changes, IDictionary<string, object> revsDiffResults)
        {

            // Go through the list of local changes again, selecting the ones the destination server
            // said were missing and mapping them to a JSON dictionary in the form _bulk_docs wants:
            var docsToSend = new List<object> ();
            var revsToSend = new RevisionList();
            IDictionary<string, object> revResults = null;
            foreach (var rev in changes) {
                // Is this revision in the server's 'missing' list?
                if (revsDiffResults != null) {
                    revResults = revsDiffResults.Get(rev.DocID).AsDictionary<string, object>(); 
                    if (revResults == null) {
                        continue;
                    }

                    var revs = revResults.Get("missing").AsList<string>();
                    if (revs == null || !revs.Any(id => id.Equals(rev.RevID.ToString()))) {
                        RemovePending(rev);
                        continue;
                    }
                }

                IDictionary<string, object> properties = null;
                RevisionInternal loadedRev;
                try {
                    loadedRev = LocalDatabase.LoadRevisionBody (rev);
                    if(loadedRev == null) {
                        throw Misc.CreateExceptionAndLog(Log.To.Sync, StatusCode.NotFound, TAG,
                            "Unable to load revision body");
                    }

                    properties = new Dictionary<string, object>(rev.GetProperties());
                } catch (Exception e1) {
                    Log.To.Sync.E(TAG, String.Format("Couldn't get local contents of {0}, marking revision failed",
                        rev), e1);
                    RevisionFailed();
                    continue;
                }

                if (properties.GetCast<bool> ("_removed")) {
                    RemovePending (rev);
                    continue;
                }

                var populatedRev = TransformRevision(loadedRev);
                var backTo = revResults?.Get("possible_ancestors")?.AsList<RevisionID>();

                try {
                    var history = LocalDatabase.GetRevisionHistory(populatedRev, backTo);
                    if(history == null) {
                        throw Misc.CreateExceptionAndLog(Log.To.Sync, StatusCode.DbError, TAG,
                            "Unable to load revision history");
                    }

                    properties["_revisions"] = TreeRevisionID.MakeRevisionHistoryDict(history);
                    populatedRev.SetPropertyForKey("_revisions", properties["_revisions"]);
                } catch(Exception e1) {
                    Log.To.Sync.E(TAG, "Error getting revision history, marking revision failed", e1);
                    RevisionFailed();
                    continue;
                }

                // Strip any attachments already known to the target db:
                if (properties.Get("_attachments") != null) {
                    // Look for the latest common ancestor and stuf out older attachments:
                    var minRevPos = FindCommonAncestor(populatedRev, backTo);
                    try {
                        LocalDatabase.ExpandAttachments(populatedRev, minRevPos + 1, !_dontSendMultipart, false);
                    } catch(Exception ex) {
                        Log.To.Sync.E(TAG, "Error expanding attachments, marking revision failed", ex);
                        RevisionFailed();
                        continue;
                    }

                    properties = populatedRev.GetProperties();
                    if (!_dontSendMultipart && UploadMultipartRevision(populatedRev)) {
                        continue;
                    }
                }

                if (properties == null || !properties.ContainsKey("_id")) {
                    throw Misc.CreateExceptionAndLog(Log.To.Sync, StatusCode.BadParam, TAG,
                        "properties must contain a document _id");
                }

                // Add the _revisions list:
                revsToSend.Add(rev);

                //now add it to the docs to send
                docsToSend.Add (properties);
            }

            UploadBulkDocs(docsToSend, revsToSend);
        }
        public void TestRevTree()
        {
            var rev = new RevisionInternal("MyDocId", "4-abcd", false);
            var revProperties = new Dictionary<string, object>();
            revProperties.Put("_id", rev.GetDocId());
            revProperties.Put("_rev", rev.GetRevId());
            revProperties["message"] = "hi";
            rev.SetProperties(revProperties);

            var revHistory = new List<string>();
            revHistory.Add(rev.GetRevId());
            revHistory.Add("3-abcd");
            revHistory.Add("2-abcd");
            revHistory.Add("1-abcd");
            database.ForceInsert(rev, revHistory, null);
            Assert.AreEqual(1, database.DocumentCount);

            VerifyHistory(database, rev, revHistory);
            var conflict = new RevisionInternal("MyDocId", "5-abcd", false);
            var conflictProperties = new Dictionary<string, object>();
            conflictProperties.Put("_id", conflict.GetDocId());
            conflictProperties.Put("_rev", conflict.GetRevId());
            conflictProperties["message"] = "yo";
            conflict.SetProperties(conflictProperties);
            
            var conflictHistory = new List<string>();
            conflictHistory.Add(conflict.GetRevId());
            conflictHistory.Add("4-bcde");
            conflictHistory.Add("3-bcde");
            conflictHistory.Add("2-abcd");
            conflictHistory.Add("1-abcd");
            database.ForceInsert(conflict, conflictHistory, null);
            Assert.AreEqual(1, database.DocumentCount);
            VerifyHistory(database, conflict, conflictHistory);
            
            // Add an unrelated document:
            var other = new RevisionInternal("AnotherDocID", "1-cdef", false);
            var otherProperties = new Dictionary<string, object>();
            otherProperties["language"] = "jp";
            other.SetProperties(otherProperties);
            var otherHistory = new List<string>();
            otherHistory.Add(other.GetRevId());
            database.ForceInsert(other, otherHistory, null);
            
            // Fetch one of those phantom revisions with no body:
            var rev2 = database.GetDocument(rev.GetDocId(), "2-abcd", 
                true);
            Assert.IsNull(rev2);

            // Make sure no duplicate rows were inserted for the common revisions:
            Assert.IsTrue(database.LastSequenceNumber <= 8);
            // Make sure the revision with the higher revID wins the conflict:
            var current = database.GetDocument(rev.GetDocId(), null, 
                true);
            Assert.AreEqual(conflict, current);
            
            // Get the _changes feed and verify only the winner is in it:
            var options = new ChangesOptions();
            var changes = database.ChangesSince(0, options, null, null);
            var expectedChanges = new RevisionList();
            expectedChanges.Add(conflict);
            expectedChanges.Add(other);
            Assert.AreEqual(expectedChanges, changes);
            options.IncludeConflicts = true;
            changes = database.ChangesSince(0, options, null, null);
            expectedChanges = new RevisionList();
            expectedChanges.Add(rev);
            expectedChanges.Add(conflict);
            expectedChanges.Add(other);
            var expectedChangesAlt = new RevisionList();
            expectedChangesAlt.Add(conflict);
            expectedChangesAlt.Add(rev);
            expectedChangesAlt.Add(other);
            Assert.IsTrue(expectedChanges.SequenceEqual(changes) || expectedChangesAlt.SequenceEqual(changes));
        }