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 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
  =============
@@ -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
- CouchPotato.database.save_document(self, validate)
18
+ retry_on_conflict do
19
+ CouchPotato.database.save_document(self, validate)
20
+ end
19
21
  end
20
22
 
21
23
  def save!
22
- CouchPotato.database.save_document!(self)
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
- version: 0.2.5
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-03-04 00:00:00 +01:00
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: couch_potato
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
- version_requirement:
19
- version_requirements: !ruby/object:Gem::Requirement
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
- version:
46
+ type: :runtime
47
+ version_requirements: *id002
25
48
  - !ruby/object:Gem::Dependency
26
49
  name: activesupport
27
- type: :runtime
28
- version_requirement:
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
- version:
58
+ type: :runtime
59
+ version_requirements: *id003
35
60
  - !ruby/object:Gem::Dependency
36
61
  name: validatable
37
- type: :runtime
38
- version_requirement:
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
- version:
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.5
129
+ rubygems_version: 1.3.6
101
130
  signing_key:
102
131
  specification_version: 3
103
132
  summary: Convenience layer for CouchDB and SimpleDB