couchbase-jruby-client 0.1.1

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