couchbase-jruby-client 0.1.0-java

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.ruby-version +1 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +203 -0
  6. data/README.md +347 -0
  7. data/Rakefile +10 -0
  8. data/couchbase-jruby-client.gemspec +30 -0
  9. data/lib/couchbase/async/callback.rb +19 -0
  10. data/lib/couchbase/async/queue.rb +26 -0
  11. data/lib/couchbase/async.rb +139 -0
  12. data/lib/couchbase/bucket.rb +663 -0
  13. data/lib/couchbase/cluster.rb +105 -0
  14. data/lib/couchbase/constants.rb +12 -0
  15. data/lib/couchbase/error.rb +28 -0
  16. data/lib/couchbase/jruby/couchbase_client.rb +22 -0
  17. data/lib/couchbase/jruby/future.rb +8 -0
  18. data/lib/couchbase/operations/arithmetic.rb +301 -0
  19. data/lib/couchbase/operations/delete.rb +104 -0
  20. data/lib/couchbase/operations/get.rb +298 -0
  21. data/lib/couchbase/operations/stats.rb +16 -0
  22. data/lib/couchbase/operations/store.rb +468 -0
  23. data/lib/couchbase/operations/touch.rb +123 -0
  24. data/lib/couchbase/operations/utils.rb +49 -0
  25. data/lib/couchbase/operations.rb +23 -0
  26. data/lib/couchbase/result.rb +43 -0
  27. data/lib/couchbase/transcoder.rb +83 -0
  28. data/lib/couchbase/utils.rb +62 -0
  29. data/lib/couchbase/version.rb +3 -0
  30. data/lib/couchbase/view.rb +506 -0
  31. data/lib/couchbase/view_row.rb +272 -0
  32. data/lib/couchbase.rb +177 -0
  33. data/lib/jars/commons-codec-1.5.jar +0 -0
  34. data/lib/jars/couchbase-client-1.2.0-javadoc.jar +0 -0
  35. data/lib/jars/couchbase-client-1.2.0-sources.jar +0 -0
  36. data/lib/jars/couchbase-client-1.2.0.jar +0 -0
  37. data/lib/jars/httpcore-4.1.1.jar +0 -0
  38. data/lib/jars/httpcore-nio-4.1.1.jar +0 -0
  39. data/lib/jars/jettison-1.1.jar +0 -0
  40. data/lib/jars/netty-3.5.5.Final.jar +0 -0
  41. data/lib/jars/spymemcached-2.10.0-javadoc.jar +0 -0
  42. data/lib/jars/spymemcached-2.10.0-sources.jar +0 -0
  43. data/lib/jars/spymemcached-2.10.0.jar +0 -0
  44. data/test/profile/.gitignore +1 -0
  45. data/test/profile/Gemfile +6 -0
  46. data/test/profile/benchmark.rb +195 -0
  47. data/test/setup.rb +201 -0
  48. data/test/test_arithmetic.rb +177 -0
  49. data/test/test_async.rb +324 -0
  50. data/test/test_bucket.rb +213 -0
  51. data/test/test_cas.rb +78 -0
  52. data/test/test_couchbase.rb +29 -0
  53. data/test/test_couchbase_rails_cache_store.rb +341 -0
  54. data/test/test_delete.rb +125 -0
  55. data/test/test_errors.rb +82 -0
  56. data/test/test_format.rb +161 -0
  57. data/test/test_get.rb +417 -0
  58. data/test/test_stats.rb +57 -0
  59. data/test/test_store.rb +216 -0
  60. data/test/test_timer.rb +42 -0
  61. data/test/test_touch.rb +97 -0
  62. data/test/test_unlock.rb +119 -0
  63. data/test/test_utils.rb +58 -0
  64. data/test/test_version.rb +52 -0
  65. metadata +226 -0
@@ -0,0 +1,663 @@
1
+ require 'pry'
2
+
3
+ module Couchbase
4
+
5
+ class Bucket
6
+
7
+ import java.io.IOException
8
+ import java.net.SocketAddress
9
+ import java.net.URI
10
+ import java.net.URISyntaxException
11
+ import java.util.ArrayList
12
+ import java.util.LinkedList
13
+ import java.util.List
14
+ import java.util.concurrent.Future
15
+ import java.util.concurrent.TimeUnit
16
+ import com.couchbase.client.CouchbaseClient
17
+ import com.couchbase.client.CouchbaseConnectionFactory
18
+ import com.couchbase.client.protocol.views.Query
19
+
20
+ include Couchbase::Operations
21
+ include Couchbase::Async
22
+
23
+ attr_accessor :quiet, :hostname, :port, :pool, :bucket, :username,
24
+ :password, :default_ttl, :timeout,
25
+ :default_arithmetic_init, :transcoder
26
+
27
+ attr_reader :client, :key_prefix, :default_format
28
+
29
+ # Initialize new Bucket.
30
+ #
31
+ # @since 1.0.0
32
+ #
33
+ # @overload initialize(url, options = {})
34
+ # Initialize bucket using URI of the cluster and options. It is possible
35
+ # to override some parts of URI using the options keys (e.g. :host or
36
+ # :port)
37
+ #
38
+ # @param [String] url The full URL of management API of the cluster.
39
+ # @param [Hash] options The options for connection. See options definition
40
+ # below.
41
+ #
42
+ # @overload initialize(options = {})
43
+ # Initialize bucket using options only.
44
+ #
45
+ # @param [Hash] options The options for operation for connection
46
+ # @option options [Array] :node_list (nil) the list of nodes to connect
47
+ # to. If specified it takes precedence over +:host+ option. The list
48
+ # must be array of strings in form of host names or host names with
49
+ # ports (in first case port 8091 will be used, see examples).
50
+ # @option options [String] :host ("localhost") the hostname or IP address
51
+ # of the node
52
+ # @option options [Fixnum] :port (8091) the port of the managemenent API
53
+ # @option options [String] :pool ("default") the pool name
54
+ # @option options [String] :bucket ("default") the bucket name
55
+ # @option options [Fixnum] :default_ttl (0) the TTL used by default during
56
+ # storing key-value pairs.
57
+ # @option options [Fixnum] :default_flags (0) the default flags.
58
+ # @option options [Symbol] :default_format (:document) the format, which
59
+ # will be used for values by default. Note that changing format will
60
+ # amend flags. (see {Bucket#default_format})
61
+ # @option options [String] :username (nil) the user name to connect to the
62
+ # cluster. Used to authenticate on management API. The username could
63
+ # be skipped for protected buckets, the bucket name will be used
64
+ # instead.
65
+ # @option options [String] :password (nil) the password of the user.
66
+ # @option options [true, false] :quiet (false) the flag controlling if raising
67
+ # exception when the client executes operations on non-existent keys. If it
68
+ # is +true+ it will raise {Couchbase::Error::NotFound} exceptions. The
69
+ # default behaviour is to return +nil+ value silently (might be useful in
70
+ # Rails cache).
71
+ # @option options [Symbol] :environment (:production) the mode of the
72
+ # connection. Currently it influences only on design documents set. If
73
+ # the environment is +:development+, you will able to get design
74
+ # documents with 'dev_' prefix, otherwise (in +:production+ mode) the
75
+ # library will hide them from you.
76
+ # @option options [String] :key_prefix (nil) the prefix string which will
77
+ # be prepended to each key before sending out, and sripped before
78
+ # returning back to the application.
79
+ # @option options [Fixnum] :timeout (2500000) the timeout for IO
80
+ # operations (in microseconds)
81
+ # @option options [Fixnum, true] :default_arithmetic_init (0) the default
82
+ # initial value for arithmetic operations. Setting this option to any
83
+ # non positive number forces creation missing keys with given default
84
+ # value. Setting it to +true+ will use zero as initial value. (see
85
+ # {Bucket#incr} and {Bucket#decr}).
86
+ # @option options [Symbol] :engine (:default) the IO engine to use
87
+ # Currently following engines are supported:
88
+ # :default :: Built-in engine (multi-thread friendly)
89
+ # :libevent :: libevent IO plugin from libcouchbase (optional)
90
+ # :libev :: libev IO plugin from libcouchbase (optional)
91
+ # :eventmachine :: EventMachine plugin (builtin, but requires EM gem and ruby 1.9+)
92
+ # @option options [true, false] :async (false) If true, the
93
+ # connection instance will be considered always asynchronous and
94
+ # IO interaction will be occured only when {Couchbase::Bucket#run}
95
+ # called. See {Couchbase::Bucket#on_connect} to hook your code
96
+ # after the instance will be connected.
97
+ #
98
+ # @example Initialize connection using default options
99
+ # Couchbase.new
100
+ #
101
+ # @example Select custom bucket
102
+ # Couchbase.new(:bucket => 'foo')
103
+ # Couchbase.new('http://localhost:8091/pools/default/buckets/foo')
104
+ #
105
+ # @example Connect to protected bucket
106
+ # Couchbase.new(:bucket => 'protected', :username => 'protected', :password => 'secret')
107
+ # Couchbase.new('http://localhost:8091/pools/default/buckets/protected',
108
+ # :username => 'protected', :password => 'secret')
109
+ #
110
+ # @example Use list of nodes, in case some nodes might be dead
111
+ # Couchbase.new(:node_list => ['example.com:8091', 'example.org:8091', 'example.net'])
112
+ #
113
+ # @raise [Couchbase::Error::BucketNotFound] if there is no such bucket to
114
+ # connect to
115
+ #
116
+ # @raise [Couchbase::Error::Connect] if the socket wasn't accessible
117
+ # (doesn't accept connections or doesn't respond in time)
118
+ #
119
+ # @return [Bucket]
120
+ #
121
+ def initialize(url = nil, options = {})
122
+ default_options = {
123
+ type: nil,
124
+ quiet: false,
125
+ hostname: 'localhost',
126
+ port: 8091,
127
+ pool: 'default',
128
+ bucket: 'default',
129
+ password: '',
130
+ engine: nil,
131
+ default_ttl: 0,
132
+ async: false,
133
+ default_arithmetic_init: 0,
134
+ default_flags: 0,
135
+ default_format: :document,
136
+ default_observe_timeout: 2500000,
137
+ on_error: nil,
138
+ on_connect: nil,
139
+ timeout: 0,
140
+ environment: nil,
141
+ key_prefix: nil,
142
+ node_list: nil,
143
+ destroying: 0,
144
+ connected: 0,
145
+ on_connect_proc: nil,
146
+ async_disconnect_hook_set: 0,
147
+ connected: false
148
+ }
149
+
150
+ url_options = if url.is_a? String
151
+ raise ArgumentError.new unless url =~ /^http:\/\//
152
+
153
+ uri = URI.new(url)
154
+
155
+ {
156
+ host: uri.host,
157
+ port: uri.port,
158
+ }.merge(path_to_pool_and_bucket(uri.path))
159
+ elsif url.nil?
160
+ {}
161
+ else
162
+ url
163
+ end
164
+
165
+ connection_options = default_options.merge(options).merge(url_options)
166
+
167
+ connection_options.each_pair do |key, value|
168
+ instance_variable_set("@#{key}", value)
169
+ end
170
+
171
+ @transcoder = case @default_format
172
+ when :document
173
+ Transcoder::Document
174
+ when :marshal
175
+ Transcoder::Marshal
176
+ when :plain
177
+ Transcoder::Plain
178
+ end
179
+
180
+ connect unless async?
181
+ end
182
+
183
+ def quiet?
184
+ !!quiet
185
+ end
186
+
187
+ def host
188
+ hostname
189
+ end
190
+
191
+ def connect
192
+ # TODO: doesn't work
193
+ ObjectSpace.define_finalizer(self, -> conn { conn.disconnect })
194
+
195
+ uris = if @node_list
196
+ Array(@node_list).map { |n| URI.new(n) }
197
+ else
198
+ Array(URI.new(base_url))
199
+ end
200
+
201
+ begin
202
+ @connection_factory = CouchbaseConnectionFactory.new(uris, bucket.to_java_string, password.to_java_string)
203
+ @client = CouchbaseClient.new(@connection_factory)
204
+ @connected = true
205
+ rescue Java::ComCouchbaseClientVbucket::ConfigurationException #,
206
+ #Java::Io::IOException
207
+ raise Couchbase::Error::Auth.new
208
+ rescue java.net.ConnectException => e
209
+ raise Couchbase::Error::Connect.new
210
+ end
211
+
212
+ self
213
+ end
214
+ alias_method :reconnect, :connect
215
+
216
+ def authority
217
+ "#{hostname}:#{port}"
218
+ end
219
+
220
+ def base_url
221
+ "http://#{authority}/pools"
222
+ end
223
+
224
+ def url
225
+ "http://#{authority}/pools/#{pool}/buckets/#{bucket}/"
226
+ end
227
+
228
+ def connected?
229
+ @connected
230
+ end
231
+
232
+ def disconnect
233
+ if connected?
234
+ @client.shutdown(3, TimeUnit::SECONDS)
235
+ @client = nil
236
+ @connection_factory = nil
237
+ @connected = false
238
+ else
239
+ raise Couchbase::Error::Connect.new
240
+ end
241
+ end
242
+
243
+ def on_connect(&block)
244
+ @on_connect = block
245
+ end
246
+
247
+ def on_error(&block)
248
+ @on_error = block
249
+ end
250
+
251
+ # Compare and swap value.
252
+ #
253
+ # @since 1.0.0
254
+ #
255
+ # Reads a key's value from the server and yields it to a block. Replaces
256
+ # the key's value with the result of the block as long as the key hasn't
257
+ # been updated in the meantime, otherwise raises
258
+ # {Couchbase::Error::KeyExists}. CAS stands for "compare and swap", and
259
+ # avoids the need for manual key mutexing. Read more info here:
260
+ #
261
+ # In asynchronous mode it will yield result twice, first for
262
+ # {Bucket#get} with {Result#operation} equal to +:get+ and
263
+ # second time for {Bucket#set} with {Result#operation} equal to +:set+.
264
+ #
265
+ # @see http://couchbase.com/docs/memcached-api/memcached-api-protocol-text_cas.html
266
+ #
267
+ # @param [String, Symbol] key
268
+ #
269
+ # @param [Hash] options the options for "swap" part
270
+ # @option options [Fixnum] :ttl (self.default_ttl) the time to live of this key
271
+ # @option options [Symbol] :format (self.default_format) format of the value
272
+ # @option options [Fixnum] :flags (self.default_flags) flags for this key
273
+ #
274
+ # @yieldparam [Object, Result] value old value in synchronous mode and
275
+ # +Result+ object in asynchronous mode.
276
+ # @yieldreturn [Object] new value.
277
+ #
278
+ # @raise [Couchbase::Error::KeyExists] if the key was updated before the the
279
+ # code in block has been completed (the CAS value has been changed).
280
+ # @raise [ArgumentError] if the block is missing for async mode
281
+ #
282
+ # @example Implement append to JSON encoded value
283
+ #
284
+ # c.default_format = :document
285
+ # c.set("foo", {"bar" => 1})
286
+ # c.cas("foo") do |val|
287
+ # val["baz"] = 2
288
+ # val
289
+ # end
290
+ # c.get("foo") #=> {"bar" => 1, "baz" => 2}
291
+ #
292
+ # @example Append JSON encoded value asynchronously
293
+ #
294
+ # c.default_format = :document
295
+ # c.set("foo", {"bar" => 1})
296
+ # c.run do
297
+ # c.cas("foo") do |val|
298
+ # case val.operation
299
+ # when :get
300
+ # val["baz"] = 2
301
+ # val
302
+ # when :set
303
+ # # verify all is ok
304
+ # puts "error: #{ret.error.inspect}" unless ret.success?
305
+ # end
306
+ # end
307
+ # end
308
+ # c.get("foo") #=> {"bar" => 1, "baz" => 2}
309
+ #
310
+ # @return [Fixnum] the CAS of new value
311
+ def cas(key, options = {})
312
+ if async?
313
+ block = Proc.new
314
+ get(key) do |ret|
315
+ val = block.call(ret) # get new value from caller
316
+ set(ret.key, val, options.merge(:cas => ret.cas, :flags => ret.flags), &block)
317
+ end
318
+ else
319
+ val, flags, ver = get(key, :extended => true)
320
+ val = yield(val) # get new value from caller
321
+ set(key, val, options.merge(:cas => ver, :flags => flags))
322
+ end
323
+ end
324
+ alias :compare_and_swap :cas
325
+
326
+ # Fetch design docs stored in current bucket
327
+ #
328
+ # @since 1.2.0
329
+ #
330
+ # @return [Hash]
331
+ def design_docs
332
+ req = make_http_request("/pools/default/buckets/#{bucket}/ddocs",
333
+ :type => :management, :extended => true)
334
+ docmap = {}
335
+ req.on_body do |body|
336
+ res = MultiJson.load(body.value)
337
+ res["rows"].each do |obj|
338
+ if obj['doc']
339
+ obj['doc']['value'] = obj['doc'].delete('json')
340
+ end
341
+ doc = DesignDoc.wrap(self, obj)
342
+ key = doc.id.sub(/^_design\//, '')
343
+ next if self.environment == :production && key =~ /dev_/
344
+ docmap[key] = doc
345
+ end
346
+ yield(docmap) if block_given?
347
+ end
348
+ req.continue
349
+ async? ? nil : docmap
350
+ end
351
+
352
+ # Update or create design doc with supplied views
353
+ #
354
+ # @since 1.2.0
355
+ #
356
+ # @param [Hash, IO, String] data The source object containing JSON
357
+ # encoded design document. It must have +_id+ key set, this key
358
+ # should start with +_design/+.
359
+ #
360
+ # @return [true, false]
361
+ def save_design_doc(data)
362
+ attrs = case data
363
+ when String
364
+ MultiJson.load(data)
365
+ when IO
366
+ MultiJson.load(data.read)
367
+ when Hash
368
+ data
369
+ else
370
+ raise ArgumentError, "Document should be Hash, String or IO instance"
371
+ end
372
+ rv = nil
373
+ id = attrs.delete('_id').to_s
374
+ attrs['language'] ||= 'javascript'
375
+ if id !~ /\A_design\//
376
+ rv = Result.new(:operation => :http_request,
377
+ :key => id,
378
+ :error => ArgumentError.new("'_id' key must be set and start with '_design/'."))
379
+ yield rv if block_given?
380
+ raise rv.error unless async?
381
+ end
382
+ req = make_http_request(id, :body => MultiJson.dump(attrs),
383
+ :method => :put, :extended => true)
384
+ req.on_body do |res|
385
+ rv = res
386
+ val = MultiJson.load(res.value)
387
+ if block_given?
388
+ if res.success? && val['error']
389
+ res.error = Error::View.new("save_design_doc", val['error'])
390
+ end
391
+ yield(res)
392
+ end
393
+ end
394
+ req.continue
395
+ unless async?
396
+ rv.success? or raise res.error
397
+ end
398
+ end
399
+
400
+ # Delete design doc with given id and revision.
401
+ #
402
+ # @since 1.2.0
403
+ #
404
+ # @param [String] id Design document id. It might have '_design/'
405
+ # prefix.
406
+ #
407
+ # @param [String] rev Document revision. It uses latest revision if
408
+ # +rev+ parameter is nil.
409
+ #
410
+ # @return [true, false]
411
+ def delete_design_doc(id, rev = nil)
412
+ ddoc = design_docs[id.sub(/^_design\//, '')]
413
+ unless ddoc
414
+ yield nil if block_given?
415
+ return nil
416
+ end
417
+ path = Utils.build_query(ddoc.id, :rev => rev || ddoc.meta['rev'])
418
+ req = make_http_request(path, :method => :delete, :extended => true)
419
+ rv = nil
420
+ req.on_body do |res|
421
+ rv = res
422
+ val = MultiJson.load(res.value)
423
+ if block_given?
424
+ if res.success? && val['error']
425
+ res.error = Error::View.new("delete_design_doc", val['error'])
426
+ end
427
+ yield(res)
428
+ end
429
+ end
430
+ req.continue
431
+ unless async?
432
+ rv.success? or raise res.error
433
+ end
434
+ end
435
+
436
+ # Delete contents of the bucket
437
+ #
438
+ # @see http://www.couchbase.com/docs/couchbase-manual-2.0/restapi-flushing-bucket.html
439
+ #
440
+ # @since 1.2.0.beta
441
+ #
442
+ # @yieldparam [Result] ret the object with +error+, +status+ and +operation+
443
+ # attributes.
444
+ #
445
+ # @raise [Couchbase::Error::Protocol] in case of an error is
446
+ # encountered. Check {Couchbase::Error::Base#status} for detailed code.
447
+ #
448
+ # @return [true] always return true (see raise section)
449
+ #
450
+ # @example Simple flush the bucket
451
+ # c.flush #=> true
452
+ #
453
+ # @example Asynchronous flush
454
+ # c.run do
455
+ # c.flush do |ret|
456
+ # ret.operation #=> :flush
457
+ # ret.success? #=> true
458
+ # ret.status #=> 200
459
+ # end
460
+ # end
461
+ def flush
462
+ if !async? && block_given?
463
+ sync_block_error
464
+ end
465
+ req = make_http_request("/pools/default/buckets/#{bucket}/controller/doFlush",
466
+ :type => :management, :method => :post, :extended => true)
467
+ res = nil
468
+ req.on_body do |r|
469
+ res = r
470
+ res.instance_variable_set("@operation", :flush)
471
+ yield(res) if block_given?
472
+ end
473
+ req.continue
474
+ true
475
+ end
476
+
477
+ # Create and register one-shot timer
478
+ #
479
+ # @return [Couchbase::Timer]
480
+ def create_timer(interval, &block)
481
+ Timer.new(self, interval, &block)
482
+ end
483
+
484
+ # Create and register periodic timer
485
+ #
486
+ # @return [Couchbase::Timer]
487
+ def create_periodic_timer(interval, &block)
488
+ Timer.new(self, interval, :periodic => true, &block)
489
+ end
490
+
491
+ # Wait for persistence condition
492
+ #
493
+ # @since 1.2.0.dp6
494
+ #
495
+ # This operation is useful when some confidence needed regarding the
496
+ # state of the keys. With two parameters +:replicated+ and +:persisted+
497
+ # it allows to set up the waiting rule.
498
+ #
499
+ # @param [String, Symbol, Array, Hash] keys The list of the keys to
500
+ # observe. Full form is hash with key-cas value pairs, but there are
501
+ # also shortcuts like just Array of keys or single key. CAS value
502
+ # needed to when you need to ensure that the storage persisted exactly
503
+ # the same version of the key you are asking to observe.
504
+ # @param [Hash] options The options for operation
505
+ # @option options [Fixnum] :timeout The timeout in microseconds
506
+ # @option options [Fixnum] :replicated How many replicas should receive
507
+ # the copy of the key.
508
+ # @option options [Fixnum] :persisted How many nodes should store the
509
+ # key on the disk.
510
+ #
511
+ # @raise [Couchbase::Error::Timeout] if the given time is up
512
+ #
513
+ # @return [Fixnum, Hash<String, Fixnum>] will return CAS value just like
514
+ # mutators or pairs key-cas in case of multiple keys.
515
+ def observe_and_wait(*keys, &block)
516
+ options = {:timeout => default_observe_timeout}
517
+ options.update(keys.pop) if keys.size > 1 && keys.last.is_a?(Hash)
518
+ verify_observe_options(options)
519
+ if block && !async?
520
+ raise ArgumentError, "synchronous mode doesn't support callbacks"
521
+ end
522
+ if keys.size == 0
523
+ raise ArgumentError, "at least one key is required"
524
+ end
525
+ if keys.size == 1 && keys[0].is_a?(Hash)
526
+ key_cas = keys[0]
527
+ else
528
+ key_cas = keys.flatten.reduce({}) do |h, kk|
529
+ h[kk] = nil # set CAS to nil
530
+ h
531
+ end
532
+ end
533
+ if async?
534
+ do_observe_and_wait(key_cas, options, &block)
535
+ else
536
+ res = do_observe_and_wait(key_cas, options, &block) while res.nil?
537
+ unless async?
538
+ if keys.size == 1 && (keys[0].is_a?(String) || keys[0].is_a?(Symbol))
539
+ return res.values.first
540
+ else
541
+ return res
542
+ end
543
+ end
544
+ end
545
+ end
546
+
547
+ private
548
+
549
+ def path_to_pool_and_bucket(path)
550
+ {}
551
+ end
552
+
553
+ def verify_observe_options(options)
554
+ unless num_replicas
555
+ raise Couchbase::Error::Libcouchbase, "cannot detect number of the replicas"
556
+ end
557
+ unless options[:persisted] || options[:replicated]
558
+ raise ArgumentError, "either :persisted or :replicated option must be set"
559
+ end
560
+ if options[:persisted] && !(1..num_replicas + 1).include?(options[:persisted])
561
+ raise ArgumentError, "persisted number should be in range (1..#{num_replicas + 1})"
562
+ end
563
+ if options[:replicated] && !(1..num_replicas).include?(options[:replicated])
564
+ raise ArgumentError, "replicated number should be in range (1..#{num_replicas})"
565
+ end
566
+ end
567
+
568
+ def do_observe_and_wait(keys, options, &block)
569
+ acc = Hash.new do |h, k|
570
+ h[k] = Hash.new(0)
571
+ h[k][:cas] = [keys[k]] # first position is for master node
572
+ h[k]
573
+ end
574
+ check_condition = lambda do
575
+ ok = catch :break do
576
+ acc.each do |key, stats|
577
+ master = stats[:cas][0]
578
+ if master.nil?
579
+ # master node doesn't have the key
580
+ throw :break
581
+ end
582
+ if options[:persisted] && (stats[:persisted] < options[:persisted] ||
583
+ stats[:cas].count(master) != options[:persisted])
584
+ throw :break
585
+ end
586
+ if options[:replicated] && (stats[:replicated] < options[:replicated] ||
587
+ stats[:cas].count(master) != options[:replicated] + 1)
588
+ throw :break
589
+ end
590
+ end
591
+ true
592
+ end
593
+ if ok
594
+ if async?
595
+ options[:timer].cancel if options[:timer]
596
+ keys.each do |k, _|
597
+ block.call(Result.new(:key => k,
598
+ :cas => acc[k][:cas][0],
599
+ :operation => :observe_and_wait))
600
+ end
601
+ return :async
602
+ else
603
+ return keys.inject({}){|res, (k, _)| res[k] = acc[k][:cas][0]; res}
604
+ end
605
+ else
606
+ options[:timeout] /= 2
607
+ if options[:timeout] > 0
608
+ if async?
609
+ options[:timer] = create_timer(options[:timeout]) do
610
+ do_observe_and_wait(keys, options, &block)
611
+ end
612
+ return :async
613
+ else
614
+ # do wait for timeout
615
+ run { create_timer(options[:timeout]){} }
616
+ # return nil to avoid recursive call
617
+ return nil
618
+ end
619
+ else
620
+ err = Couchbase::Error::Timeout.new("the observe request was timed out")
621
+ err.instance_variable_set("@operation", :observe_and_wait)
622
+ if async?
623
+ keys.each do |k, _|
624
+ block.call(Result.new(:key => k,
625
+ :cas => acc[k][:cas][0],
626
+ :operation => :observe_and_wait,
627
+ :error => err))
628
+ end
629
+ return :async
630
+ else
631
+ err.instance_variable_set("@key", keys.keys)
632
+ raise err
633
+ end
634
+ end
635
+ end
636
+ end
637
+ collect = lambda do |results|
638
+ results.each do |res|
639
+ if res.completed?
640
+ check_condition.call if async?
641
+ else
642
+ if res.from_master?
643
+ acc[res.key][:cas][0] = res.cas
644
+ else
645
+ acc[res.key][:cas] << res.cas
646
+ end
647
+ acc[res.key][res.status] += 1
648
+ if res.status == :persisted
649
+ acc[res.key][:replicated] += 1
650
+ end
651
+ end
652
+ end
653
+ end
654
+ if async?
655
+ observe(keys.keys, options, &collect)
656
+ else
657
+ observe(keys.keys, options).each{|_, v| collect.call(v)}
658
+ check_condition.call
659
+ end
660
+ end
661
+ end
662
+
663
+ end