rhosync 2.1.10 → 2.1.11

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,3 +1,12 @@
1
+ ## 2.1.11 (not released)
2
+ * #17526603 - implement clientreset support for specified sources
3
+ * #18356697 - store lock is never released (support request #1466)
4
+ * use redis 2.2.12 by default
5
+ * #18672811 - edge case produces race condition which leads to corruption of Store data
6
+ * #18508155 - on failed syncs allow the user to retry it up to pre-defined number of times
7
+ * #18888077 - implement Redis transactions optimization for push_objects and push_deletes
8
+ * #19254217 - hard dependency on sinatra 1.2.7 (to be fixed in 2.1.12)
9
+
1
10
  ## 2.1.10
2
11
  * #16001227 - raise exceptions on c2dm errors
3
12
  * #1018 - delete read state for user as well
data/Rakefile CHANGED
@@ -1,4 +1,6 @@
1
1
  require 'yaml'
2
+ load 'tasks/redis.rake'
3
+
2
4
  $:.unshift File.join(File.dirname(__FILE__),'lib')
3
5
  require 'rhosync'
4
6
 
@@ -64,6 +66,7 @@ begin
64
66
  gemspec.files = FileList["[A-Z]*", "{bench,bin,generators,lib,spec,tasks}/**/*"]
65
67
 
66
68
  # TODO: Due to https://www.pivotaltracker.com/story/show/3417862, we can't use JSON 1.4.3
69
+ gemspec.add_dependency "sinatra", "= 1.2.7"
67
70
  gemspec.add_dependency "json", "~>1.4.2"
68
71
  gemspec.add_dependency "sqlite3-ruby", "~>1.2.5"
69
72
  gemspec.add_dependency "rubyzip", "~>0.9.4"
@@ -71,7 +74,6 @@ begin
71
74
  gemspec.add_dependency "redis", "~>2.1.1"
72
75
  gemspec.add_dependency "resque", "~>1.14.0"
73
76
  gemspec.add_dependency "rest-client", "~>1.6.1"
74
- gemspec.add_dependency "sinatra", "~>1.2"
75
77
  gemspec.add_dependency "templater", "~>1.0.0"
76
78
  gemspec.add_dependency "rake", "~>0.9.2"
77
79
  gemspec.add_development_dependency "log4r", "~>1.1.7"
@@ -108,6 +110,4 @@ end
108
110
  def ask(msg)
109
111
  print msg
110
112
  STDIN.gets.chomp
111
- end
112
-
113
- load 'tasks/redis.rake'
113
+ end
@@ -1,6 +1,9 @@
1
1
  Server.api :push_deletes do |params,user|
2
2
  source = Source.load(params[:source_id],{:app_id=>APP_NAME,:user_id=>params[:user_id]})
3
3
  source_sync = SourceSync.new(source)
4
- source_sync.push_deletes(params[:objects])
4
+ timeout = params[:timeout] || 10
5
+ raise_on_expire = params[:raise_on_expire] || false
6
+ rebuild_md = params[:rebuild_md].nil? ? true : params[:rebuild_md]
7
+ source_sync.push_deletes(params[:objects],timeout,raise_on_expire,rebuild_md)
5
8
  'done'
6
9
  end
@@ -1,6 +1,9 @@
1
1
  Server.api :push_objects do |params,user|
2
2
  source = Source.load(params[:source_id],{:app_id=>APP_NAME,:user_id=>params[:user_id]})
3
3
  source_sync = SourceSync.new(source)
4
- source_sync.push_objects(params[:objects])
4
+ timeout = params[:timeout] || 10
5
+ raise_on_expire = params[:raise_on_expire] || false
6
+ rebuild_md = params[:rebuild_md].nil? ? true : params[:rebuild_md]
7
+ source_sync.push_objects(params[:objects],timeout,raise_on_expire,rebuild_md)
5
8
  'done'
6
9
  end
@@ -8,6 +8,7 @@ module Rhosync
8
8
  field :phone_id,:string
9
9
  field :user_id,:string
10
10
  field :app_id,:string
11
+ field :last_sync,:datetime
11
12
  attr_accessor :source_name
12
13
  validates_presence_of :app_id, :user_id
13
14
 
@@ -29,6 +30,7 @@ module Rhosync
29
30
  end
30
31
 
31
32
  def self.load(id,params)
33
+ params.merge!(:last_sync => Time.now)
32
34
  validate_attributes(params)
33
35
  super(id,params)
34
36
  end
@@ -204,8 +204,16 @@ module Rhosync
204
204
 
205
205
  class << self
206
206
  # Resets the store for a given app,client
207
- def reset(client)
208
- client.flash_data('*') if client
207
+ # Resets the store for a given app,client
208
+ def reset(client, params=nil)
209
+ return unless client
210
+ if params == nil or params[:sources] == nil
211
+ client.flash_data('*')
212
+ else
213
+ params[:sources].each do |source|
214
+ client.flash_source_data('*', source['name'])
215
+ end
216
+ end
209
217
  end
210
218
 
211
219
  def search_all(client,params=nil)
@@ -21,10 +21,24 @@ module Document
21
21
  Store.delete_data(docname(doctype),data)
22
22
  end
23
23
 
24
+ def update_objects(doctype,updates)
25
+ Store.update_objects(docname(doctype),updates)
26
+ end
27
+
28
+ def remove_objects(doctype,deletes)
29
+ Store.delete_objects(docname(doctype),deletes)
30
+ end
31
+
24
32
  def flash_data(doctype)
25
33
  Store.flash_data(docname(doctype))
26
34
  end
27
35
 
36
+ def flash_source_data(doctype, from_source)
37
+ self.source_name=from_source
38
+ docnamestr = docname('') + doctype
39
+ Store.flash_data(docnamestr)
40
+ end
41
+
28
42
  def rename(srcdoctype,dstdoctype)
29
43
  Store.rename(docname(srcdoctype),docname(dstdoctype))
30
44
  end
data/lib/rhosync/model.rb CHANGED
@@ -33,14 +33,14 @@ module Rhosync
33
33
  # specified amount.
34
34
  def increment!(name,amount=1)
35
35
  raise ArgumentError, "Only integer fields can be incremented." unless self.class.fields.include?({:name => name.to_s, :type => :integer})
36
- redis.incr(field_key(name), amount)
36
+ redis.incrby(field_key(name), amount)
37
37
  end
38
38
 
39
39
  # Decrement the specified integer field by 1 or the
40
40
  # specified amount.
41
41
  def decrement!(name,amount=1)
42
42
  raise ArgumentError, "Only integer fields can be decremented." unless self.class.fields.include?({:name => name.to_s, :type => :integer})
43
- redis.decr(field_key(name), amount)
43
+ redis.decrby(field_key(name), amount)
44
44
  end
45
45
 
46
46
  def next_id #:nodoc:
@@ -1,6 +1,7 @@
1
1
  module Rhosync
2
2
  class ReadState < Model
3
3
  field :refresh_time, :integer
4
+ field :retry_counter, :integer
4
5
 
5
6
  def self.create(fields)
6
7
  fields[:id] = get_id(fields)
@@ -8,6 +9,7 @@ module Rhosync
8
9
  fields.delete(:user_id)
9
10
  fields.delete(:source_name)
10
11
  fields[:refresh_time] ||= Time.now.to_i
12
+ fields[:retry_counter] ||= 0
11
13
  super(fields,{})
12
14
  end
13
15
 
@@ -236,7 +236,7 @@ module Rhosync
236
236
 
237
237
  get '/application/clientreset' do
238
238
  catch_all do
239
- ClientSync.reset(current_client)
239
+ ClientSync.reset(current_client, params)
240
240
  source_config.to_json
241
241
  end
242
242
  end
@@ -90,7 +90,7 @@ module Rhosync
90
90
 
91
91
  # source fields
92
92
  define_fields([:id, :rho__id, :name, :url, :login, :password, :callback_url, :partition_type, :sync_type,
93
- :queue, :query_queue, :cud_queue, :belongs_to, :has_many], [:source_id, :priority])
93
+ :queue, :query_queue, :cud_queue, :belongs_to, :has_many], [:source_id, :priority, :retry_limit])
94
94
 
95
95
  def initialize(fields)
96
96
  self.name = fields['name'] || fields[:name]
@@ -112,6 +112,7 @@ module Rhosync
112
112
  fields[:rho__id] = fields[:name]
113
113
  fields[:belongs_to] = fields[:belongs_to].to_json if fields[:belongs_to]
114
114
  fields[:schema] = fields[:schema].to_json if fields[:schema]
115
+ fields[:retry_limit] = fields[:retry_limit] ? fields[:retry_limit] : 0
115
116
  end
116
117
 
117
118
  def self.create(fields,params)
@@ -262,15 +263,41 @@ module Rhosync
262
263
  self.poll_interval == 0 or
263
264
  (self.poll_interval != -1 and self.read_state.refresh_time <= Time.now.to_i)
264
265
  end
265
-
266
+
266
267
  def if_need_refresh(client_id=nil,params=nil)
267
- need_refresh = lock(:md) do |s|
268
- check = check_refresh_time
269
- s.read_state.refresh_time = Time.now.to_i + s.poll_interval if check
270
- check
271
- end
268
+ need_refresh = check_refresh_time
272
269
  yield client_id,params if need_refresh
273
270
  end
271
+
272
+ def update_refresh_time(query_failure = false)
273
+ if self.poll_interval == 0
274
+ self.read_state.refresh_time = Time.now.to_i + self.poll_interval
275
+ return
276
+ end
277
+
278
+ allowed_update = true
279
+ # reset number of retries on succesfull query
280
+ # or if last refresh was more than 'poll_interval' time ago
281
+ if not query_failure or (Time.now.to_i - self.read_state.refresh_time >= self.poll_interval)
282
+ self.read_state.retry_counter = 0
283
+ end
284
+
285
+ # do not reset the refresh time on failure
286
+ # if retry limit is not reached
287
+ if query_failure
288
+ if self.read_state.retry_counter < self.retry_limit
289
+ allowed_update = false
290
+ self.read_state.increment!(:retry_counter)
291
+ # we have reached the limit - update the refresh time
292
+ # and reset the counter
293
+ else
294
+ self.read_state.retry_counter = 0
295
+ end
296
+ end
297
+ if allowed_update
298
+ self.read_state.refresh_time = Time.now.to_i + self.poll_interval
299
+ end
300
+ end
274
301
 
275
302
  private
276
303
  def poll_interval_key
@@ -41,20 +41,16 @@ module Rhosync
41
41
 
42
42
  def sync
43
43
  if @result and @result.empty?
44
- @source.lock(:md) do |s|
45
- s.flash_data(:md)
46
- s.put_value(:md_size,0)
47
- end
44
+ @source.flash_data(:md)
45
+ @source.put_value(:md_size,0)
48
46
  else
49
47
  if @result
50
48
  Store.put_data(@tmp_docname,@result)
51
49
  @stash_size += @result.size
52
50
  end
53
- @source.lock(:md) do |s|
54
- s.flash_data(:md)
55
- Store.rename(@tmp_docname,s.docname(:md))
56
- s.put_value(:md_size,@stash_size)
57
- end
51
+ @source.flash_data(:md)
52
+ Store.rename(@tmp_docname,@source.docname(:md))
53
+ @source.put_value(:md_size,@stash_size)
58
54
  end
59
55
  end
60
56
 
@@ -59,12 +59,18 @@ module Rhosync
59
59
  end
60
60
 
61
61
  def do_query(params=nil)
62
- @source.if_need_refresh do
63
- Stats::Record.update("source:query:#{@source.name}") do
64
- return if _auth_op('login') == false
65
- self.read(nil,params)
66
- _auth_op('logoff')
67
- end
62
+ @source.lock(:md) do
63
+ @source.if_need_refresh do
64
+ Stats::Record.update("source:query:#{@source.name}") do
65
+ if _auth_op('login')
66
+ result = self.read(nil,params)
67
+ _auth_op('logoff')
68
+ end
69
+ # update refresh time
70
+ query_failure = Store.get_keys(@source.docname(:errors)).size > 0
71
+ @source.update_refresh_time(query_failure)
72
+ end
73
+ end
68
74
  end
69
75
  end
70
76
 
@@ -75,31 +81,57 @@ module Rhosync
75
81
  @source.app_id,@source.user_id,client_id,params)
76
82
  end
77
83
 
78
- def push_objects(objects,timeout=10,raise_on_expire=false)
84
+ def push_objects(objects,timeout=10,raise_on_expire=false,rebuild_md=true)
79
85
  @source.lock(:md,timeout,raise_on_expire) do |s|
80
- doc = @source.get_data(:md)
81
- orig_doc_size = doc.size
82
- objects.each do |id,obj|
83
- doc[id] ||= {}
84
- doc[id].merge!(obj)
85
- end
86
- diff_count = doc.size - orig_doc_size
87
- @source.put_data(:md,doc)
86
+ diff_count = 0
87
+ # in case of rebuild_md
88
+ # we clean-up and rebuild the whole :md doc
89
+ # on every request
90
+ if(rebuild_md)
91
+ doc = @source.get_data(:md)
92
+ orig_doc_size = doc.size
93
+ objects.each do |id,obj|
94
+ doc[id] ||= {}
95
+ doc[id].merge!(obj)
96
+ end
97
+ diff_count = doc.size - orig_doc_size
98
+ @source.put_data(:md,doc)
99
+ else
100
+ # if rebuild_md == false
101
+ # we only operate on specific set values
102
+ # which brings a big optimization
103
+ # in case of small transactions
104
+ diff_count = @source.update_objects(:md, objects)
105
+ end
106
+
88
107
  @source.update_count(:md_size,diff_count)
89
108
  end
90
109
  end
91
110
 
92
- def push_deletes(objects,timeout=10,raise_on_expire=false)
111
+ def push_deletes(objects,timeout=10,raise_on_expire=false,rebuild_md=true)
93
112
  @source.lock(:md,timeout,raise_on_expire) do |s|
94
- doc = @source.get_data(:md)
95
- orig_doc_size = doc.size
96
- objects.each do |id|
97
- doc.delete(id)
98
- end
99
- diff_count = doc.size - orig_doc_size
100
- @source.put_data(:md,doc)
113
+ diff_count = 0
114
+ if(rebuild_md)
115
+ # in case of rebuild_md
116
+ # we clean-up and rebuild the whole :md doc
117
+ # on every request
118
+ doc = @source.get_data(:md)
119
+ orig_doc_size = doc.size
120
+ objects.each do |id|
121
+ doc.delete(id)
122
+ end
123
+ diff_count = doc.size - orig_doc_size
124
+ @source.put_data(:md,doc)
125
+ else
126
+ # if rebuild_md == false
127
+ # we only operate on specific set values
128
+ # which brings a big optimization
129
+ # in case of small transactions
130
+ diff_count = -@source.remove_objects(:md, objects)
131
+ end
132
+
101
133
  @source.update_count(:md_size,diff_count)
102
- end
134
+ end
103
135
  end
104
136
 
105
137
  private
data/lib/rhosync/store.rb CHANGED
@@ -46,12 +46,66 @@ module Rhosync
46
46
  end
47
47
  true
48
48
  end
49
-
49
+
50
+ # updates objects for a given doctype, source, user
51
+ # create new objects if necessary
52
+ def update_objects(dockey, data={})
53
+ return 0 unless dockey and data
54
+
55
+ new_object_count = 0
56
+ doc = get_data(dockey)
57
+ @@db.pipelined do
58
+ data.each do |key,value|
59
+ is_create = doc[key].nil?
60
+ new_object_count += 1 if is_create
61
+ value.each do |attrib,value|
62
+ next if _is_reserved?(attrib, value)
63
+
64
+ new_element = setelement(key,attrib,value)
65
+ element_exists = is_create ? false : doc[key].has_key?(attrib)
66
+ if element_exists
67
+ existing_element = setelement(key,attrib,doc[key][attrib])
68
+ if existing_element != new_element
69
+ @@db.srem(dockey, existing_element)
70
+ @@db.sadd(dockey, new_element)
71
+ end
72
+ else
73
+ @@db.sadd(dockey, new_element)
74
+ end
75
+ end
76
+ end
77
+ end
78
+ new_object_count
79
+ end
80
+
81
+ # Removes objects from a given doctype,source,user
82
+ def delete_objects(dockey,data=[])
83
+ return 0 unless dockey and data
84
+
85
+ deleted_object_count = 0
86
+ doc = get_data(dockey)
87
+ @@db.pipelined do
88
+ data.each do |id|
89
+ if doc[id]
90
+ doc[id].each do |name,value|
91
+ @@db.srem(dockey, setelement(id,name,value))
92
+ end
93
+ deleted_object_count += 1
94
+ end
95
+ doc.delete(id)
96
+ end
97
+ end
98
+ deleted_object_count
99
+ end
100
+
50
101
  # Adds a simple key/value pair
51
102
  def put_value(dockey,value)
52
103
  if dockey
53
- @@db.del(dockey)
54
- @@db.set(dockey,value.to_s) if value
104
+ if value
105
+ @@db.set(dockey,value.to_s)
106
+ else
107
+ @@db.del(dockey)
108
+ end
55
109
  end
56
110
  end
57
111
 
@@ -145,7 +199,7 @@ module Rhosync
145
199
  def lock(dockey,timeout=0,raise_on_expire=false)
146
200
  m_lock = get_lock(dockey,timeout,raise_on_expire)
147
201
  res = yield
148
- release_lock(dockey,m_lock)
202
+ release_lock(dockey,m_lock,raise_on_expire)
149
203
  res
150
204
  end
151
205
 
@@ -187,8 +241,8 @@ module Rhosync
187
241
  # Time.now.to_i+timeout+1
188
242
  # end
189
243
 
190
- def release_lock(dockey,lock)
191
- @@db.del(_lock_key(dockey)) if (lock >= Time.now.to_i)
244
+ def release_lock(dockey,lock,raise_on_expire=false)
245
+ @@db.del(_lock_key(dockey)) if raise_on_expire or Rhosync.raise_on_expired_lock or (lock >= Time.now.to_i)
192
246
  end
193
247
 
194
248
  # Create a copy of srckey in dstkey
@@ -1,3 +1,3 @@
1
1
  module Rhosync
2
- VERSION = '2.1.10'
2
+ VERSION = '2.1.11'
3
3
  end
@@ -6,7 +6,7 @@ describe "RhosyncApiGetClientParams" do
6
6
  it "should list client attributes" do
7
7
  post "/api/get_client_params", {:api_token => @api_token, :client_id =>@c.id}
8
8
  res = JSON.parse(last_response.body)
9
- res.delete_if { |attrib| attrib['name'] == 'rho__id' }
9
+ res.delete_if { |attrib| attrib['name'] == 'rho__id' || attrib['name'] == 'last_sync'}
10
10
  res.sort{|x,y| x['name']<=>y['name']}.should == [
11
11
  {"name"=>"device_type", "value"=>"Apple", "type"=>"string"},
12
12
  {"name"=>"device_pin", "value"=>"abcd", "type"=>"string"},
@@ -15,7 +15,8 @@ describe "RhosyncApiGetSourceParams" do
15
15
  {"name"=>"password", "value"=>"testpass", "type"=>"string"},
16
16
  {"name"=>"priority", "value"=>3, "type"=>"integer"},
17
17
  {"name"=>"callback_url", "value"=>nil, "type"=>"string"},
18
- {"name"=>"poll_interval", "value"=>300, "type"=>"integer"},
18
+ {"name"=>"poll_interval", "value"=>300, "type"=>"integer"},
19
+ {"name"=>"retry_limit", "value"=>0, "type"=>"integer"},
19
20
  {"name"=>"partition_type", "value"=>"user", "type"=>"string"},
20
21
  {"name"=>"sync_type", "value"=>"incremental", "type"=>"string"},
21
22
  {"name"=>"belongs_to", "type"=>"string", "value"=>nil},
@@ -121,7 +121,7 @@ describe "RhosyncApi" do
121
121
 
122
122
  it "should list client attributes using direct api call" do
123
123
  res = RhosyncApi::get_client_params('',@api_token,@c.id)
124
- res.delete_if { |attrib| attrib['name'] == 'rho__id' }
124
+ res.delete_if { |attrib| attrib['name'] == 'rho__id' || attrib['name'] == 'last_sync'}
125
125
  res.sort{|x,y| x['name']<=>y['name']}.should == [
126
126
  {"name"=>"device_type", "value"=>"Apple", "type"=>"string"},
127
127
  {"name"=>"device_pin", "value"=>"abcd", "type"=>"string"},
@@ -164,7 +164,8 @@ describe "RhosyncApi" do
164
164
  {"name"=>"password", "value"=>"testpass", "type"=>"string"},
165
165
  {"name"=>"priority", "value"=>3, "type"=>"integer"},
166
166
  {"name"=>"callback_url", "value"=>nil, "type"=>"string"},
167
- {"name"=>"poll_interval", "value"=>300, "type"=>"integer"},
167
+ {"name"=>"poll_interval", "value"=>300, "type"=>"integer"},
168
+ {"name"=>"retry_limit", "type"=>"integer", "value"=>0},
168
169
  {"name"=>"partition_type", "value"=>"user", "type"=>"string"},
169
170
  {"name"=>"sync_type", "value"=>"incremental", "type"=>"string"},
170
171
  {"name"=>"belongs_to", "type"=>"string", "value"=>nil},
@@ -150,6 +150,24 @@ describe "ClientSync" do
150
150
  verify_result(@c.docname(:cd) => {})
151
151
  Client.load(@c.id,{:source_name => @s.name}).should_not be_nil
152
152
  end
153
+
154
+ it "should handle reset on individual source adapters" do
155
+ @c.source_name = 'SampleAdapter'
156
+ set_state(@c.docname(:cd) => @data)
157
+ verify_result(@c.docname(:cd) => @data)
158
+
159
+ @c.source_name = 'SimpleAdapter'
160
+ set_state(@c.docname(:cd) => @data)
161
+ verify_result(@c.docname(:cd) => @data)
162
+
163
+ sources = [{'name'=>'SimpleAdapter'}]
164
+ ClientSync.reset(@c, {:sources => sources})
165
+
166
+ @c.source_name = 'SampleAdapter'
167
+ verify_result(@c.docname(:cd) => @data)
168
+ @c.source_name = 'SimpleAdapter'
169
+ verify_result(@c.docname(:cd) => {})
170
+ end
153
171
  end
154
172
 
155
173
  describe "search" do
data/spec/model_spec.rb CHANGED
@@ -207,12 +207,12 @@ describe Rhosync::Model do
207
207
  end
208
208
 
209
209
  it "should send INCR when #increment! is called on an integer" do
210
- @redisMock.should_receive(:incr).with("test_increments:1:foo", 1)
210
+ @redisMock.should_receive(:incrby).with("test_increments:1:foo", 1)
211
211
  @x.increment!(:foo)
212
212
  end
213
213
 
214
214
  it "should send DECR when #decrement! is called on an integer" do
215
- @redisMock.should_receive(:decr).with("test_increments:1:foo", 1)
215
+ @redisMock.should_receive(:decrby).with("test_increments:1:foo", 1)
216
216
  @x.decrement!(:foo)
217
217
  end
218
218
 
@@ -177,6 +177,20 @@ describe "Server" do
177
177
  verify_result(@c.docname(:cd) => {})
178
178
  end
179
179
 
180
+ it "should respond to clientreset with individual adapters" do
181
+ @c.source_name = 'SimpleAdapter'
182
+ set_state(@c.docname(:cd) => @data)
183
+ @c.source_name = 'SampleAdapter'
184
+ set_state(@c.docname(:cd) => @data)
185
+ sources = [{'name' => 'SimpleAdapter'}]
186
+ get "/application/clientreset", :client_id => @c.id,:version => ClientSync::VERSION, :sources => sources
187
+ JSON.parse(last_response.body).should == @source_config
188
+ @c.source_name = 'SampleAdapter'
189
+ verify_result(@c.docname(:cd) => @data)
190
+ @c.source_name = 'SimpleAdapter'
191
+ verify_result(@c.docname(:cd) => {})
192
+ end
193
+
180
194
  it "should switch client user if client user_id doesn't match session user" do
181
195
  set_test_data('test_db_storage',@data)
182
196
  get "/application",:client_id => @c.id,:source_name => @s.name,:version => ClientSync::VERSION
@@ -208,6 +208,77 @@ describe "SourceSync" do
208
208
  it "should do search with exception raised" do
209
209
  verify_read_operation_with_error('search')
210
210
  end
211
+
212
+ it "should do query with exception raised and update refresh time only after retries limit is exceeded" do
213
+ @s.retry_limit = 1
214
+ msg = "Error during query"
215
+ set_test_data('test_db_storage',{},msg,"query error")
216
+ res = @ss.do_query
217
+ verify_result(@s.docname(:md) => {},
218
+ @s.docname(:errors) => {'query-error'=>{'message'=>msg}})
219
+ # 1) if retry_limit is set to N - then, first N retries should not update refresh_time
220
+ @s.read_state.retry_counter.should == 1
221
+ @s.read_state.refresh_time.should <= Time.now.to_i
222
+
223
+ # try once more and fail again
224
+ set_test_data('test_db_storage',{},msg,"query error")
225
+ res = @ss.do_query
226
+ verify_result(@s.docname(:md) => {},
227
+ @s.docname(:errors) => {'query-error'=>{'message'=>msg}})
228
+
229
+ # 2) if retry_limit is set to N and number of retries exceeded it - update refresh_time
230
+ @s.read_state.retry_counter.should == 0
231
+ @s.read_state.refresh_time.should > Time.now.to_i
232
+ end
233
+
234
+ it "should do query with exception raised and restore state with succesfull retry" do
235
+ @s.retry_limit = 1
236
+ msg = "Error during query"
237
+ set_test_data('test_db_storage',{},msg,"query error")
238
+ res = @ss.do_query
239
+ verify_result(@s.docname(:md) => {},
240
+ @s.docname(:errors) => {'query-error'=>{'message'=>msg}})
241
+ # 1) if retry_limit is set to N - then, first N retries should not update refresh_time
242
+ @s.read_state.retry_counter.should == 1
243
+ @s.read_state.refresh_time.should <= Time.now.to_i
244
+
245
+ # try once more (with success)
246
+ expected = {'1'=>@product1,'2'=>@product2}
247
+ set_test_data('test_db_storage',expected)
248
+ @ss.do_query
249
+ verify_result(@s.docname(:md) => expected,
250
+ @s.docname(:errors) => {})
251
+ @s.read_state.retry_counter.should == 0
252
+ @s.read_state.refresh_time.should > Time.now.to_i
253
+ end
254
+
255
+ it "should do query with exception raised and update refresh time if retry_limit is 0" do
256
+ @s.retry_limit = 0
257
+ msg = "Error during query"
258
+ set_test_data('test_db_storage',{},msg,"query error")
259
+ res = @ss.do_query
260
+ verify_result(@s.docname(:md) => {},
261
+ @s.docname(:errors) => {'query-error'=>{'message'=>msg}})
262
+ # if poll_interval is set to 0 - refresh time should be updated
263
+ @s.read_state.retry_counter.should == 0
264
+ @s.read_state.refresh_time.should > Time.now.to_i
265
+ end
266
+
267
+ it "should do query with exception raised and update refresh time if poll_interval == 0" do
268
+ @s.retry_limit = 1
269
+ @s.poll_interval = 0
270
+ msg = "Error during query"
271
+ set_test_data('test_db_storage',{},msg,"query error")
272
+ prev_refresh_time = @s.read_state.refresh_time
273
+ # make sure refresh time is expired
274
+ sleep(1)
275
+ res = @ss.do_query
276
+ verify_result(@s.docname(:md) => {},
277
+ @s.docname(:errors) => {'query-error'=>{'message'=>msg}})
278
+ # if poll_interval is set to 0 - refresh time should be updated
279
+ @s.read_state.retry_counter.should == 0
280
+ @s.read_state.refresh_time.should > prev_refresh_time
281
+ end
211
282
  end
212
283
 
213
284
  describe "app-level partitioning" do
@@ -221,7 +292,8 @@ describe "SourceSync" do
221
292
  verify_result("source:#{@test_app_name}:__shared__:#{@s_fields[:name]}:md" => expected)
222
293
  Store.db.keys("read_state:#{@test_app_name}:__shared__*").sort.should ==
223
294
  [ "read_state:#{@test_app_name}:__shared__:SampleAdapter:refresh_time",
224
- "read_state:#{@test_app_name}:__shared__:SampleAdapter:rho__id"]
295
+ "read_state:#{@test_app_name}:__shared__:SampleAdapter:retry_counter",
296
+ "read_state:#{@test_app_name}:__shared__:SampleAdapter:rho__id"].sort
225
297
  end
226
298
  end
227
299
 
data/spec/store_spec.rb CHANGED
@@ -51,6 +51,41 @@ describe "Store" do
51
51
  Store.get_data('mydata').should == data
52
52
  end
53
53
 
54
+ it "should update_objects with simple data and one changed attribute" do
55
+ data = { '1' => { 'hello' => 'world', "attr1" => 'value1' } }
56
+ update_data = { '1' => {'attr1' => 'value2'}}
57
+ Store.put_data('mydata', data)
58
+ Store.get_data('mydata').should == data
59
+ Store.update_objects('mydata', update_data)
60
+ data['1'].merge!(update_data['1'])
61
+ Store.get_data('mydata').should == data
62
+ end
63
+
64
+ it "should update_objects with simple data and verify that srem and sadd is called only on affected fields" do
65
+ data = { '1' => { 'hello' => 'world', "attr1" => 'value1' } }
66
+ update_data = { '1' => {'attr1' => 'value2', 'new_attr' => 'new_val', 'hello' => 'world'},
67
+ '2' => {'whole_new_object' => 'new_value' } }
68
+ Store.put_data('mydata', data)
69
+ Store.db.should_receive(:srem).exactly(1).times
70
+ Store.db.should_receive(:sadd).exactly(3).times
71
+ Store.update_objects('mydata', update_data)
72
+ end
73
+
74
+ it "should delete_objects with simple data" do
75
+ data = { '1' => { 'hello' => 'world', "attr1" => 'value1' } }
76
+ Store.put_data('mydata', data)
77
+ Store.delete_objects('mydata', ['1'])
78
+ Store.get_data('mydata').should == {}
79
+ end
80
+
81
+ it "should delete_objects with simple data and verify that srem is called only on affected fields" do
82
+ data = { '1' => { 'hello' => 'world', "attr1" => 'value1' } }
83
+ Store.put_data('mydata', data)
84
+ Store.db.should_receive(:srem).exactly(2).times
85
+ Store.db.should_receive(:sadd).exactly(0).times
86
+ Store.delete_objects('mydata', ['1'])
87
+ end
88
+
54
89
  it "should add simple array data to new set" do
55
90
  @data = ['1','2','3']
56
91
  Store.put_data(@s.docname(:md),@data).should == true
data/tasks/redis.rake CHANGED
@@ -7,7 +7,7 @@ def windows?
7
7
  end
8
8
 
9
9
  if windows?
10
- $redis_ver = "redis-2.2.2"
10
+ $redis_ver = "redis-2.2.12"
11
11
  $redis_zip = "C:/#{$redis_ver}.zip"
12
12
  $redis_dest = "C:/"
13
13
  end
@@ -161,7 +161,7 @@ namespace :redis do
161
161
  else
162
162
  sh 'rm -rf /tmp/redis/' if File.exists?("#{RedisRunner.redisdir}")
163
163
  sh 'git clone git://github.com/antirez/redis.git /tmp/redis -n'
164
- sh "cd #{RedisRunner.redisdir} && git reset --hard && git checkout 2.2.2"
164
+ sh "cd #{RedisRunner.redisdir} && git reset --hard && git checkout 2.2.12"
165
165
  end
166
166
  end
167
167
 
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rhosync
3
3
  version: !ruby/object:Gem::Version
4
- hash: 31
4
+ hash: 29
5
5
  prerelease:
6
6
  segments:
7
7
  - 2
8
8
  - 1
9
- - 10
10
- version: 2.1.10
9
+ - 11
10
+ version: 2.1.11
11
11
  platform: ruby
12
12
  authors:
13
13
  - Rhomobile
@@ -15,12 +15,28 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-08-23 00:00:00 Z
18
+ date: 2011-10-04 00:00:00 Z
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
- name: json
21
+ name: sinatra
22
22
  prerelease: false
23
23
  requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - "="
27
+ - !ruby/object:Gem::Version
28
+ hash: 17
29
+ segments:
30
+ - 1
31
+ - 2
32
+ - 7
33
+ version: 1.2.7
34
+ type: :runtime
35
+ version_requirements: *id001
36
+ - !ruby/object:Gem::Dependency
37
+ name: json
38
+ prerelease: false
39
+ requirement: &id002 !ruby/object:Gem::Requirement
24
40
  none: false
25
41
  requirements:
26
42
  - - ~>
@@ -32,11 +48,11 @@ dependencies:
32
48
  - 2
33
49
  version: 1.4.2
34
50
  type: :runtime
35
- version_requirements: *id001
51
+ version_requirements: *id002
36
52
  - !ruby/object:Gem::Dependency
37
53
  name: sqlite3-ruby
38
54
  prerelease: false
39
- requirement: &id002 !ruby/object:Gem::Requirement
55
+ requirement: &id003 !ruby/object:Gem::Requirement
40
56
  none: false
41
57
  requirements:
42
58
  - - ~>
@@ -48,11 +64,11 @@ dependencies:
48
64
  - 5
49
65
  version: 1.2.5
50
66
  type: :runtime
51
- version_requirements: *id002
67
+ version_requirements: *id003
52
68
  - !ruby/object:Gem::Dependency
53
69
  name: rubyzip
54
70
  prerelease: false
55
- requirement: &id003 !ruby/object:Gem::Requirement
71
+ requirement: &id004 !ruby/object:Gem::Requirement
56
72
  none: false
57
73
  requirements:
58
74
  - - ~>
@@ -64,11 +80,11 @@ dependencies:
64
80
  - 4
65
81
  version: 0.9.4
66
82
  type: :runtime
67
- version_requirements: *id003
83
+ version_requirements: *id004
68
84
  - !ruby/object:Gem::Dependency
69
85
  name: uuidtools
70
86
  prerelease: false
71
- requirement: &id004 !ruby/object:Gem::Requirement
87
+ requirement: &id005 !ruby/object:Gem::Requirement
72
88
  none: false
73
89
  requirements:
74
90
  - - ">="
@@ -80,11 +96,11 @@ dependencies:
80
96
  - 1
81
97
  version: 2.1.1
82
98
  type: :runtime
83
- version_requirements: *id004
99
+ version_requirements: *id005
84
100
  - !ruby/object:Gem::Dependency
85
101
  name: redis
86
102
  prerelease: false
87
- requirement: &id005 !ruby/object:Gem::Requirement
103
+ requirement: &id006 !ruby/object:Gem::Requirement
88
104
  none: false
89
105
  requirements:
90
106
  - - ~>
@@ -96,11 +112,11 @@ dependencies:
96
112
  - 1
97
113
  version: 2.1.1
98
114
  type: :runtime
99
- version_requirements: *id005
115
+ version_requirements: *id006
100
116
  - !ruby/object:Gem::Dependency
101
117
  name: resque
102
118
  prerelease: false
103
- requirement: &id006 !ruby/object:Gem::Requirement
119
+ requirement: &id007 !ruby/object:Gem::Requirement
104
120
  none: false
105
121
  requirements:
106
122
  - - ~>
@@ -112,11 +128,11 @@ dependencies:
112
128
  - 0
113
129
  version: 1.14.0
114
130
  type: :runtime
115
- version_requirements: *id006
131
+ version_requirements: *id007
116
132
  - !ruby/object:Gem::Dependency
117
133
  name: rest-client
118
134
  prerelease: false
119
- requirement: &id007 !ruby/object:Gem::Requirement
135
+ requirement: &id008 !ruby/object:Gem::Requirement
120
136
  none: false
121
137
  requirements:
122
138
  - - ~>
@@ -128,21 +144,6 @@ dependencies:
128
144
  - 1
129
145
  version: 1.6.1
130
146
  type: :runtime
131
- version_requirements: *id007
132
- - !ruby/object:Gem::Dependency
133
- name: sinatra
134
- prerelease: false
135
- requirement: &id008 !ruby/object:Gem::Requirement
136
- none: false
137
- requirements:
138
- - - ~>
139
- - !ruby/object:Gem::Version
140
- hash: 11
141
- segments:
142
- - 1
143
- - 2
144
- version: "1.2"
145
- type: :runtime
146
147
  version_requirements: *id008
147
148
  - !ruby/object:Gem::Dependency
148
149
  name: templater
@@ -569,7 +570,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
569
570
  requirements: []
570
571
 
571
572
  rubyforge_project:
572
- rubygems_version: 1.8.7
573
+ rubygems_version: 1.8.10
573
574
  signing_key:
574
575
  specification_version: 3
575
576
  summary: RhoSync Synchronization Framework