This is an automated email from the ASF dual-hosted git repository. wohali pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/couchdb-documentation.git
commit 4f24edf0867b70e9d43d0b6e07ec1a0379d1fe7e Author: Joan Touzet <jo...@atypical.net> AuthorDate: Mon Dec 17 18:21:17 2018 -0500 Migrating 'How to design for replication' from MoinMoin --- src/best-practices/documents.rst | 243 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) diff --git a/src/best-practices/documents.rst b/src/best-practices/documents.rst index 991731c..80fd04a 100644 --- a/src/best-practices/documents.rst +++ b/src/best-practices/documents.rst @@ -69,3 +69,246 @@ Later, you can then further `decimate walking the entire database and generating documents to be stored in a new database with a lower level of granularity (say, 1 document a day). You can then delete the older, more fine-grained database when you're done with it. + +Designing an application to work with replication +------------------------------------------------- + +Whilst CouchDB includes replication and a conflict-flagging mechanism, this is +not the whole story for building an application which replicates in a way which +users expect. + +Here we consider a simple example of a bookmarks application. The idea is that +a user can replicate their own bookmarks, work with them on another machine, +and then synchronise their changes later. + +Let's start with a very simple definition of bookmarks: an ordered, nestable +mapping of name to URL. Internally the application might represent it like +this: + +.. code-block:: javascript + + [ + {"name":"Weather", "url":"http://www.bbc.co.uk/weather"}, + {"name":"News", "url":"http://news.bbc.co.uk/"}, + {"name":"Tech", "bookmarks": [ + {"name":"Register", "url":"http://www.theregister.co.uk/"}, + {"name":"CouchDB", "url":"http://couchdb.apache.org/"} + ]} + ] + +It can then present the bookmarks menu and sub-menus by traversing this structure. + +Now consider this scenario: the user has a set of bookmarks on her PC, and then +replicates it to her laptop. On the laptop, she changes the News link to point +to CNN, renames "Register" to "The Register", and adds a new link to slashdot +just after it. On the desktop, her husband deletes the Weather link, and adds a +new link to CNET in the Tech folder. + +So after these changes, the laptop has: + +.. code-block:: javascript + + [ + {"name":"Weather", "url":"http://www.bbc.co.uk/weather"}, + {"name":"News", "url":"http://www.cnn.com/"}, + {"name":"Tech", "bookmarks": [ + {"name":"The Register", "url":"http://www.theregister.co.uk/"}, + {"name":"Slashdot", "url":"http://www.slashdot.new/"}, + {"name":"CouchDB", "url":"http://couchdb.apache.org/"} + ]} + ] + +and the PC has: + +.. code-block:: javascript + + [ + {"name":"News", "url":"http://www.cnn.com/"}, + {"name":"Tech", "bookmarks": [ + {"name":"Register", "url":"http://www.theregister.co.uk/"}, + {"name":"CouchDB", "url":"http://couchdb.apache.org/"}, + {"name":"CNET", "url":"http://news.cnet.com/"} + ]} + ] + +Upon the next synchronisation, we want the expected merge to take place. That +is: links which were changed, added or deleted on one side are also changed, +added or deleted on the other side - with no human intervention required unless +absolutely necessary. + +We will also assume that both sides are doing a CouchDB "compact" operation +periodically, and are disconnected for more than this time before they +resynchronise. + +All of the approaches below which allow automated merging of changes rely on +having some sort of history back in time to the point where the replicas +diverged. + +CouchDB does not provide a mechanism for this itself. It stores arbitrary +numbers of old _ids for one document (trunk now has a mechanism for pruning the +_id history), for the purposes of replication. However it will not keep the +documents themselves through a compaction cycle, except where there are +conflicting versions of a document. + +*Do not rely on the CouchDB revision history mechanism to help you build an +application-level version history.* Its sole purpose is to ensure eventually +consistent replication between databases. It is up to you to maintain history +explicitly in whatever form makes sense for your application, and to prune it +to avoid excessive storage utilisation, whilst not pruning past the point where +live replicas last diverged. + +Approach 1: Single JSON doc +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The above structure is already valid Javascript, and so could be represented in +CouchDB just by wrapping it in an object and storing as a single document: + +.. code-block:: javascript + + { + "bookmarks": + // ... same as above + } + +This makes life very easy for the application, as the ordering and nesting is +all taken care of. The trouble here is that on replication, only two sets of +bookmarks will be visible: example B and example C. One will be chosen as the +main revision, and the other will be stored as a conflicting revision. + +At this point, the semantics are very unsatisfactory from the user's point of +view. The best that can be offered is a choice saying "Which of these two sets +of bookmarks do you wish to keep: B or C?" However neither represents the +desired outcome. There is also insufficient data to be able to correctly merge +them, since the base revision A is lost. + +This is going to be highly unsatisfactory for the user, who will have to apply +one set of changes again manually. + +Approach 2: Separate document per bookmark +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +An alternative solution is to make each field (bookmark) a separate document in +its own right. Adding or deleting a bookmark is then just a case of adding or +deleting a document, which will never conflict (although if the same bookmark +is added on both sides, then you will end up with two copies of it). Changing a +bookmark will only conflict if both sides made changes to the same one, and +then it is reasonable to ask the user to choose between them. + +Since there will now be lots of small documents, you may either wish to keep a +completely separate database for bookmarks, or else add an attribute to +distinguish bookmarks from other kinds of document in the database. In the +latter case, a view can be made to return only bookmark documents. + +Whilst replication is now fixed, care is needed with the "ordered" and +"nestable" properties of bookmarks. + +For ordering, one suggestion is to give each item a floating-point index, and +then when inserting an object between A and B, give it an index which is the +average of A and B's indices. Unfortunately, this will fail after a while when +you run out of precision, and the user will be bemused to find that their most +recent bookmarks no longer remember the exact position they were put in. + +A better way is to keep a string representation of index, which can grow as the +tree is subdivided. This will not suffer the above problem, but it may result +in this string becoming arbitrarily long after time. They could be renumbered, +but the renumbering operation could introduce a lot of conflicts, especially if +attempted by both sides independently. + +For "nestable", you can have a separate doc which represents a list of +bookmarks, and each bookmark can have a "belongs to" field which identifies the +list. It may be useful anyway to be able to have multiple top-level bookmark +sets (Bob's bookmarks, Jill's bookmarks etc). Some care is needed when deleting +a list or sub-list, to ensure that all associated bookmarks are also deleted, +otherwise they will become orphaned. + +Building the entire bookmark set can be performed through the use of emitting +a compound key that describes the path to the document, then using group levels +to retrieve the position of the tree in the document. The following code +excerpt describes a tree of files, where the path to the file is stored in +the document under the ``"path"`` key: + +.. code-block:: javascript + + // map function + function(doc) { + if (doc.type === "file") { + if (doc.path.substr(-1) === "/") { + var raw_path = doc.path.slice(0, -1); + } else { + var raw_path = doc.path; + } + emit (raw_path.split('/'), 1); + } + } + + // reduce + _sum + +This will emit rows into the view of the form ``["opt", "couchdb", "etc", +"local.ini"]`` for a ``doc.path`` of ``/opt/couchdb/etc/local.ini``. You can +then query a list of files in the ``/opt/couchdb/etc`` directory by specifying +a ``startkey`` of ``["opt", "couchdb", "etc"]`` and an ``endkey`` of ``["opt", +"couchdb", "etc", {}]``. + +Approach 3: Immutable history / event sourcing +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Another approach to consider is `Event Sourcing +<https://martinfowler.com/eaaDev/EventSourcing.html>`_ or Command Logging, as +implemented in many NoSQL databases and as used in many `operational +transformation <https://en.wikipedia.org/wiki/Operational_transformation>`_ +systems. + +In this model, instead of storing individual bookmarks, you store records of +changes made - "Bookmark added", "Bookmark changed", "Bookmark moved", +"Bookmark deleted". These are stored in an append-only fashion. Since records +are never modified or deleted, only added to, there are never any replication +conflicts. + +These records can also be stored as an array in a single CouchDB document. +Replication can cause a conflict, but in this case it is easy to resolve by +simply combining elements from the two arrays. + +In order to see the full set of bookmarks, you need to start with a baseline +set (initially empty) and run all the change records since the baseline was +created; and/or you need to maintain a most-recent version and update it with +changes not yet seen. + +Care is needed after replication when merging together history from multiple +sources. You may get different results depending on how you order them - +consider taking all A's changes before B's, taking all B's before A's, or +interleaving them (e.g. if each change has a timestamp). + +Also, over time the amount of storage used can grow arbitrarily large, even if +the set of bookmarks itself is small. This can be controlled by moving the +baseline version forwards and then keeping only the changes after that point. +However, care is needed not to move the baseline version forward so far that +there are active replicas out there which last synchronised before that time, +as this may result in conflicts which cannot be resolved automatically. + +If there is any uncertainty, it is best to present the user with a prompt to +assist with merging the content in the application itself. + +Approach 4: Keep historic versions explicitly +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If you are going to keep a command log history, then it may be simpler just to +keep old revisions of the bookmarks list itself around. The intention is to +subvert CouchDB's automatic behaviour of purging old revisions, by keeping +these revisions as separate documents. + +You can keep a pointer to the 'most current' revision, and each revision can +point to its predecessor. On replication, merging can take place by diffing +each of the previous versions (in effect synthesising the command logs) back to +a common ancestor. + +This is the sort of behaviour which revision control systems such as `Git +<http://git-scm.org/>`_ implement as a matter of routine, although generally +comparing text files line-by-line rather than comparing JSON objects +field-by-field. + +Systems like Git will accumulate arbitrarily large amounts of history (although +they will attempt to compress it by packing multiple revisions so that only +their diffs are stored). With Git you can use "history rewriting" to remove old +history, but this may prohibit merging if history doesn't go back far enough in +time.