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 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