couchbase-jruby-client 0.1.0-java

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