simply_stored 0.2.5 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +9 -0
- data/README.md +33 -0
- data/lib/simply_stored/couch.rb +8 -0
- data/lib/simply_stored/instance_methods.rb +53 -2
- data/test/simply_stored_couch_test.rb +91 -1
- metadata +46 -17
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,15 @@
|
|
1
1
|
Changelog
|
2
2
|
=============
|
3
3
|
|
4
|
+
0.3.0
|
5
|
+
=============
|
6
|
+
|
7
|
+
- SimplyStored now automatically retries conflicted save operations if it is possible to resolve the conflict.
|
8
|
+
Solving the conflict means that if updated were done one different attributes the local object will
|
9
|
+
refresh those attributes and try to save again. This will be tried two times by default. Afterwards the conflict
|
10
|
+
exception will be re-raised.
|
11
|
+
|
12
|
+
This feature can be controlled on the class level like this: User.auto_conflict_resolution_on_save = true | false
|
4
13
|
|
5
14
|
0.2.5
|
6
15
|
=============
|
data/README.md
CHANGED
@@ -10,6 +10,8 @@ Both backends have also support for S3 attachments.
|
|
10
10
|
See also [RockingChair](http://github.com/jweiss/rocking_chair) on how to speed-up your unit tests
|
11
11
|
by using an in-memory CouchDB backend.
|
12
12
|
|
13
|
+
More examples on how to work with SimplyStored can be found [here](http://github.com/jweiss/simply_stored_examples)
|
14
|
+
|
13
15
|
Installation
|
14
16
|
============
|
15
17
|
|
@@ -217,6 +219,37 @@ SimplyStored also has support for "soft deleting" - much like acts_as_paranoid.
|
|
217
219
|
Document.find_all_by_title('secret project info', :with_deleted => true)
|
218
220
|
# => [doc]
|
219
221
|
|
222
|
+
CouchDB - Auto resolution of conflicts on save
|
223
|
+
|
224
|
+
SimplyStored now by default retries conflicted save operations if it is possible to resolve the conflict.
|
225
|
+
Solving the conflict means that if updated were done one different attributes the local object will
|
226
|
+
refresh those attributes and try to save again. This will be tried two times by default. Afterwards the conflict
|
227
|
+
exception will be re-raised.
|
228
|
+
|
229
|
+
This feature can be controlled on the class level like this:
|
230
|
+
User.auto_conflict_resolution_on_save = true | false
|
231
|
+
|
232
|
+
If auto_conflict_resolution_on_save is enabled, something like this will work:
|
233
|
+
|
234
|
+
class Document
|
235
|
+
include SimplyStored::Couch
|
236
|
+
|
237
|
+
property :title
|
238
|
+
property :content
|
239
|
+
end
|
240
|
+
|
241
|
+
original = Document.create(:title => 'version 1', :content => 'Hi there')
|
242
|
+
|
243
|
+
other_client = Document.find(original.id)
|
244
|
+
|
245
|
+
original.title = 'version 2'
|
246
|
+
original.save!
|
247
|
+
|
248
|
+
other_client.content = 'A better version'
|
249
|
+
other_client.save! # -> this line would fail without auto_conflict_resolution_on_save
|
250
|
+
|
251
|
+
other_client.title
|
252
|
+
# => 'version 2'
|
220
253
|
|
221
254
|
License
|
222
255
|
=============
|
data/lib/simply_stored/couch.rb
CHANGED
@@ -114,6 +114,14 @@ module SimplyStored
|
|
114
114
|
!soft_delete_attribute.nil?
|
115
115
|
end
|
116
116
|
|
117
|
+
def auto_conflict_resolution_on_save
|
118
|
+
@auto_conflict_resolution_on_save.nil? ? true : @auto_conflict_resolution_on_save
|
119
|
+
end
|
120
|
+
|
121
|
+
def auto_conflict_resolution_on_save=(val)
|
122
|
+
@auto_conflict_resolution_on_save = val
|
123
|
+
end
|
124
|
+
|
117
125
|
def simpledb_string(*names)
|
118
126
|
names.each do |name|
|
119
127
|
property name
|
@@ -15,11 +15,15 @@ module SimplyStored
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def save(validate = true)
|
18
|
-
|
18
|
+
retry_on_conflict do
|
19
|
+
CouchPotato.database.save_document(self, validate)
|
20
|
+
end
|
19
21
|
end
|
20
22
|
|
21
23
|
def save!
|
22
|
-
|
24
|
+
retry_on_conflict do
|
25
|
+
CouchPotato.database.save_document!(self)
|
26
|
+
end
|
23
27
|
end
|
24
28
|
|
25
29
|
def destroy(override_soft_delete=false)
|
@@ -64,6 +68,53 @@ module SimplyStored
|
|
64
68
|
|
65
69
|
protected
|
66
70
|
|
71
|
+
def retry_on_conflict(max_retries = 2, &blk)
|
72
|
+
retry_count = 0
|
73
|
+
begin
|
74
|
+
blk.call
|
75
|
+
rescue RestClient::Conflict => e
|
76
|
+
if self.class.auto_conflict_resolution_on_save && retry_count < max_retries && try_to_merge_conflict
|
77
|
+
retry_count += 1
|
78
|
+
retry
|
79
|
+
else
|
80
|
+
raise e
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def try_to_merge_conflict
|
86
|
+
original = self.class.find(id)
|
87
|
+
our_attributes = self.attributes.dup
|
88
|
+
their_attributes = original.attributes.dup
|
89
|
+
[:updated_at, :created_at, :id, :rev, :_id, :_rev].each do |skipped_attribute|
|
90
|
+
our_attributes.delete(skipped_attribute)
|
91
|
+
their_attributes.delete(skipped_attribute)
|
92
|
+
end
|
93
|
+
if _merge_possible?(our_attributes, their_attributes)
|
94
|
+
_copy_non_conflicting_attributes(our_attributes, their_attributes)
|
95
|
+
self._rev = original._rev
|
96
|
+
true
|
97
|
+
else
|
98
|
+
false
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def _copy_non_conflicting_attributes(our_attributes, their_attributes)
|
103
|
+
their_attributes.each do |attr_name, their_value|
|
104
|
+
if !self.send("#{attr_name}_changed?") && our_attributes[attr_name] != their_value
|
105
|
+
self.send("#{attr_name}=", their_value)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def _merge_possible?(our_attributes, their_attributes)
|
111
|
+
their_attributes.all? do |attr_name, their_value|
|
112
|
+
our_attributes[attr_name] == their_value || # same
|
113
|
+
!self.send("#{attr_name}_changed?") || # we didn't change
|
114
|
+
self.send("#{attr_name}_changed?") && their_value == self.send("#{attr_name}_was") # we changed and they kept the original
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
67
118
|
def reset_association_caches
|
68
119
|
self.class.properties.each do |property|
|
69
120
|
if property.respond_to?(:association?) && property.association?
|
@@ -1938,7 +1938,6 @@ class CouchTest < Test::Unit::TestCase
|
|
1938
1938
|
|
1939
1939
|
context "when counting" do
|
1940
1940
|
setup do
|
1941
|
-
recreate_db
|
1942
1941
|
@hemorrhoid = Hemorrhoid.create(:nickname => 'Claas')
|
1943
1942
|
assert @hemorrhoid.destroy
|
1944
1943
|
assert @hemorrhoid.reload.deleted?
|
@@ -1973,5 +1972,96 @@ class CouchTest < Test::Unit::TestCase
|
|
1973
1972
|
|
1974
1973
|
end
|
1975
1974
|
|
1975
|
+
context "when handling conflicts" do
|
1976
|
+
setup do
|
1977
|
+
@original = User.create(:name => 'Mickey Mouse', :title => "Dr.", :homepage => 'www.gmx.de')
|
1978
|
+
@copy = User.find(@original.id)
|
1979
|
+
User.auto_conflict_resolution_on_save = true
|
1980
|
+
end
|
1981
|
+
|
1982
|
+
should "be able to save without modifications" do
|
1983
|
+
assert @copy.save
|
1984
|
+
end
|
1985
|
+
|
1986
|
+
should "be able to save when modification happen on different attributes" do
|
1987
|
+
@original.name = "Pluto"
|
1988
|
+
assert @original.save
|
1989
|
+
|
1990
|
+
@copy.title = 'Prof.'
|
1991
|
+
assert_nothing_raised do
|
1992
|
+
assert @copy.save
|
1993
|
+
end
|
1994
|
+
|
1995
|
+
assert_equal "Pluto", @copy.reload.name
|
1996
|
+
assert_equal "Prof.", @copy.reload.title
|
1997
|
+
assert_equal "www.gmx.de", @copy.reload.homepage
|
1998
|
+
end
|
1999
|
+
|
2000
|
+
should "be able to save when modification happen on different, multiple attributes - remote" do
|
2001
|
+
@original.name = "Pluto"
|
2002
|
+
@original.homepage = 'www.google.com'
|
2003
|
+
assert @original.save
|
2004
|
+
|
2005
|
+
@copy.title = 'Prof.'
|
2006
|
+
assert_nothing_raised do
|
2007
|
+
assert @copy.save
|
2008
|
+
end
|
2009
|
+
|
2010
|
+
assert_equal "Pluto", @copy.reload.name
|
2011
|
+
assert_equal "Prof.", @copy.reload.title
|
2012
|
+
assert_equal "www.google.com", @copy.reload.homepage
|
2013
|
+
end
|
2014
|
+
|
2015
|
+
should "be able to save when modification happen on different, multiple attributes locally" do
|
2016
|
+
@original.name = "Pluto"
|
2017
|
+
assert @original.save
|
2018
|
+
|
2019
|
+
@copy.title = 'Prof.'
|
2020
|
+
@copy.homepage = 'www.google.com'
|
2021
|
+
assert_nothing_raised do
|
2022
|
+
assert @copy.save
|
2023
|
+
end
|
2024
|
+
|
2025
|
+
assert_equal "Pluto", @copy.reload.name
|
2026
|
+
assert_equal "Prof.", @copy.reload.title
|
2027
|
+
assert_equal "www.google.com", @copy.reload.homepage
|
2028
|
+
end
|
2029
|
+
|
2030
|
+
should "re-raise the conflict if there is no merge possible" do
|
2031
|
+
@original.name = "Pluto"
|
2032
|
+
assert @original.save
|
2033
|
+
|
2034
|
+
@copy.name = 'Prof.'
|
2035
|
+
assert_raise(RestClient::Conflict) do
|
2036
|
+
assert @copy.save
|
2037
|
+
end
|
2038
|
+
|
2039
|
+
assert_equal "Prof.", @copy.name
|
2040
|
+
assert_equal "Pluto", @copy.reload.name
|
2041
|
+
end
|
2042
|
+
|
2043
|
+
should "re-raise the conflict if retried several times" do
|
2044
|
+
exception = RestClient::Conflict.new
|
2045
|
+
CouchPotato.database.expects(:save_document).raises(exception).times(3)
|
2046
|
+
|
2047
|
+
@copy.name = 'Prof.'
|
2048
|
+
assert_raise(RestClient::Conflict) do
|
2049
|
+
assert @copy.save
|
2050
|
+
end
|
2051
|
+
end
|
2052
|
+
|
2053
|
+
should "not try to merge and re-save if auto_conflict_resolution_on_save is disabled" do
|
2054
|
+
User.auto_conflict_resolution_on_save = false
|
2055
|
+
exception = RestClient::Conflict.new
|
2056
|
+
CouchPotato.database.expects(:save_document).raises(exception).times(1)
|
2057
|
+
|
2058
|
+
@copy.name = 'Prof.'
|
2059
|
+
assert_raise(RestClient::Conflict) do
|
2060
|
+
assert @copy.save
|
2061
|
+
end
|
2062
|
+
end
|
2063
|
+
|
2064
|
+
end
|
2065
|
+
|
1976
2066
|
end
|
1977
2067
|
end
|
metadata
CHANGED
@@ -1,7 +1,12 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: simply_stored
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 3
|
8
|
+
- 0
|
9
|
+
version: 0.3.0
|
5
10
|
platform: ruby
|
6
11
|
authors:
|
7
12
|
- Mathias Meyer, Jonathan Weiss
|
@@ -9,39 +14,61 @@ autorequire:
|
|
9
14
|
bindir: bin
|
10
15
|
cert_chain: []
|
11
16
|
|
12
|
-
date: 2010-
|
17
|
+
date: 2010-04-10 00:00:00 +02:00
|
13
18
|
default_executable:
|
14
19
|
dependencies:
|
15
20
|
- !ruby/object:Gem::Dependency
|
16
|
-
name:
|
21
|
+
name: rest-client
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 1
|
29
|
+
- 4
|
30
|
+
- 2
|
31
|
+
version: 1.4.2
|
17
32
|
type: :runtime
|
18
|
-
|
19
|
-
|
33
|
+
version_requirements: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: couch_potato
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
20
38
|
requirements:
|
21
39
|
- - ">="
|
22
40
|
- !ruby/object:Gem::Version
|
41
|
+
segments:
|
42
|
+
- 0
|
43
|
+
- 2
|
44
|
+
- 15
|
23
45
|
version: 0.2.15
|
24
|
-
|
46
|
+
type: :runtime
|
47
|
+
version_requirements: *id002
|
25
48
|
- !ruby/object:Gem::Dependency
|
26
49
|
name: activesupport
|
27
|
-
|
28
|
-
|
29
|
-
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
prerelease: false
|
51
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
30
52
|
requirements:
|
31
53
|
- - ">="
|
32
54
|
- !ruby/object:Gem::Version
|
55
|
+
segments:
|
56
|
+
- 0
|
33
57
|
version: "0"
|
34
|
-
|
58
|
+
type: :runtime
|
59
|
+
version_requirements: *id003
|
35
60
|
- !ruby/object:Gem::Dependency
|
36
61
|
name: validatable
|
37
|
-
|
38
|
-
|
39
|
-
version_requirements: !ruby/object:Gem::Requirement
|
62
|
+
prerelease: false
|
63
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
40
64
|
requirements:
|
41
65
|
- - ">="
|
42
66
|
- !ruby/object:Gem::Version
|
67
|
+
segments:
|
68
|
+
- 0
|
43
69
|
version: "0"
|
44
|
-
|
70
|
+
type: :runtime
|
71
|
+
version_requirements: *id004
|
45
72
|
description: Convenience layer for CouchDB and SimpleDB. Requires CouchPotato and RightAWS library respectively.
|
46
73
|
email: info@peritor.com
|
47
74
|
executables: []
|
@@ -86,18 +113,20 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
86
113
|
requirements:
|
87
114
|
- - ">="
|
88
115
|
- !ruby/object:Gem::Version
|
116
|
+
segments:
|
117
|
+
- 0
|
89
118
|
version: "0"
|
90
|
-
version:
|
91
119
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
92
120
|
requirements:
|
93
121
|
- - ">="
|
94
122
|
- !ruby/object:Gem::Version
|
123
|
+
segments:
|
124
|
+
- 0
|
95
125
|
version: "0"
|
96
|
-
version:
|
97
126
|
requirements: []
|
98
127
|
|
99
128
|
rubyforge_project:
|
100
|
-
rubygems_version: 1.3.
|
129
|
+
rubygems_version: 1.3.6
|
101
130
|
signing_key:
|
102
131
|
specification_version: 3
|
103
132
|
summary: Convenience layer for CouchDB and SimpleDB
|