simply_stored 0.2.5 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|