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 @@ ...@@ -22,7 +22,11 @@
(function (jIO, RSVP, Rusha) { (function (jIO, RSVP, Rusha) {
"use strict"; "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 Use a local jIO to read/write/search documents
...@@ -53,6 +57,24 @@ ...@@ -53,6 +57,24 @@
}); });
this._use_remote_post = spec.use_remote_post || false; 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; this._check_local_modification = spec.check_local_modification;
if (this._check_local_modification === undefined) { if (this._check_local_modification === undefined) {
this._check_local_modification = true; this._check_local_modification = true;
...@@ -208,6 +230,13 @@ ...@@ -208,6 +230,13 @@
skip_document_dict[id] = null; 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 // Already exists on destination
throw new jIO.util.jIOError("Conflict on '" + id + "'", throw new jIO.util.jIOError("Conflict on '" + id + "'",
409); 409);
...@@ -279,7 +308,8 @@ ...@@ -279,7 +308,8 @@
}); });
} }
function checkSignatureDifference(queue, source, destination, id) { function checkSignatureDifference(queue, source, destination, id,
conflict_force, conflict_ignore) {
queue queue
.push(function () { .push(function () {
return RSVP.all([ return RSVP.all([
...@@ -308,8 +338,13 @@ ...@@ -308,8 +338,13 @@
skip_document_dict[id] = null; skip_document_dict[id] = null;
}); });
} }
throw new jIO.util.jIOError("Conflict on '" + id + "'", if (conflict_ignore === true) {
409); return;
}
if (conflict_force !== true) {
throw new jIO.util.jIOError("Conflict on '" + id + "'",
409);
}
} }
return propagateModification(source, destination, doc, return propagateModification(source, destination, doc,
local_hash, id); local_hash, id);
...@@ -384,7 +419,9 @@ ...@@ -384,7 +419,9 @@
if (signature_dict.hasOwnProperty(key)) { if (signature_dict.hasOwnProperty(key)) {
if (local_dict.hasOwnProperty(key)) { if (local_dict.hasOwnProperty(key)) {
if (options.check_modification === true) { if (options.check_modification === true) {
checkSignatureDifference(queue, source, destination, key); checkSignatureDifference(queue, source, destination, key,
options.conflict_force,
options.conflict_ignore);
} }
} else { } else {
if (options.check_deletion === true) { if (options.check_deletion === true) {
...@@ -440,6 +477,12 @@ ...@@ -440,6 +477,12 @@
context._remote_sub_storage, context._remote_sub_storage,
{ {
use_post: context._use_remote_post, 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_modification: context._check_local_modification,
check_creation: context._check_local_creation, check_creation: context._check_local_creation,
check_deletion: context._check_local_deletion check_deletion: context._check_local_deletion
...@@ -464,6 +507,10 @@ ...@@ -464,6 +507,10 @@
return pushStorage(context._remote_sub_storage, return pushStorage(context._remote_sub_storage,
context._local_sub_storage, { context._local_sub_storage, {
use_bulk_get: use_bulk_get, 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_modification: context._check_remote_modification,
check_creation: context._check_remote_creation, check_creation: context._check_remote_creation,
check_deletion: context._check_remote_deletion check_deletion: context._check_remote_deletion
......
...@@ -46,6 +46,7 @@ ...@@ -46,6 +46,7 @@
deepEqual(jio.__storage._query_options, {}); deepEqual(jio.__storage._query_options, {});
equal(jio.__storage._use_remote_post, false); 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_creation, true);
equal(jio.__storage._check_local_deletion, true); equal(jio.__storage._check_local_deletion, true);
equal(jio.__storage._check_local_modification, true); equal(jio.__storage._check_local_modification, true);
...@@ -80,6 +81,7 @@ ...@@ -80,6 +81,7 @@
}, },
query: {query: 'portal_type: "Foo"', limit: [0, 1234567890]}, query: {query: 'portal_type: "Foo"', limit: [0, 1234567890]},
use_remote_post: true, use_remote_post: true,
conflict_handling: 3,
check_local_creation: false, check_local_creation: false,
check_local_deletion: false, check_local_deletion: false,
check_local_modification: false, check_local_modification: false,
...@@ -93,6 +95,7 @@ ...@@ -93,6 +95,7 @@
{query: 'portal_type: "Foo"', limit: [0, 1234567890]} {query: 'portal_type: "Foo"', limit: [0, 1234567890]}
); );
equal(jio.__storage._use_remote_post, true); 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_creation, false);
equal(jio.__storage._check_local_deletion, false); equal(jio.__storage._check_local_deletion, false);
equal(jio.__storage._check_local_modification, false); equal(jio.__storage._check_local_modification, false);
...@@ -104,6 +107,31 @@ ...@@ -104,6 +107,31 @@
"_replicate_623653d45a4e770a2c9f6b71e3144d18ee1b5bec"); "_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 // replicateStorage.get
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
...@@ -847,6 +875,195 @@ ...@@ -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 () { test("local and remote same document creations", function () {
stop(); stop();
expect(1); expect(1);
...@@ -1182,6 +1399,216 @@ ...@@ -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 () { test("local and remote document same modifications", function () {
stop(); stop();
expect(1); 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