Jelajahi Sumber

Delete/delete, create/create, update/update conflicts

Joeri Exelmans 3 tahun lalu
induk
melakukan
5ef01b6e73
2 mengubah file dengan 632 tambahan dan 0 penghapusan
  1. 154 0
      src/onion/micro_op.test.ts
  2. 478 0
      src/onion/micro_op.ts

+ 154 - 0
src/onion/micro_op.test.ts

@@ -0,0 +1,154 @@
+import {
+  UUID,
+} from './types';
+
+import {
+  NodeCreation,
+  NodeDeletion,
+  EdgeCreation,
+  EdgeUpdate,
+  // EdgeDeletion,
+  // DeltaGraph,
+} from "./micro_op";
+
+let nextId = 0;
+function getId(): UUID {
+  return new UUID(nextId++);
+}
+
+function assert(expression: boolean, msg: string) {
+  if (!expression) {
+    throw new Error(msg);
+  }
+}
+
+describe("Delta", () => {
+
+  it("Delete/delete node conflict", () => {
+    const creation = new NodeCreation(getId());
+    assert(creation.getConflicts().length === 0, "did not expect node creation to be conflicting with any other operation");
+
+    const deletion1 = new NodeDeletion(creation);
+    assert(deletion1.getConflicts().length === 0, "did not expect first deletion alone to be conflicting with anything");
+
+    const deletion2 = new NodeDeletion(creation);
+    assert(deletion1.getConflicts().length === 1, "expected second deletion to be conflicting with first deletion");
+    assert(deletion2.getConflicts().length === 1, "expected second deletion to be conflicting with first deletion");
+  });
+
+  it("Create/create edge conflict", () => {
+    const sourceCreation = new NodeCreation(getId());
+    const target1Creation = new NodeCreation(getId());
+    const target2Creation = new NodeCreation(getId());
+
+    const edge1Creation = new EdgeCreation(sourceCreation, "label", target1Creation);
+    assert(edge1Creation.getConflicts().length === 0, "expected a single edge alone to not be involved in conflicts");
+
+    const edge2Creation = new EdgeCreation(sourceCreation, "label", target2Creation);
+    assert(edge1Creation.getConflicts().length === 1, "expected conflict: same edge created twice, concurrently")
+    assert(edge2Creation.getConflicts().length === 1, "expected conflict: same edge created twice, concurrently")
+  });
+
+  it("Update/update edge conflict", () => {
+    const sourceCreation = new NodeCreation(getId());
+    const targetCreation = new NodeCreation(getId());
+    const newTarget1Creation = new NodeCreation(getId());
+    const newTarget2Creation = new NodeCreation(getId());
+
+    const edgeCreation = new EdgeCreation(sourceCreation, "label", targetCreation);
+
+    const update1 = new EdgeUpdate(edgeCreation, newTarget1Creation);
+    assert(update1.getConflicts().length === 0, "expected no conflict with a single edge update.")
+
+    const update2 = new EdgeUpdate(edgeCreation, newTarget2Creation);
+    assert(update1.getConflicts().length === 1, "expected conflict between concurrent edge updates.")
+    assert(update2.getConflicts().length === 1, "expected conflict between concurrent edge updates.")
+  });
+
+  it("Delete/require (edge source) conflict", () => {
+    const sourceCreation = new NodeCreation(getId());
+    const targetCreation = new NodeCreation(getId());
+
+    const sourceDeletion = new NodeDeletion(sourceCreation);
+    assert(sourceDeletion.getConflicts().length === 0, "expected no conflicts so far");
+
+    const edgeCreation = new EdgeCreation(sourceCreation, "label", targetCreation);
+    assert(edgeCreation.getConflicts().length === 1, "expected require/delete conflict, because edge source is concurrently deleted");
+    assert(sourceDeletion.getConflicts().length === 1, "expected require/delete conflict, because edge source is concurrently deleted");
+  })
+
+  // it("Delete/delete conflict", () => {
+  //   const deltaGraph = new DeltaGraph();
+
+  //   const nodeCreation = deltaGraph.newNodeCreation(getId());
+
+  //   (() => {
+  //     const [_, delDel, delReq] = deltaGraph.newNodeDeletion(nodeCreation, []);
+  //     assert(delDel.length === 0, "did not expect DEL/DEL conflict");
+  //     assert(delReq.length === 0, "did not expect DEL/REQ conflict");
+  //   })();
+
+  //   (() => {
+  //     const [_, delDel, delReq] = deltaGraph.newNodeDeletion(nodeCreation, []);
+  //     assert(delDel.length === 1, "expected DEL/DEL conflict");
+  //     assert(delReq.length === 0, "did not expect DEL/REQ conflict");
+  //   })();
+  // });
+
+  // it("Delete/require conflict", () => {
+  //   const deltaGraph = new DeltaGraph();
+
+  //   const sourceCreation = deltaGraph.newNodeCreation(getId());
+  //   const targetCreation = deltaGraph.newNodeCreation(getId());
+
+  //   const [edgeCreation, edgeCreate, delReq] = deltaGraph.newEdgeCreation(sourceCreation, "label", targetCreation);
+  //   assert(edgeCreate.length === 0, "did not expect edge creation conflict");
+  //   assert(delReq.length === 0, "did not expect DEL/REQ conflict");
+
+  //   // Source and target are deleted, unaware of the fact that there was an edge
+  //   // This results in a DEL/REQ conflict
+  //   (() => {
+  //     const [_, delDel, delReq] = deltaGraph.newNodeDeletion(sourceCreation, []);
+  //     assert(delDel.length === 0, "did not expect DEL/DEL conflict");
+  //     assert(delReq.length === 1, "expected DEL/REQ conflict");
+  //   })();
+
+  //   (() => {
+  //     const [_, delDel, delReq] = deltaGraph.newNodeDeletion(targetCreation, []);
+  //     assert(delDel.length === 0, "did not expect DEL/DEL conflict");
+  //     assert(delReq.length === 1, "expected DEL/REQ conflict");
+  //   })();
+
+  //   // Part two - edge is deleted
+  //   const [edgeDeletion, edgeConflicts] = deltaGraph.newEdgeDeletion(edgeCreation);
+  //   assert(edgeConflicts.length === 0, "did not expect edge update conflicts");
+
+  //   // Deletions that depend on the edge deletion do not cause a DEL/REQ conflict
+  //   (() => {
+  //     const [_, delDel, delReq] = deltaGraph.newNodeDeletion(sourceCreation, [edgeDeletion]);
+  //     assert(delDel.length === 1, "expected DEL/DEL conflict");
+  //     assert(delReq.length === 0, "did not expect DEL/REQ conflict");
+  //   })();
+
+  //   (() => {
+  //     const [_, delDel, delReq] = deltaGraph.newNodeDeletion(targetCreation, [edgeDeletion]);
+  //     assert(delDel.length === 1, "expected DEL/DEL conflict");
+  //     assert(delReq.length === 0, "did not expect DEL/REQ conflict");
+  //   })();
+  // });
+
+  // it("Delete/require conflict (2)", () => {
+  //   const deltaGraph = new DeltaGraph();
+
+  //   const sourceCreation = deltaGraph.newNodeCreation(getId());
+  //   const targetCreation = deltaGraph.newNodeCreation(getId());
+
+  //   const sourceDeletion = deltaGraph.newNodeDeletion(sourceCreation, []);
+  //   const targetDeletion = deltaGraph.newNodeDeletion(sourceCreation, []);
+
+  //   // Edge creation is unaware that source and target are concurrently being deleted.
+  //   const [edgeCreation, edgeCreate, delReq] = deltaGraph.newEdgeCreation(sourceCreation, "label", targetCreation);
+  //   assert(delReq.length === 2, "expected 2 DEL/REQ conflicts");
+  // });
+
+});

+ 478 - 0
src/onion/micro_op.ts

@@ -0,0 +1,478 @@
+import {UUID} from "./types";
+
+import {
+  Delta,
+} from "./delta";
+
+export class NodeCreation implements Delta {
+  readonly id: UUID;
+
+  // Inverse dependency: Deletions of this node.
+  deletions: Array<NodeDeletion> = []; // append-only
+
+  // Inverse dependency: Creation outgoing edges.
+  outgoingEdges: Array<EdgeCreation> = []; // append-only
+
+  constructor(id: UUID) {
+    this.id = id;
+  }
+
+  getDependencies(): [] {
+    return [];
+  }
+
+  getConflicts(): [] {
+    return [];
+  }
+}
+
+export class NodeDeletion implements Delta {
+  // Dependency: The node being deleted.
+  readonly creation: NodeCreation;
+
+  // Dependency: Deletion of a node depends on deletion of its incoming and outgoing edges.
+  // readonly deletedEdges: Array<EdgeUpdate2>;
+
+  // Conflicts: Concurrent deletion of the same node.
+  deleteConflicts: Array<NodeDeletion> = [];
+
+  // Conflicts: Concurrent creation of an edge with as source the deleted node.
+  // requireConflicts: Array<EdgeUpdate2>;
+
+  constructor(creation: NodeCreation/*, deletedEdges: Array<EdgeDeletion>*/) {
+    this.creation = creation;
+    // this.deletedEdges = deletedEdges;
+
+    // Detect conflicts
+    for (const concurrentDeletion of this.creation.deletions) {
+      // Symmetric:
+      this.deleteConflicts.push(concurrentDeletion);
+      concurrentDeletion.deleteConflicts.push(this);
+    }
+    // this.requireConflicts = [];
+    // for (const edgeCreation of this.creation.edges) {
+    //   // Incoming or outgoing edge
+    //   if (! this.deletedEdges.some(del => del.getCreation() === edgeCreation)) {
+    //     // Symmetric:
+    //     this.requireConflicts.push(edgeCreation);
+    //     edgeCreation.deleteConflicts.push(this);
+    //   }
+    // }
+
+    // Create inverse dependency
+    this.creation.deletions.push(this);
+  }
+
+  getDependencies(): [NodeCreation] {
+    return [this.creation];
+  }
+
+  getConflicts(): Array<Delta> {
+    // return [].concat(
+    //   this.deleteConflicts,
+    //   this.requireConflicts,
+    // );
+    return this.deleteConflicts;
+  }
+}
+
+export class EdgeCreation implements Delta {
+  // Dependencies
+  readonly source: NodeCreation;
+  readonly label: string;
+  readonly target: NodeCreation;
+
+  // Inverse dependency
+  // NodeDeletion if source of edge is deleted.
+  overwrittenBy: Array<EdgeUpdate | NodeDeletion> = []; // append-only
+
+  // Conflicts: Concurrent creations of the same edge.
+  createConflicts: Array<EdgeCreation> = []; // append-only
+
+  constructor(source: NodeCreation, label: string, target: NodeCreation) {
+    this.source = source;
+    this.label = label;
+    this.target = target;
+
+    // Detect conflicts
+    for (const outgoingEdge of this.source.outgoingEdges) {
+      if (outgoingEdge.label === this.label) {
+        // Symmetric:
+        this.createConflicts.push(outgoingEdge);
+        outgoingEdge.createConflicts.push(this);
+      }
+    }
+
+    // Create inverse dependency
+    this.source.outgoingEdges.push(this);
+  }
+
+  getDependencies(): [NodeCreation, NodeCreation] {
+    return [this.source, this.target];
+  }
+
+  getConflicts(): Array<EdgeCreation> {
+    return this.createConflicts;
+  }
+}
+
+export class EdgeUpdate implements Delta {
+  // Dependencies
+  readonly overwrites: EdgeCreation | EdgeUpdate;
+  readonly newTarget: NodeCreation;
+
+  // Inverse dependency
+  // NodeDeletion if source of edge is deleted.
+  overwrittenBy: Array<EdgeUpdate | NodeDeletion> = []; // append-only
+
+  // Conflicts: Concurrent updates
+  updateConflicts: Array<EdgeUpdate> = []; // append-only
+
+  constructor(overwrites: EdgeCreation | EdgeUpdate, newTarget: NodeCreation) {
+    this.overwrites = overwrites;
+    this.newTarget = newTarget;
+
+    // Detect conflicts
+    for (const concurrentUpdate of this.overwrites.overwrittenBy) {
+      // Symmetric
+      this.updateConflicts.push(concurrentUpdate);
+      concurrentUpdate.updateConflicts.push(this);
+    }
+
+    // Create inverse dependency
+    this.overwrites.overwrittenBy.push(this);
+  }
+
+  getDependencies(): Array<Delta> {
+    return [this.overwrites, this.newTarget];
+  }
+
+  getConflicts(): Array<EdgeUpdate> {
+    return this.updateConflicts;
+  }
+}
+
+
+
+
+// export class EdgeUpdate {
+//   // Dependency: The (new) target node
+//   readonly newTarget: NodeCreation;
+
+//   // Dependency: The previous update of the same edge
+//   readonly overwrites: EdgeUpdate | null;
+
+//   constructor(newTarget: NodeCreation, overwrites: EdgeUpdate | null) {
+//     this.newTarget = newTarget;
+//     this.overwrites = overwrites;
+//   }
+// }
+
+
+
+// abstract class EdgeCreationOrUpdate {
+//   // Dependency: (new) target node
+
+//   // Inverse dependency: Next operation on the edge
+//   nextOperations: Array<EdgeUpdate | EdgeDeletion>; // append-only
+
+//   constructor(target: NodeCreation) {
+//     this.target = target;
+
+//     this.nextOperations = [];
+//   }
+
+//   abstract getCreation(): EdgeCreation;
+// }
+
+// export class EdgeCreation implements Delta {
+//   // Dependencies
+//   readonly source: NodeCreation;
+//   readonly label: string;
+
+//   // Conflicts: Concurrent creation of the same edge.
+//   createConflicts: Array<EdgeCreation>;
+
+//   // Conflicts: Concurrent deletion of source or target of this edge.
+//   sourceDeleteConflicts: Array<NodeDeletion>;
+
+//   // Inverse dependency
+//   overwrittenBy: Array<EdgeUpdate2>;
+
+//   constructor(source: NodeCreation, label: string) {
+//     super(target);
+
+//     this.source = source;
+//     this.label = label;
+
+//     // Detect conflicts
+//     for (const concurrentCreation of this.source.edges) {
+//       if (concurrentCreation.label === this.label) {
+//         this.createConflicts.push(concurrentCreation);
+//         concurrentCreation.createConflicts.push(this);
+//       }
+//     }
+//     const detectDeleteConflicts = nodeCreation => {
+//       for (const concurrentDeletion of nodeCreation.deletions) {
+//         if (!concurrentDeletion.some(del => del.getCreation() === this)) {
+//           this.sourceDeleteConflicts.push(concurrentDeletion);
+//           concurrentDeletion.requireConflicts.push(this);
+//         }
+//       }
+//     }
+//     detectDeleteConflicts(this.source);
+
+//     // Create inverse dependency
+//     this.source.sourceEdges.push(this);
+//   }
+
+//   getCreation(): EdgeCreation {
+//     return this;
+//   }
+
+//   getDependencies(): [NodeCreation, NodeCreation] {
+//     return [this.source, this.target];
+//   }
+
+//   getConflicts(): Array<Delta> {
+//     return [].concat(
+//       this.createConflicts,
+//       this.sourceDeleteConflicts,
+//     );
+//   }
+// }
+
+// export class EdgeUpdate2 implements Delta {
+//   // dependencies
+//   readonly overwrites: EdgeCreation | EdgeUpdate2;
+//   readonly newTarget: NodeCreation | null; // null if edge is deleted.
+
+//   // inverse dependency: edge target is updated, or target 
+//   overwrittenBy: Array<EdgeUpdate2 | NodeDeletion>;
+
+//   // Conflicts: concurrent updates of the same edge
+//   updateConflicts: Array<EdgeUpdate2>;
+
+//   // Conflicts: concurrent delete of target
+//   targetDeleteConflicts: Array<NodeDeletion>;
+
+//   constructor(overwrites: EdgeCreation | EdgeUpdate2, newTarget: NodeCreation | null) {
+//     this.overwrites = overwrites;
+//     this.newTarget = newTarget;
+
+//     this.overwrittenBy = [];
+
+//     for (const concurrentUpdate of this.overwrites.overwrittenBy) {
+//       // Symmetric:
+//       this.updateConflicts.push(concurrentUpdate);
+//       concurrentUpdate.updateConflicts.push(this);
+//     }
+//     if (newTarget !== null) {
+//       for (const concurrentTargetDeletion of this.newTarget.deletions) {
+//         if (concurrentTargetDeletion.)
+//       }
+
+//     }
+
+//     // create inverse dependency
+//     this.overwrites.overwrittenBy.push(this);
+
+//     // create inverse dependency
+//     this.newTarget.targetEdges.push(this);
+//   }
+
+//   getCreation(): EdgeCreation {
+//     let overwrites = this.overwrites;
+//     while (true) {
+//       if (overwrites instanceof EdgeCreation) {
+//         return this.overwrites;
+//       }
+//       overwrites = overwrites.overwrites;
+//     }
+//   }
+
+//   getDependencies(): Array<Delta> {
+//     this.newTarget !== null ? [this.overwrites, this.newTarget] : [this.overwrites];
+//   }
+
+//   getConflicts(): Array<Delta> {
+//     return this.updateConflicts;
+//   }
+// }
+
+
+// export class EdgeUpdate extends EdgeCreationOrUpdate implements EdgeOperation {
+//   readonly edge: EdgeCreation | EdgeUpdate; // UPD-dependency
+
+//   constructor(edge: EdgeCreation | EdgeUpdate, newTarget: NodeCreation) {
+//     super(newTarget);
+
+//     this.edge = edge;
+
+//     // Create inverse dependency
+//     this.edge.nextOperations.push(this);
+//   }
+
+//   getCreation(): EdgeCreation {
+//     return this.edge.getCreation();
+//   }
+
+//   getDependencies(): [EdgeCreation | EdgeUpdate] {
+//     return [this.edge];
+//   }
+// }
+
+// export class EdgeDeletion implements EdgeOperation {
+//   readonly edge: EdgeCreation | EdgeUpdate; // UPD-dependency
+
+//   constructor(edge: EdgeCreation | EdgeUpdate) {
+//     this.edge = edge;
+
+//     // Create inverse dependency
+//     this.edge.nextOperations.push(this);
+//   }
+
+//   getCreation(): EdgeCreation {
+//     return this.edge.getCreation();
+//   }
+
+//   getDependencies(): [EdgeCreation | EdgeUpdate] {
+//     return [this.edge];
+//   }
+// }
+
+// // CONFLICT TYPES //
+
+// // Two concurrent deletes of the same node
+// export class DeleteDeleteConflict implements Conflict {
+//   readonly del0: NodeDeletion;
+//   readonly del1: NodeDeletion;
+
+//   constructor(del0: NodeDeletion, del1: NodeDeletion) {
+//     this.del0 = del0;
+//     this.del1 = del1;
+//   }
+
+//   first(): MicroOp {
+//     return this.del0;
+//   }
+//   second(): MicroOp {
+//     return this.del1;
+//   }
+// }
+
+// // When a node is deleted, and concurrently an edge from/to that node is created.
+// export class DeleteRequireConflict implements Conflict {
+//   readonly del: NodeDeletion;
+//   readonly edge: EdgeCreation;
+
+//   constructor(del: NodeDeletion, edge: EdgeCreation) {
+//     this.del = del;
+//     this.edge = edge;
+//   }
+
+//   first(): MicroOp {
+//     return this.del;
+//   }
+//   second(): MicroOp {
+//     return this.edge;
+//   }
+// }
+
+// // Two edges with the same source and label concurrently created
+// export class EdgeCreateConflict implements Conflict {
+//   readonly edge0: EdgeCreation;
+//   readonly edge1: EdgeCreation;
+
+//   constructor(edge0: EdgeCreation, edge1: EdgeCreation) {
+//     this.edge0 = edge0;
+//     this.edge1 = edge1;
+//   }
+
+//   first(): MicroOp {
+//     return this.edge0;
+//   }
+//   second(): MicroOp {
+//     return this.edge1;
+//   }
+// }
+
+// // An edge is concurrently updated or deleted two times.
+// export class EdgeUpdateConflict implements Conflict {
+//   readonly upd0: EdgeUpdateOrDeletion;
+//   readonly upd1: EdgeUpdateOrDeletion;
+
+//   constructor(upd0: EdgeUpdateOrDeletion, upd1: EdgeUpdateOrDeletion) {
+//     this.upd0 = upd0;
+//     this.upd1 = upd1;
+//   }
+
+//   first(): MicroOp {
+//     return upd0;
+//   }
+//   second(): MicroOp {
+//     return upd1;
+//   }
+// }
+
+
+// export class MicroOpGraph {
+//   conflictRegistry: ConflictRegistry
+
+//   constructor() {
+//     this.conflictRegistry = new ConflictRegistry();
+//   }
+
+//   newNodeCreation(id: UUID): NodeCreation {
+//     return new NodeCreation(id);
+//   }
+
+//   // Besides the operation object, also returns newly created conflicts
+//   newNodeDeletion(nodeCreation: NodeCreation, edgeDeletions: Array<EdgeDeletion>): [NodeDeletion, DeleteDeleteConflict[], DeleteRequireConflict[]] {
+//     const nodeDeletion = new NodeDeletion(nodeCreation, edgeDeletions);
+//     const deleteDeleteConflicts = nodeCreation.deletions
+//       .filter(nodeDeletion2 => nodeDeletion2 !== nodeDeletion)
+//       .map(nodeDeletion2 => new DeleteDeleteConflict(nodeDeletion, nodeDeletion2));
+//     const deleteRequireConflicts = nodeCreation.edges
+//       .filter(edge => !edgeDeletions.some(edgeDeletion => edge === edgeDeletion.getCreation()))
+//       .map(edge => new DeleteRequireConflict(nodeDeletion, edge));
+
+//     deleteDeleteConflicts.forEach(c => this.conflictRegistry.registerConflict(c));
+//     deleteRequireConflicts.forEach(c => this.conflictRegistry.registerConflict(c));
+
+//     return [nodeDeletion, deleteDeleteConflicts, deleteRequireConflicts];
+//   }
+
+//   // Besides the operation object, also returns newly created conflicts
+//   newEdgeCreation(source: NodeCreation, label: string, target: NodeCreation): [EdgeCreation, EdgeCreateConflict[], DeleteRequireConflict[]] {
+//     const edgeCreation = new EdgeCreation(source, label, target);
+//     const edgeCreationConflicts = source.edges
+//       .filter(edgeCreation2 => edgeCreation2 !== edgeCreation && edgeCreation2.label === label)
+//       .map(edgeCreation2 => new EdgeCreateConflict(edgeCreation, edgeCreation2));
+//     function getDeleteRequireConflicts(nodeCreation) {
+//       return nodeCreation.deletions
+//         .filter(nodeDeletion => !nodeDeletion.edgeDeletions.some(edgeDeletion => edgeCreation === edgeDeletion.getCreation()))
+//         .map(nodeDeletion => new DeleteRequireConflict(nodeDeletion, edgeCreation))
+//     }
+//     const deleteRequireConflicts = getDeleteRequireConflicts(source).concat(getDeleteRequireConflicts(target));
+
+//     edgeCreationConflicts.forEach(c => this.conflictRegistry.registerConflict(c));
+//     deleteRequireConflicts.forEach(c => this.conflictRegistry.registerConflict(c));
+
+//     return [edgeCreation, edgeCreationConflicts, deleteRequireConflicts];
+//   }
+
+//   // newEdgeUpdate(edge: EdgeCreationOrUpdate):
+
+//   // Besides the operation object, also returns newly created conflicts
+//   newEdgeDeletion(edge: EdgeCreationOrUpdate): [EdgeDeletion, EdgeUpdateConflict[]] {
+//     const edgeDeletion = new EdgeDeletion(edge);
+//     const edgeUpdateConflicts = edge.nextOperations
+//       .filter(op => op !== edgeDeletion)
+//       .map(op => new EdgeUpdateConflict(edgeDeletion, op));
+
+//     edgeUpdateConflicts.forEach(c => this.conflictRegistry.registerConflict(c));
+
+//     return [edgeDeletion, edgeUpdateConflicts];
+//   }
+// }