rhosync 2.1.11 → 2.1.12

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,4 +1,9 @@
1
- ## 2.1.11 (not released)
1
+ ## 2.1.12
2
+ * #19304885 - fixing race condition in get_lock (lock is released between setnx and get calls)
3
+ * #18508155 - on failed syncs allow the user to retry it up to pre-defined number of times (another approach)
4
+ * #19143845 - added sinatra 1.3.x support
5
+
6
+ ## 2.1.11
2
7
  * #17526603 - implement clientreset support for specified sources
3
8
  * #18356697 - store lock is never released (support request #1466)
4
9
  * use redis 2.2.12 by default
data/Rakefile CHANGED
@@ -66,7 +66,7 @@ begin
66
66
  gemspec.files = FileList["[A-Z]*", "{bench,bin,generators,lib,spec,tasks}/**/*"]
67
67
 
68
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"
69
+ gemspec.add_dependency "sinatra", ">= 1.2.7"
70
70
  gemspec.add_dependency "json", "~>1.4.2"
71
71
  gemspec.add_dependency "sqlite3-ruby", "~>1.2.5"
72
72
  gemspec.add_dependency "rubyzip", "~>0.9.4"
@@ -0,0 +1,29 @@
1
+ module Rhosync
2
+ class BodyContentTypeParser
3
+
4
+ # Constants
5
+ #
6
+ CONTENT_TYPE = 'CONTENT_TYPE'.freeze
7
+ POST_BODY = 'rack.input'.freeze
8
+ FORM_INPUT = 'rack.request.form_input'.freeze
9
+ FORM_HASH = 'rack.request.form_hash'.freeze
10
+
11
+ def initialize(app)
12
+ @app = app
13
+ end
14
+
15
+ def call(env)
16
+ if env['CONTENT_TYPE'] && env['CONTENT_TYPE'].match(/^application\/json/)
17
+ begin
18
+ if (body = env[POST_BODY].read).length != 0
19
+ env.update(FORM_HASH => JSON.parse(body), FORM_INPUT => env[POST_BODY])
20
+ end
21
+ rescue JSON::ParserError => jpe
22
+ log jpe.message + jpe.backtrace.join("\n")
23
+ return [500, {'Content-Type' => 'text/plain'}, ["Server error while processing client data"]]
24
+ end
25
+ end
26
+ @app.call(env)
27
+ end
28
+ end
29
+ end
@@ -40,7 +40,7 @@ module Rhosync
40
40
  return _resend_search_result if params[:token] and params[:resend]
41
41
  if params[:token] and !_ack_search(params[:token])
42
42
  formatted_result = _format_search_result
43
- @client.flash_data('search*')
43
+ _delete_search
44
44
  return formatted_result
45
45
  end
46
46
  end
@@ -279,7 +279,7 @@ module Rhosync
279
279
 
280
280
  def _ack_search(search_token)
281
281
  if @client.get_value(:search_token) != search_token
282
- @client.flash_data('search*')
282
+ _delete_search
283
283
  @client.put_data(:search_errors,
284
284
  {'search-error'=>{'message'=>'Search error - invalid token'}}
285
285
  )
@@ -294,7 +294,7 @@ module Rhosync
294
294
  @source_sync.search(@client.id,search_params) if params.nil? or !params[:token]
295
295
  res,diffsize = compute_search
296
296
  formatted_res = _format_search_result(res,diffsize)
297
- @client.flash_data('search*') if diffsize == 0
297
+ _delete_search if diffsize == 0
298
298
  formatted_res
299
299
  end
300
300
 
@@ -361,6 +361,12 @@ module Rhosync
361
361
  end
362
362
  @client.flash_data("update_rollback_page")
363
363
  end
364
+
365
+ def _delete_search
366
+ [:search, :search_page, :search_token, :search_errors].each do |search_doc|
367
+ @client.flash_data(search_doc)
368
+ end
369
+ end
364
370
 
365
371
  def _send_errors
366
372
  res = {}
@@ -15,9 +15,13 @@ module RhosyncConsole
15
15
  end
16
16
 
17
17
  class Server < Sinatra::Base
18
- set :views, RhosyncConsole::root_path("app","views")
19
- set :public, RhosyncConsole::root_path("app","public")
20
- set :static, true
18
+ set :views, RhosyncConsole::root_path("app","views")
19
+ if Sinatra.const_defined?("VERSION") && Gem::Version.new(Sinatra::VERSION) >= Gem::Version.new("1.3.0")
20
+ set :public_folder, RhosyncConsole::root_path("app","public")
21
+ else
22
+ set :public, RhosyncConsole::root_path("app","public")
23
+ end
24
+ set :static, true
21
25
  use Rack::Session::Cookie
22
26
  before do
23
27
  headers['Expires'] = 'Sun, 19 Nov 1978 05:00:00 GMT'
@@ -1,6 +1,7 @@
1
1
  module Rhosync
2
2
  class ReadState < Model
3
3
  field :refresh_time, :integer
4
+ field :prev_refresh_time, :integer
4
5
  field :retry_counter, :integer
5
6
 
6
7
  def self.create(fields)
@@ -9,6 +10,7 @@ module Rhosync
9
10
  fields.delete(:user_id)
10
11
  fields.delete(:source_name)
11
12
  fields[:refresh_time] ||= Time.now.to_i
13
+ fields[:prev_refresh_time] ||= Time.now.to_i
12
14
  fields[:retry_counter] ||= 0
13
15
  super(fields,{})
14
16
  end
@@ -4,6 +4,7 @@ require 'erb'
4
4
  require 'json'
5
5
  require 'fileutils'
6
6
  require 'rhosync'
7
+ require 'rhosync/body_content_type_parser'
7
8
 
8
9
  module Rhosync
9
10
 
@@ -17,15 +18,17 @@ module Rhosync
17
18
 
18
19
  class Server < Sinatra::Base
19
20
  libdir = File.dirname(File.expand_path(__FILE__))
20
- set :views, "#{libdir}/server/views"
21
- set :public, "#{libdir}/server/public"
22
- set :static, true
21
+ set :views, "#{libdir}/server/views"
22
+ if Sinatra.const_defined?("VERSION") && Gem::Version.new(Sinatra::VERSION) >= Gem::Version.new("1.3.0")
23
+ set :public_folder, "#{libdir}/server/public"
24
+ else
25
+ set :public, "#{libdir}/server/public"
26
+ end
27
+ set :static, true
28
+ set :stats, false
23
29
 
24
30
  # default secret
25
31
  @@secret = '<changeme>'
26
-
27
- # stats middleware disabled by default
28
- @@stats = false
29
32
 
30
33
  # Setup route and mimetype for bulk data downloads
31
34
  # TODO: Figure out why "mime :data, 'application/octet-stream'" doesn't work
@@ -60,7 +63,7 @@ module Rhosync
60
63
  def login
61
64
  if params[:login] == 'rhoadmin'
62
65
  user = User.authenticate(params[:login], params[:password])
63
- elsif current_app and current_app.can_authenticate?
66
+ elsif current_app and current_app.can_authenticate? and params[:login]
64
67
  user = current_app.authenticate(params[:login], params[:password], session)
65
68
  end
66
69
  if user
@@ -142,26 +145,25 @@ module Rhosync
142
145
 
143
146
  # hook into new so we can enable middleware
144
147
  def self.new
145
- if @@stats == true
148
+ use Rhosync::BodyContentTypeParser
149
+ if settings.respond_to?(:stats) and settings.send(:stats) == true
146
150
  use Rhosync::Stats::Middleware
147
- Rhosync.stats = true
151
+ Rhosync.stats = true
152
+ else
153
+ Rhosync::Server.disable :stats
154
+ Rhosync.stats = false
148
155
  end
156
+ Rhosync::Server.set :secret, @@secret unless settings.respond_to?(:secret)
149
157
  use Rack::Session::Cookie,
150
- :key => 'rhosync_session',
151
- :expire_after => 31536000,
152
- :secret => @@secret
153
- super
154
- end
155
-
156
- def self.set(option, value=self, &block)
157
- @@stats = value if option == :stats and (value.is_a?(TrueClass) or value.is_a?(FalseClass))
158
- @@secret = value if option == :secret and value.is_a?(String)
158
+ :key => 'rhosync_session',
159
+ :expire_after => 31536000,
160
+ :secret => Rhosync::Server.secret
159
161
  super
160
162
  end
161
163
 
162
164
  def initialize
163
165
  # Whine about default session secret
164
- check_default_secret!(@@secret)
166
+ check_default_secret!(Rhosync::Server.secret)
165
167
  super
166
168
  end
167
169
 
@@ -173,11 +175,6 @@ module Rhosync
173
175
  cud = JSON.parse(params["cud"])
174
176
  params.delete("cud")
175
177
  params.merge!(cud)
176
- end
177
- #application/json; charset=UTF-8
178
- if request.env['CONTENT_TYPE'] && request.env['CONTENT_TYPE'].match(/^application\/json/)
179
- params.merge!(JSON.parse(request.body.read))
180
- request.body.rewind
181
178
  end
182
179
  rescue JSON::ParserError => jpe
183
180
  log jpe.message + jpe.backtrace.join("\n")
@@ -189,7 +186,6 @@ module Rhosync
189
186
  if params[:version] and params[:version].to_i < 3
190
187
  throw :halt, [404, "Server supports version 3 or higher of the protocol."]
191
188
  end
192
- #log "request params: #{params.inspect}"
193
189
  end
194
190
 
195
191
  %w[get post].each do |verb|
@@ -265,40 +265,50 @@ module Rhosync
265
265
  end
266
266
 
267
267
  def if_need_refresh(client_id=nil,params=nil)
268
- need_refresh = check_refresh_time
268
+ need_refresh = lock(:md) do |s|
269
+ check = check_refresh_time
270
+ self.read_state.prev_refresh_time = self.read_state.refresh_time if check
271
+ self.read_state.refresh_time = Time.now.to_i + self.poll_interval if check
272
+ check
273
+ end
269
274
  yield client_id,params if need_refresh
270
275
  end
271
276
 
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
277
+ def rewind_refresh_time(query_failure)
278
+ return if self.poll_interval == 0
279
+ lock(:md) do |s|
280
+ rewind_time = false
281
+ # reset number of retries
282
+ # and prev_refresh_time on succesfull query
283
+ # or if last refresh was more than 'poll_interval' time ago
284
+ if not query_failure or ((Time.now.to_i - self.read_state.prev_refresh_time) >= self.poll_interval)
285
+ # we need to reset the prev_refresh_time here
286
+ # otherwise in case of expired poll interval
287
+ # and repeating failures - it will reset the counter
288
+ # on every error
289
+ self.read_state.prev_refresh_time = Time.now.to_i
290
+ self.read_state.retry_counter = 0
291
+ end
284
292
 
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
293
+ # rewind the refresh time on failure
294
+ # if retry limit is not reached
295
+ if query_failure
296
+ if self.read_state.retry_counter < self.retry_limit
297
+ self.read_state.increment!(:retry_counter)
298
+ rewind_time = true
299
+ # we have reached the limit - do not rewind the refresh time
300
+ # and reset the counter
301
+ else
302
+ self.read_state.retry_counter = 0
303
+ end
295
304
  end
296
- end
297
- if allowed_update
298
- self.read_state.refresh_time = Time.now.to_i + self.poll_interval
299
- end
305
+
306
+ if rewind_time
307
+ self.read_state.refresh_time = self.read_state.prev_refresh_time
308
+ end
309
+ end
300
310
  end
301
-
311
+
302
312
  private
303
313
  def poll_interval_key
304
314
  "source:#{self.name}:poll_interval"
@@ -41,16 +41,20 @@ module Rhosync
41
41
 
42
42
  def sync
43
43
  if @result and @result.empty?
44
- @source.flash_data(:md)
45
- @source.put_value(:md_size,0)
44
+ @source.lock(:md) do |s|
45
+ s.flash_data(:md)
46
+ s.put_value(:md_size,0)
47
+ end
46
48
  else
47
49
  if @result
48
50
  Store.put_data(@tmp_docname,@result)
49
51
  @stash_size += @result.size
50
52
  end
51
- @source.flash_data(:md)
52
- Store.rename(@tmp_docname,@source.docname(:md))
53
- @source.put_value(:md_size,@stash_size)
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
54
58
  end
55
59
  end
56
60
 
@@ -59,19 +59,19 @@ module Rhosync
59
59
  end
60
60
 
61
61
  def do_query(params=nil)
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)
62
+ result = nil
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')
72
68
  end
69
+ # re-wind refresh time in case of error
70
+ query_failure = Store.exists?(@source.docname(:errors))
71
+ @source.rewind_refresh_time(query_failure)
73
72
  end
74
73
  end
74
+ result
75
75
  end
76
76
 
77
77
  # Enqueue a job for the source based on job type
data/lib/rhosync/store.rb CHANGED
@@ -180,10 +180,23 @@ module Rhosync
180
180
 
181
181
  # Deletes all keys matching a given mask
182
182
  def flash_data(keymask)
183
- @@db.keys(keymask).each do |key|
184
- @@db.del(key)
183
+ if keymask[/[*\[\]?]/]
184
+ # If the keymask contains any pattern matching characters
185
+ # Use keys command to find all keys matching pattern (this is extremely expensive)
186
+ # Then delete matches
187
+ @@db.keys(keymask).each do |key|
188
+ @@db.del(key)
189
+ end
190
+ else
191
+ # The keymask doesn't contain pattern matching characters
192
+ # A delete call is all that is needed
193
+ @@db.del(keymask)
185
194
  end
186
195
  end
196
+
197
+ def exists?(key)
198
+ @@db.exists(key)
199
+ end
187
200
 
188
201
  # Returns array of keys matching a given keymask
189
202
  def get_keys(keymask)
@@ -209,19 +222,27 @@ module Rhosync
209
222
  ts = current_time+(Rhosync.lock_duration || timeout)+1
210
223
  loop do
211
224
  if not @@db.setnx(lock_key,ts)
212
- if raise_on_expire or Rhosync.raise_on_expired_lock
213
- if @@db.get(lock_key).to_i <= current_time
214
- # lock expired before operation which set it up completed
215
- # this process cannot continue without corrupting locked data
216
- raise StoreLockException, "Lock \"#{lock_key}\" expired before it was released"
217
- end
218
- else
219
- if @@db.get(lock_key).to_i <= current_time and
220
- @@db.getset(lock_key,ts).to_i <= current_time
221
- # previous lock expired and we replaced it with our own
222
- break
223
- end
224
- end
225
+ current_lock = @@db.get(lock_key)
226
+ # ensure lock wasn't released between the setnx and get calls
227
+ if current_lock
228
+ current_lock_timeout = current_lock.to_i
229
+ if raise_on_expire or Rhosync.raise_on_expired_lock
230
+ if current_lock_timeout <= current_time
231
+ # lock expired before operation which set it up completed
232
+ # this process cannot continue without corrupting locked data
233
+ raise StoreLockException, "Lock \"#{lock_key}\" expired before it was released"
234
+ end
235
+ else
236
+ if current_lock_timeout <= current_time and
237
+ @@db.getset(lock_key,ts).to_i <= current_time
238
+ # previous lock expired and we replaced it with our own
239
+ break
240
+ end
241
+ end
242
+ # lock was released between setnx and get - try to acquire it again
243
+ elsif @@db.setnx(lock_key,ts)
244
+ break
245
+ end
225
246
  sleep(1)
226
247
  current_time = Time.now.to_i
227
248
  else
@@ -1,3 +1,3 @@
1
1
  module Rhosync
2
- VERSION = '2.1.11'
2
+ VERSION = '2.1.12'
3
3
  end
@@ -3,12 +3,16 @@ require File.join(File.dirname(__FILE__),'api_helper')
3
3
  describe "RhosyncApiStats" do
4
4
  it_should_behave_like "ApiHelper"
5
5
 
6
+ def app
7
+ @app = Rhosync::Server.new
8
+ end
9
+
6
10
  before(:each) do
7
- Rhosync.stats = true
11
+ Rhosync::Server.set :stats, true
8
12
  end
9
13
 
10
14
  after(:each) do
11
- Rhosync.stats = false
15
+ Rhosync::Server.set :stats, false
12
16
  end
13
17
 
14
18
  it "should retrieve metric names" do
@@ -55,6 +59,7 @@ describe "RhosyncApiStats" do
55
59
  end
56
60
 
57
61
  it "should raise error if stats not enabled" do
62
+ Rhosync::Server.set :stats, false
58
63
  Rhosync.stats = false
59
64
  post "/api/stats", {
60
65
  :api_token => @api_token,
@@ -48,10 +48,10 @@ describe "Server" do
48
48
  User.load('new_user').should_not be_nil
49
49
  end
50
50
 
51
- it "should failt to login if wrong content-type" do
51
+ it "should fail to login if wrong content-type" do
52
52
  User.load('unknown').should be_nil
53
53
  post "/login", {"login" => 'unknown', "password" => 'testpass'}.to_json, {'CONTENT_TYPE'=>'application/x-www-form-urlencoded'}
54
- last_response.should be_ok
54
+ last_response.should_not be_ok
55
55
  User.load('unknown').should be_nil
56
56
  end
57
57
 
@@ -255,11 +255,10 @@ describe "Server" do
255
255
  last_response.body.should == "Server error while processing client data"
256
256
  end
257
257
 
258
- it "should handle client posting broken body" do
259
- broken_json = ['foo']
260
- post "/application", broken_json, {'CONTENT_TYPE'=>'application/json'}
261
- last_response.status.should == 500
262
- last_response.body.should == "Internal server error"
258
+ it "should not login if login is empty" do
259
+ post "/application/clientlogin", ''
260
+ last_response.status.should == 401
261
+ last_response.body.should == ""
263
262
  end
264
263
 
265
264
  it "should get inserts json" do
@@ -219,13 +219,13 @@ describe "SourceSync" do
219
219
  # 1) if retry_limit is set to N - then, first N retries should not update refresh_time
220
220
  @s.read_state.retry_counter.should == 1
221
221
  @s.read_state.refresh_time.should <= Time.now.to_i
222
-
222
+
223
223
  # try once more and fail again
224
224
  set_test_data('test_db_storage',{},msg,"query error")
225
225
  res = @ss.do_query
226
226
  verify_result(@s.docname(:md) => {},
227
227
  @s.docname(:errors) => {'query-error'=>{'message'=>msg}})
228
-
228
+
229
229
  # 2) if retry_limit is set to N and number of retries exceeded it - update refresh_time
230
230
  @s.read_state.retry_counter.should == 0
231
231
  @s.read_state.refresh_time.should > Time.now.to_i
@@ -241,7 +241,7 @@ describe "SourceSync" do
241
241
  # 1) if retry_limit is set to N - then, first N retries should not update refresh_time
242
242
  @s.read_state.retry_counter.should == 1
243
243
  @s.read_state.refresh_time.should <= Time.now.to_i
244
-
244
+
245
245
  # try once more (with success)
246
246
  expected = {'1'=>@product1,'2'=>@product2}
247
247
  set_test_data('test_db_storage',expected)
@@ -251,7 +251,40 @@ describe "SourceSync" do
251
251
  @s.read_state.retry_counter.should == 0
252
252
  @s.read_state.refresh_time.should > Time.now.to_i
253
253
  end
254
-
254
+
255
+ it "should reset the retry counter if prev_refresh_time was set more than poll_interval secs ago" do
256
+ @s.retry_limit = 3
257
+ @s.poll_interval = 2
258
+ msg = "Error during query"
259
+ set_test_data('test_db_storage',{},msg,"query error")
260
+ res = @ss.do_query
261
+ verify_result(@s.docname(:md) => {},
262
+ @s.docname(:errors) => {'query-error'=>{'message'=>msg}})
263
+ # 1) if retry_limit is set to N - then, first N retries should not update refresh_time
264
+ @s.read_state.retry_counter.should == 1
265
+ @s.read_state.refresh_time.should <= Time.now.to_i
266
+
267
+ # 2) Make another error - results are the same
268
+ set_test_data('test_db_storage',{},msg,"query error")
269
+ res = @ss.do_query
270
+ verify_result(@s.docname(:md) => {},
271
+ @s.docname(:errors) => {'query-error'=>{'message'=>msg}})
272
+ # 1) if retry_limit is set to N - then, first N retries should not update refresh_time
273
+ @s.read_state.retry_counter.should == 2
274
+ @s.read_state.refresh_time.should <= Time.now.to_i
275
+
276
+ # wait until time interval exprires and prev_refresh_time is too old -
277
+ # this should reset the counter on next request with error
278
+ # and do not update refresh_time
279
+ sleep(3)
280
+ set_test_data('test_db_storage',{},msg,"query error")
281
+ res = @ss.do_query
282
+ verify_result(@s.docname(:md) => {},
283
+ @s.docname(:errors) => {'query-error'=>{'message'=>msg}})
284
+ @s.read_state.retry_counter.should == 1
285
+ @s.read_state.refresh_time.should <= Time.now.to_i
286
+ end
287
+
255
288
  it "should do query with exception raised and update refresh time if retry_limit is 0" do
256
289
  @s.retry_limit = 0
257
290
  msg = "Error during query"
@@ -263,7 +296,7 @@ describe "SourceSync" do
263
296
  @s.read_state.retry_counter.should == 0
264
297
  @s.read_state.refresh_time.should > Time.now.to_i
265
298
  end
266
-
299
+
267
300
  it "should do query with exception raised and update refresh time if poll_interval == 0" do
268
301
  @s.retry_limit = 1
269
302
  @s.poll_interval = 0
@@ -292,6 +325,7 @@ describe "SourceSync" do
292
325
  verify_result("source:#{@test_app_name}:__shared__:#{@s_fields[:name]}:md" => expected)
293
326
  Store.db.keys("read_state:#{@test_app_name}:__shared__*").sort.should ==
294
327
  [ "read_state:#{@test_app_name}:__shared__:SampleAdapter:refresh_time",
328
+ "read_state:#{@test_app_name}:__shared__:SampleAdapter:prev_refresh_time",
295
329
  "read_state:#{@test_app_name}:__shared__:SampleAdapter:retry_counter",
296
330
  "read_state:#{@test_app_name}:__shared__:SampleAdapter:rho__id"].sort
297
331
  end
data/spec/store_spec.rb CHANGED
@@ -167,6 +167,29 @@ describe "Store" do
167
167
  Store.get_data(@s.docname(:md)).should == {}
168
168
  end
169
169
 
170
+ it "should flash_data for all keys matching pattern" do
171
+ keys = ['test_flash_data1','test_flash_data2']
172
+ keys.each {|key| Store.put_data(key,@data)}
173
+ Store.flash_data('test_flash_data*')
174
+ keys.each {|key| Store.get_data(key).should == {} }
175
+ end
176
+
177
+ it "should flash_data without calling KEYS when there aren't pattern matching characters in the provided keymask" do
178
+ key = 'test_flash_data'
179
+ Store.put_data(key,@data)
180
+ Store.db.should_not_receive(:keys)
181
+ Store.db.should_receive(:del).once.with(key).and_return(true)
182
+ Store.flash_data(key)
183
+ end
184
+
185
+ it "should flash_data and call KEYS when there are pattern matching characters in the provided keymask" do
186
+ keys = ['test_flash_data1','test_flash_data2']
187
+ keys.each {|key| Store.put_data(key,@data)}
188
+ Store.db.should_receive(:keys).exactly(1).times.with("test_flash_data*").and_return(keys)
189
+ Store.db.should_receive(:del).exactly(2).times.with(/^test_flash_data[12]$/).and_return(true)
190
+ Store.flash_data("test_flash_data*")
191
+ end
192
+
170
193
  it "should get_keys" do
171
194
  expected = ["doc1:1:1:1:source1", "doc1:1:1:1:source2"]
172
195
  Store.put_data(expected[0],@data)
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: 29
4
+ hash: 19
5
5
  prerelease:
6
6
  segments:
7
7
  - 2
8
8
  - 1
9
- - 11
10
- version: 2.1.11
9
+ - 12
10
+ version: 2.1.12
11
11
  platform: ruby
12
12
  authors:
13
13
  - Rhomobile
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-10-04 00:00:00 Z
18
+ date: 2011-10-14 00:00:00 Z
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
21
  name: sinatra
@@ -23,7 +23,7 @@ dependencies:
23
23
  requirement: &id001 !ruby/object:Gem::Requirement
24
24
  none: false
25
25
  requirements:
26
- - - "="
26
+ - - ">="
27
27
  - !ruby/object:Gem::Version
28
28
  hash: 17
29
29
  segments:
@@ -372,6 +372,7 @@ files:
372
372
  - lib/rhosync/api/upload_file.rb
373
373
  - lib/rhosync/api_token.rb
374
374
  - lib/rhosync/app.rb
375
+ - lib/rhosync/body_content_type_parser.rb
375
376
  - lib/rhosync/bulk_data.rb
376
377
  - lib/rhosync/bulk_data/bulk_data.rb
377
378
  - lib/rhosync/bulk_data/syncdb.index.schema