Commit 7407f357 authored by Romain Courteaud's avatar Romain Courteaud

ReplicateStorage: add automatic conflict resolution

Configure the storage with the conflict_handling parameter value:
   0: (default): no resolution (ie, throw an Error)
   1: keep the local state
      (overwrites the remote document with local content)
      (delete remote document if local is deleted)
   2: keep the remote state
      (overwrites the local document with remote content)
      (delete local document if remote is deleted)
   3: keep both copies (leave documents untouched, no signature update)
parent 5163b15b
......@@ -22,7 +22,11 @@
(function (jIO, RSVP, Rusha) {
"use strict";
var rusha = new Rusha();
var rusha = new Rusha(),
CONFLICT_THROW = 0,
CONFLICT_KEEP_LOCAL = 1,
CONFLICT_KEEP_REMOTE = 2,
CONFLICT_CONTINUE = 3;
/****************************************************
Use a local jIO to read/write/search documents
......@@ -53,6 +57,24 @@
});
this._use_remote_post = spec.use_remote_post || false;
this._conflict_handling = spec.conflict_handling || 0;
// 0: no resolution (ie, throw an Error)
// 1: keep the local state
// (overwrites the remote document with local content)
// (delete remote document if local is deleted)
// 2: keep the remote state
// (overwrites the local document with remote content)
// (delete local document if remote is deleted)
// 3: keep both copies (leave documents untouched, no signature update)
if ((this._conflict_handling !== CONFLICT_THROW) &&
(this._conflict_handling !== CONFLICT_KEEP_LOCAL) &&
(this._conflict_handling !== CONFLICT_KEEP_REMOTE) &&
(this._conflict_handling !== CONFLICT_CONTINUE)) {
throw new jIO.util.jIOError("Unsupported conflict handling: " +
this._conflict_handling, 400);
}
this._check_local_modification = spec.check_local_modification;
if (this._check_local_modification === undefined) {
this._check_local_modification = true;
......@@ -208,6 +230,13 @@
skip_document_dict[id] = null;
});
}
if (options.conflict_ignore === true) {
return;
}
if (options.conflict_force === true) {
return propagateModification(source, destination, doc, local_hash,
id, options);
}
// Already exists on destination
throw new jIO.util.jIOError("Conflict on '" + id + "'",
409);
......@@ -279,7 +308,8 @@
});
}
function checkSignatureDifference(queue, source, destination, id) {
function checkSignatureDifference(queue, source, destination, id,
conflict_force, conflict_ignore) {
queue
.push(function () {
return RSVP.all([
......@@ -308,8 +338,13 @@
skip_document_dict[id] = null;
});
}
throw new jIO.util.jIOError("Conflict on '" + id + "'",
409);
if (conflict_ignore === true) {
return;
}
if (conflict_force !== true) {
throw new jIO.util.jIOError("Conflict on '" + id + "'",
409);
}
}
return propagateModification(source, destination, doc,
local_hash, id);
......@@ -384,7 +419,9 @@
if (signature_dict.hasOwnProperty(key)) {
if (local_dict.hasOwnProperty(key)) {
if (options.check_modification === true) {
checkSignatureDifference(queue, source, destination, key);
checkSignatureDifference(queue, source, destination, key,
options.conflict_force,
options.conflict_ignore);
}
} else {
if (options.check_deletion === true) {
......@@ -440,6 +477,12 @@
context._remote_sub_storage,
{
use_post: context._use_remote_post,
conflict_force: (context._conflict_handling ===
CONFLICT_KEEP_LOCAL),
conflict_ignore: ((context._conflict_handling ===
CONFLICT_CONTINUE) ||
(context._conflict_handling ===
CONFLICT_KEEP_REMOTE)),
check_modification: context._check_local_modification,
check_creation: context._check_local_creation,
check_deletion: context._check_local_deletion
......@@ -464,6 +507,10 @@
return pushStorage(context._remote_sub_storage,
context._local_sub_storage, {
use_bulk_get: use_bulk_get,
conflict_force: (context._conflict_handling ===
CONFLICT_KEEP_REMOTE),
conflict_ignore: (context._conflict_handling ===
CONFLICT_CONTINUE),
check_modification: context._check_remote_modification,
check_creation: context._check_remote_creation,
check_deletion: context._check_remote_deletion
......
......@@ -46,6 +46,7 @@
deepEqual(jio.__storage._query_options, {});
equal(jio.__storage._use_remote_post, false);
equal(jio.__storage._conflict_handling, 0);
equal(jio.__storage._check_local_creation, true);
equal(jio.__storage._check_local_deletion, true);
equal(jio.__storage._check_local_modification, true);
......@@ -80,6 +81,7 @@
},
query: {query: 'portal_type: "Foo"', limit: [0, 1234567890]},
use_remote_post: true,
conflict_handling: 3,
check_local_creation: false,
check_local_deletion: false,
check_local_modification: false,
......@@ -93,6 +95,7 @@
{query: 'portal_type: "Foo"', limit: [0, 1234567890]}
);
equal(jio.__storage._use_remote_post, true);
equal(jio.__storage._conflict_handling, 3);
equal(jio.__storage._check_local_creation, false);
equal(jio.__storage._check_local_deletion, false);
equal(jio.__storage._check_local_modification, false);
......@@ -104,6 +107,31 @@
"_replicate_623653d45a4e770a2c9f6b71e3144d18ee1b5bec");
});
test("reject unknow conflict resolution", function () {
throws(
function () {
jIO.createJIO({
type: "replicate",
local_sub_storage: {
type: "replicatestorage200"
},
remote_sub_storage: {
type: "replicatestorage500"
},
query: {query: 'portal_type: "Foo"', limit: [0, 1234567890]},
conflict_handling: 4
});
},
function (error) {
ok(error instanceof jIO.util.jIOError);
equal(error.status_code, 400);
equal(error.message,
"Unsupported conflict handling: 4");
return true;
}
);
});
/////////////////////////////////////////////////////////////////
// replicateStorage.get
/////////////////////////////////////////////////////////////////
......@@ -847,6 +875,195 @@
});
});
test("local and remote document creations: keep local", function () {
stop();
expect(3);
var context = this;
this.jio = jIO.createJIO({
type: "replicate",
local_sub_storage: {
type: "uuid",
sub_storage: {
type: "memory"
}
},
remote_sub_storage: {
type: "uuid",
sub_storage: {
type: "memory"
}
},
conflict_handling: 1
});
RSVP.all([
context.jio.put("conflict", {"title": "foo"}),
context.jio.__storage._remote_sub_storage.put("conflict",
{"title": "bar"})
])
.then(function () {
return context.jio.repair();
})
.then(function () {
return context.jio.__storage._signature_sub_storage.get("conflict");
})
.then(function (result) {
deepEqual(result, {
hash: "5ea9013447539ad65de308cbd75b5826a2ae30e5"
});
})
.then(function () {
return context.jio.get("conflict");
})
.then(function (result) {
deepEqual(result, {
title: "foo"
});
})
.then(function () {
return context.jio.__storage._remote_sub_storage.get("conflict");
})
.then(function (result) {
deepEqual(result, {
title: "foo"
});
})
.fail(function (error) {
ok(false, error);
})
.always(function () {
start();
});
});
test("local and remote document creations: keep remote", function () {
stop();
expect(3);
var context = this;
this.jio = jIO.createJIO({
type: "replicate",
local_sub_storage: {
type: "uuid",
sub_storage: {
type: "memory"
}
},
remote_sub_storage: {
type: "uuid",
sub_storage: {
type: "memory"
}
},
conflict_handling: 2
});
RSVP.all([
context.jio.put("conflict", {"title": "foo"}),
context.jio.__storage._remote_sub_storage.put("conflict",
{"title": "bar"})
])
.then(function () {
return context.jio.repair();
})
.then(function () {
return context.jio.__storage._signature_sub_storage.get("conflict");
})
.then(function (result) {
deepEqual(result, {
hash: "6799f3ea80e325b89f19589282a343c376c1f1af"
});
})
.then(function () {
return context.jio.get("conflict");
})
.then(function (result) {
deepEqual(result, {
title: "bar"
});
})
.then(function () {
return context.jio.__storage._remote_sub_storage.get("conflict");
})
.then(function (result) {
deepEqual(result, {
title: "bar"
});
})
.fail(function (error) {
ok(false, error);
})
.always(function () {
start();
});
});
test("local and remote document creations: continue", function () {
stop();
expect(4);
var context = this;
this.jio = jIO.createJIO({
type: "replicate",
local_sub_storage: {
type: "uuid",
sub_storage: {
type: "memory"
}
},
remote_sub_storage: {
type: "uuid",
sub_storage: {
type: "memory"
}
},
conflict_handling: 3
});
RSVP.all([
context.jio.put("conflict", {"title": "foo"}),
context.jio.__storage._remote_sub_storage.put("conflict",
{"title": "bar"})
])
.then(function () {
return context.jio.repair();
})
.then(function () {
return context.jio.__storage._signature_sub_storage.get("conflict");
})
.fail(function (error) {
ok(error instanceof jIO.util.jIOError);
// equal(error.message, "Cannot find document: conflict");
equal(error.status_code, 404);
})
.then(function () {
return context.jio.get("conflict");
})
.then(function (result) {
deepEqual(result, {
title: "foo"
});
})
.then(function () {
return context.jio.__storage._remote_sub_storage.get("conflict");
})
.then(function (result) {
deepEqual(result, {
title: "bar"
});
})
.fail(function (error) {
ok(false, error);
})
.always(function () {
start();
});
});
test("local and remote same document creations", function () {
stop();
expect(1);
......@@ -1182,6 +1399,216 @@
});
});
test("local and remote document modifications: keep local", function () {
stop();
expect(3);
var id,
context = this;
this.jio = jIO.createJIO({
type: "replicate",
local_sub_storage: {
type: "uuid",
sub_storage: {
type: "memory"
}
},
remote_sub_storage: {
type: "uuid",
sub_storage: {
type: "memory"
}
},
conflict_handling: 1
});
context.jio.post({"title": "foo"})
.then(function (result) {
id = result;
return context.jio.repair();
})
.then(function () {
return RSVP.all([
context.jio.put(id, {"title": "foo4"}),
context.jio.__storage._remote_sub_storage.put(id, {"title": "foo5"})
]);
})
.then(function () {
return context.jio.repair();
})
.then(function () {
return context.jio.__storage._signature_sub_storage.get(id);
})
.then(function (result) {
deepEqual(result, {
hash: "6f700e813022233a785692585484c21cb5a412fd"
});
})
.then(function () {
return context.jio.get(id);
})
.then(function (result) {
deepEqual(result, {
title: "foo4"
});
})
.then(function () {
return context.jio.__storage._remote_sub_storage.get(id);
})
.then(function (result) {
deepEqual(result, {
title: "foo4"
});
})
.fail(function (error) {
ok(false, error);
})
.always(function () {
start();
});
});
test("local and remote document modifications: keep remote", function () {
stop();
expect(3);
var id,
context = this;
this.jio = jIO.createJIO({
type: "replicate",
local_sub_storage: {
type: "uuid",
sub_storage: {
type: "memory"
}
},
remote_sub_storage: {
type: "uuid",
sub_storage: {
type: "memory"
}
},
conflict_handling: 2
});
context.jio.post({"title": "foo"})
.then(function (result) {
id = result;
return context.jio.repair();
})
.then(function () {
return RSVP.all([
context.jio.put(id, {"title": "foo4"}),
context.jio.__storage._remote_sub_storage.put(id, {"title": "foo5"})
]);
})
.then(function () {
return context.jio.repair();
})
.then(function () {
return context.jio.__storage._signature_sub_storage.get(id);
})
.then(function (result) {
deepEqual(result, {
hash: "7bea6f87fd1dda14e340e5b14836cc8578fd615f"
});
})
.then(function () {
return context.jio.get(id);
})
.then(function (result) {
deepEqual(result, {
title: "foo5"
});
})
.then(function () {
return context.jio.__storage._remote_sub_storage.get(id);
})
.then(function (result) {
deepEqual(result, {
title: "foo5"
});
})
.fail(function (error) {
ok(false, error);
})
.always(function () {
start();
});
});
test("local and remote document modifications: continue", function () {
stop();
expect(3);
var id,
context = this;
this.jio = jIO.createJIO({
type: "replicate",
local_sub_storage: {
type: "uuid",
sub_storage: {
type: "memory"
}
},
remote_sub_storage: {
type: "uuid",
sub_storage: {
type: "memory"
}
},
conflict_handling: 3
});
context.jio.post({"title": "foo"})
.then(function (result) {
id = result;
return context.jio.repair();
})
.then(function () {
return RSVP.all([
context.jio.put(id, {"title": "foo4"}),
context.jio.__storage._remote_sub_storage.put(id, {"title": "foo5"})
]);
})
.then(function () {
return context.jio.repair();
})
.then(function () {
return context.jio.__storage._signature_sub_storage.get(id);
})
.then(function (result) {
deepEqual(result, {
hash: "5ea9013447539ad65de308cbd75b5826a2ae30e5"
});
})
.then(function () {
return context.jio.get(id);
})
.then(function (result) {
deepEqual(result, {
title: "foo4"
});
})
.then(function () {
return context.jio.__storage._remote_sub_storage.get(id);
})
.then(function (result) {
deepEqual(result, {
title: "foo5"
});
})
.fail(function (error) {
ok(false, error);
})
.always(function () {
start();
});
});
test("local and remote document same modifications", function () {
stop();
expect(1);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment