// 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; }
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); }
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()); }
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); } }); }
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); } }); }
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)); }