jackrabbit-oak-dev mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From Thomas Mueller <muel...@adobe.com>
Subject Re: Conflict handling in Oak
Date Tue, 18 Dec 2012 09:38:39 GMT
Hi,

I think it would be better if we clearly define the rules in the
MicroKernel API. There are various edge cases, and having too much freedom
in the MicroKernel API will make oak-core more complicated I guess.

We should also look at what we did in Jackrabbit 2.x and what problems we
ran into. As far as I understand, Jackrabbit 2.x tries to merge concurrent
updates within jackrabbit-core, but we did run into some problems
(concurrent modifications to a single node, concurrently adding child
nodes, concurrently changing and deleting a node). There is an eventing
mechanism within jackrabbit-core so that changes are applied in real time
in other sessions, I believe this mechanism is problematic
performance-wise if there are hundreds of open sessions (there is an open
issue for that). I wonder if we should try to emulate what Jackrabbit 2.x
did, or rather use a different behaviour, now that we have MVCC.

In the past, we discussed that multiple cluster nodes could contain the
same (logical) data, for example "/lib", and concurrently update this
area, and the cluster nodes would then synchronize (merge the changes).
This would require all changes can be merged, but it would be rather
complex, and we would probably need to consult the upper layer on how to
merge (so that node type or other restrictions are not violated, or so
that the index doesn't become corrupt).

With the current MongoDB architecture, there is no shared data: each
MongoDB shard is responsible for a part of the data, but there is no
overlap (for replica sets, all the writes occur on the master, and the
slaves only support read operations). With this architecture, we wouldn't
need to merge changes to the same node; instead, we could throw an
exception to the user of the JCR API.

Within the Microkernel, I wonder if we actually should merge conflicting
updates at all. So, I propose the second session fails (concurrent update
within the MicroKernel) for:


* Two sessions concurrently add a node "/test" (even if the node has the
same properties and values): the second session fails. Reason: this could
be incorrectly interpreted as a same name sibling.

* Two sessions concurrently update the same property of a node to a
different value ("/test/x=1" and "/test/x=2"): the second session fails

* Two sessions concurrently update a property of a node ("/test/x=1" and
"/test/y=2"): the second session fails. This is to simplify checking node
type constraints.


* Two sessions concurrently update the same property of a node to the same
value ("/test/x=1" and "/test/x=1"): the second session fails. This might
seem strange, but let's assume originally the value was "itemsInStock=10",
then a session updates that to "itemsInStock=5" and another session
updates it as well - for a stock keeping (or reservation) application it
would be better if the second update would fail.

* Two session concurrently move a node to another location: the second
sessions fails


* Two session concurrently move a node to the same location: the second
sessions fails (similar reason as for concurrent update)

* One session moves a node, another updates a property: the second session
fails

* One session moves a node, another deletes a node: the second session
fails

* Two sessions concurrently delete a node: the second session fails. This
also seems strange, but it would make it consistent with other concurrent
updates. A possible use case is a reservation system, where each node is
an available seat (so that deleting an available seat would make it
unavailable for other sessions).


* One session moves a node and another deletes, moves, or updates a child
node: the second session fails



What I suggest should be merged within the MicroKernel:

* Two sessions concurrently add different child nodes to a node ("/test/a"
and "/test/b"): this is merged as it's not really a conflict

* Two sessions concurrently delete different child nodes ("/test/a" and
"/test/b"): this is merged

* Two sessions concurrently move different child nodes to another location

The reason for this is to allow concurrently manipulating child nodes if
there are many child nodes (concurrent repository loading).

With this rules, I believe that "2) Furthermore merges should be correctly
mirrored in the journal" wouldn't be required, as there are no merges that
would cause the journal to change.

As for "3) Throwing an unspecific MicrokernelException": yes, this should
be changed. We could also include the line number and position within the
journal in the exception object. But I'm not sure if oak-core should try
to merge (validating node type constraints) or rather also throw to the
JCR API caller.

Regards,
Thomas











On 12/17/12 5:05 PM, "Michael Marth" <mmarth@adobe.com> wrote:

>Hi,
>
>you raise a very important point for a distributed MK implementation.
>
>I agree with your suggestions for 1 and 3.
>Re 2 I would prefer to specify the MUST NOTs (which we would probably
>have to do anyway if we specify the MUSTs IMO)
>
>Michael
>
>On Dec 12, 2012, at 4:46 PM, Michael Dürig wrote:
>
>> Hi,
>> 
>> Currently the Microkernel contract does not specify a merge policy but
>> is free to try to merge conflicting changes or throw an exception. I
>> think this is problematic in various ways:
>> 
>> 1) Automatic merging may violate the principal of least surprise. It
>>can 
>> be arbitrary complex and still be incorrect wrt. different use cases
>> which need different merge strategies for the same conflict.
>> 
>> 2) Furthermore merges should be correctly mirrored in the journal.
>> According to the Microkernel API: "deleting a node is allowed if the
>> node existed in the given revision, even if it was deleted in the
>> meantime." So the following should currently not fail (it does though,
>> see OAK-507):
>> 
>>     String base = mk.getHeadRevision();
>>     String r1 = mk.commit("-a", base)
>>     String r2 = mk.commit("-a", base)
>> 
>> At this point retrieving the journal up to revision r2 should only
>> contain a single -a operation. I'm quite sure this is currently not the
>> case and the journal will contain two -a operations. One for revision
>>r1 
>> and another for revision r2.
>> 
>> 3) Throwing an unspecific MicrokernelException leaves the API consumer
>> with no clue on what caused a commit to fail. Retrying a commit after
>> some client side conflict resolution becomes a hit and miss. See
>>OAK-442.
>> 
>> 
>> To address 1) I suggest we define a set of clear cut cases where any
>> Microkernel implementations MUST merge. For the other cases I'm not
>>sure 
>> whether we should make them MUST NOT, SHOULD NOT or MAY merge.
>> 
>> To address 2) My preferred solution would be to drop getJournal
>>entirely 
>> from the Microkernel API. However, this means rebasing a branch would
>> need to go into the Microkernel (OAK-464). Otherwise every merge
>>defined 
>> for 1) would need to take care the journal is adjusted accordingly.
>> Another possibility here is to leave the journal unadjusted. However
>> then we need to specify MUST NOT for other merges in 1). Because only
>> then can clients of the journal know how to interpret the journal
>> (receptively the conflicts contained therein).
>> 
>> To address 3) I'd simply derive a more specific exception from
>> MicroKernelException and throw that in the case of a conflict. See
>>OAK-496.
>> 
>> Michael
>


Mime
View raw message