Sorry for writing so many messages; I just realized I made an unwarranted assumption in my previous two messages, and I'm beginning to understand better what you have been saying.
For people who don't want to read through all my verbose stuff, the point of this message is to explore more carefully (1) how to simplify editing conflicts by only thinking in terms of creations/deletions rather than other operations; and (2) how exactly the real-time nature of local editing can be reconciled with the non-real-time nature of network communication in this context. This is a completely different viewpoint than the one I was previously advocating, and it leads to issues that are completely different but probably a lot simpler. (It's much less permissive, since conflicting edits end up being rejected rather than merged.) You might not want to actually go this way and do something very different. My main point is that, before embarking onto actual coding, one should think about what actually happens in various scenarios of editing conflicts such as the ones I discuss below, and come up with strategies for all possible cases. Failing to think carefully in advance means one might end up wasting a lot of effort... Denis (1) FORMULATING ALL OPERATIONS AS CREATIONS/DELETIONS: The types of items that currently go into the undo/redo stack are fairly close to "user operations" conceptually, but in fact one could try to view them all in terms of adding and removing items (strokes or text boxes) in the journal. Currently, moving a selected set of strokes is encoded as an ITEM_MOVESEL operation, which basically says which strokes are moved and by how much (the strokes don't have IDs, but if they did, they would keep the same IDs along the way); instead one could try to encode it as "delete all these strokes, and insert all these new strokes". The two main conceptual differences I can see are: 1. Once everything is expressed in terms purely of creating and deleting items, there are far fewer things to worry about concerning the order in which operations are performed. So expressing everything as creations/deletions should simplify a lot the handling of editing conflicts. This is a compelling reason to move away from thinking in terms of actual user actions such as "move strokes", and expressing everything in terms of creations and deletions. 2. On the other hand, this completely breaks the "identity" of the strokes being manipulated: if I'm deleting my strokes and creating new ones elsewhere, they're no longer the same strokes. To take an example: let's say we start with stroke #1 in green, user A changes it to red, and user B moves it on the page: if they are both doing this at the same time, A will try to say "delete stroke #1, create a new red stroke -- which will become say #2", while B will try to say "delete stroke #1, create a new green stroke elsewhere -- which becomes say stroke #3". The server gets to decide which operation happened first. Let's say it gets the message from A first, so that stroke #1 gets deleted and replaced with stroke #2. Then what happens when B's operation is received? The question is: do we end up with the two strokes #2 and #3 on the page? (one moved and the other repainted). Or does the second operation processed by the server become entirely invalid (because stroke #1 can't be deleted anymore, stroke #3 should not be created either)? The second outcome is of course much better than the first one, and it is satisfactory in my opinion. (It is different from ending up with a stroke that's been both moved and recolored, which would be the outcome in the "other" approach. That outcome is just impossible in this model; but I really don't think it's a serious problem). Perhaps this is what Andreas meant by "error management" (if so, sorry, I was slow in understanding). I think the conclusion here is that messages sent to the server should consist of "minimally combined deletion and creation operations", i.e. within a same message one sends the set of creation/deletion pairs that correspond to a same conceptual operation, with the understanding that if one part of the operation is invalid because it involves deleting an already-deleted stroke, then the whole operation should become invalid and ignored. In practice this means: if I try to modify a stroke that's already been nearly simultaneously modified by someone else, my operation will be rejected since the stroke I'm trying to delete doesn't exist anymore for the server. This is a very reasonable way to handle conflicts (less permissive than what I was initially proposing but much more realistic to implement). In effect, rather than handling out-of-order insertion of operations into the timeline of a stroke (where the stroke keeps existing in successive states over time, and we permit all sorts of conflicting operations on the stroke to be performed in any order), we end up with "ephemeral" strokes (that disappear as soon as they get modified), and we have to reject conflicting edits because the stroke no longer exists after its first modification. Of course this simplifies vastly the question of reordering operations, but it brings up the issue of cancelling rejected operations. (2) REAL-TIME LOCAL EDITING VS. NETWORK COMMUNICATION DELAYS: There's now the question of what the local instance should be doing while waiting for validation of a pending action from the server. The point is that the user needs to be able to keep working even if communication is very slow and delayed. In my initial proposal, the idea was that different operations on the same stroke can be carried out in different orders. In this proposal, since the first operation performed on the stroke actually destroys it and replaces it by a different stroke, we'll never perform a second operation on it. Thinking of instance B in the above scenario, here's what might happen in succession: 1. locally I perform a move operation, replacing stroke #1 by a new stroke, and send a message to the server to let everyone know. [I think the operation has to be performed locally -- waiting for confirmation is not realistic since it might take a while to arrive.] 2. I might have time to perform more operations involving my new stroke, still without hearing anything from the server (perhaps there's a network lag right now). My stroke needs to have an ID in order for me to be able to tell the server what I'm doing locally -- so indeed IDs have to be generated by the client, not by the server. (Sorry!) Let's say I give the new stroke of step 1 the ID number 3. I would then send the server more messages telling it that I'm further manipulating stroke #3 (i.e., destroying it and replacing it by yet new strokes). 3. I might learn from the server about other pending operations which don't obviously conflict with what I attempted in steps 1 and 2. Those should be processed, and if ordering matters, these operations happen *before* the ones I attempted in step 1 and 2. (But I think ordering might not matter.) 4. I might learn from the server (*before* hearing back about my attempted move operation from step 1, since the message might have already been in transit over the network before step 1 took place) that someone has erased stroke #1 and replaced it by stroke #2. Since stroke #1 is already deleted in my local copy, this is the first indication of trouble to come. What to do about it is not 100% clear-cut. I can find out easily in my local data structures that the operation which caused stroke #1 to be deleted is the operation from step 1 -- and I know it hasn't been validated by the server yet. This tells me right away that the server will likely reject my request of step 1. Since stroke #3 will never get created, the subsequent operations involving it will also get rejected. I'm heading into dangerous territory. So perhaps the only reasonable thing to do would be to just undo ALL the operations that I've tried to perform locally since the last one that was confirmed by the server -- or I can try to undo them all, then internally redo those that do not depend on stroke #1 or stroke #3 or other subsequently endangered strokes. At this point, I might even want to freeze my local user out of action until I know a bit better what's happening. This might even be the right time to bring up a "please wait while resolving editing conflict" dialog box (with a "cancel" button that would disconnect from the server and just continue in a local session, in case things break down too badly). Either that, or I can let the user continue working, but it might be really confusing if the user continues to work and sees more and more things get cancelled for no apparent reason. In any case, I need to perform the operation replacing stroke #1 by stroke #2, and perform it chronologically before any of the unresolved conflicts -- so I think that forces undoing the local operations of steps 1 and 2. 5. I learn subsequently from the server that the requests I made at step 1 is denied. Not surprising, and perfectly ok since I already undid it. Or: I might also learn that my request was denied without my having already figured it out at step 4. [Not sure! Can it actually happen? If the server handles requests in a single sequential order, then it shouldn't reject the request unless it already processed information that invalidated stroke #1. But one might want to handle this possibility in case it happens, for robustness.] In that case I again need to undo the operation of step 1, as well as that of step 2 since stroke #3 is no longer valid either upon undoing step 1. Or, more surprising: I might learn from the server that my request from step 1 is approved. [Can it actually happen? Most likely, YES because of undo. Point: the replacement of stroke #1 by stroke #2 could have been cancelled by now, due to an undo from user A. In this case stroke #1 exists again by the time the server processes my request from step 1, and has no reason to deny it.] What do I do then? Do I request a cancellation of the operation since I've already cancelled it locally (and let the server propagate the cancellation to everyone?) Or do I redo it locally, since the user wanted it to happen and the server is letting it happen? I think the correct behavior is to process the action again (i.e. replace stroke #1 by stroke #3, as the server is telling me to). *** One point here about undo: one could try to take the "deletion/creation" approach to everything including undo, so that undoing the operation (delete stroke #1, create stroke #2) is performing the operation (delete stroke #2, create stroke #4 = identical to #1 but with a new ID number). However this is not good, because next I might try to undo the creation of stroke #1, which refers to it by its ID number (1) that has now become invalid. So the undo should really re-create the stroke with the same ID it used to have, rather than a similar stroke with a different ID number. This can still be sent to the server as a (delete stroke #2, create stroke #1) message -- and the server could choose to reject it if either it thinks stroke #2 doesn't exist, or it thinks stroke #1 actually exists. In any case, the point is that, in the above sequence of steps, stroke #1 might disappear at step 4 but might still reappear before step 5, leading to the surprising situation of the previous paragraph where a local operation I was sure would be rejected is actually getting accepted. Sorry this is getting messy. Perhaps it's not the easiest thing either. This kind of sequential model with approval/rejection of conflicting edits feels a lot more "standard", and less strange, than my other proposal. I'm guessing that such a model might even be already implemented in some existing frameworks for collaborative software, i.e. one might not even need to implement everything from scratch. Ok, I'll stop now. Sorry for the long-winded messages, and congratulations if you read all the way to here. Denis On 04/19/2011 04:17 PM, Denis Auroux wrote: > There are two main types of operations in there: > > - some that create items (ITEM_STROKE, ITEM_PASTE, etc.); these are > *almost* commutative, the only difference if you switch them being which > stroke ends up on top of which -- important if you're detail-oriented > since the strokes are opaque, but very minor for most people. > > - operations which change the attributes of existing items, such as for > example ITEM_MOVESEL which moves a set of objects on the page or to a > different page, ITEM_REPAINTSEL which modifies the color/thickness of an > item, ITEM_ERASURE which replaces an existing stroke by the bits and > pieces that remain after erasing the relevant portions of it. These are > more dangerous to switch around. (You can certainly perform a MOVESEL > and a REPAINTSEL on the same items in any order, but two REPAINTSELs or > two MOVESELs of the same items can't be switched.) -- Denis Auroux MIT Department of Mathematics aur...@math.mit.edu (on leave) and University of California, Berkeley aur...@math.berkeley.edu Department of Mathematics Tel: 510-642-4367 817 Evans Hall # 3840 Fax: 510-642-8204 Berkeley, CA 94720-3840 ------------------------------------------------------------------------------ Benefiting from Server Virtualization: Beyond Initial Workload Consolidation -- Increasing the use of server virtualization is a top priority.Virtualization can reduce costs, simplify management, and improve application availability and disaster protection. Learn more about boosting the value of server virtualization. http://p.sf.net/sfu/vmware-sfdev2dev _______________________________________________ Xournal-devel mailing list Xournal-devel@lists.sourceforge.net https://lists.sourceforge.net/lists/listinfo/xournal-devel