couchbase-jruby-client 0.1.1

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