jmoses-couchbase 1.3.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. data/.gitignore +15 -0
  2. data/.travis.yml +22 -0
  3. data/.yardopts +5 -0
  4. data/CONTRIBUTING.markdown +75 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE +201 -0
  7. data/Makefile +3 -0
  8. data/README.markdown +665 -0
  9. data/RELEASE_NOTES.markdown +819 -0
  10. data/Rakefile +20 -0
  11. data/couchbase.gemspec +49 -0
  12. data/examples/chat-em/Gemfile +7 -0
  13. data/examples/chat-em/README.markdown +45 -0
  14. data/examples/chat-em/server.rb +82 -0
  15. data/examples/chat-goliath-grape/Gemfile +5 -0
  16. data/examples/chat-goliath-grape/README.markdown +50 -0
  17. data/examples/chat-goliath-grape/app.rb +67 -0
  18. data/examples/chat-goliath-grape/config/app.rb +20 -0
  19. data/examples/transcoders/Gemfile +3 -0
  20. data/examples/transcoders/README.markdown +59 -0
  21. data/examples/transcoders/cb-zcat +40 -0
  22. data/examples/transcoders/cb-zcp +45 -0
  23. data/examples/transcoders/gzip_transcoder.rb +49 -0
  24. data/examples/transcoders/options.rb +54 -0
  25. data/ext/couchbase_ext/.gitignore +4 -0
  26. data/ext/couchbase_ext/arguments.c +956 -0
  27. data/ext/couchbase_ext/arithmetic.c +316 -0
  28. data/ext/couchbase_ext/bucket.c +1373 -0
  29. data/ext/couchbase_ext/context.c +65 -0
  30. data/ext/couchbase_ext/couchbase_ext.c +1364 -0
  31. data/ext/couchbase_ext/couchbase_ext.h +644 -0
  32. data/ext/couchbase_ext/delete.c +163 -0
  33. data/ext/couchbase_ext/eventmachine_plugin.c +452 -0
  34. data/ext/couchbase_ext/extconf.rb +169 -0
  35. data/ext/couchbase_ext/get.c +316 -0
  36. data/ext/couchbase_ext/gethrtime.c +129 -0
  37. data/ext/couchbase_ext/http.c +432 -0
  38. data/ext/couchbase_ext/multithread_plugin.c +1090 -0
  39. data/ext/couchbase_ext/observe.c +171 -0
  40. data/ext/couchbase_ext/plugin_common.c +171 -0
  41. data/ext/couchbase_ext/result.c +129 -0
  42. data/ext/couchbase_ext/stats.c +163 -0
  43. data/ext/couchbase_ext/store.c +542 -0
  44. data/ext/couchbase_ext/timer.c +192 -0
  45. data/ext/couchbase_ext/touch.c +186 -0
  46. data/ext/couchbase_ext/unlock.c +176 -0
  47. data/ext/couchbase_ext/utils.c +551 -0
  48. data/ext/couchbase_ext/version.c +142 -0
  49. data/lib/action_dispatch/middleware/session/couchbase_store.rb +38 -0
  50. data/lib/active_support/cache/couchbase_store.rb +430 -0
  51. data/lib/couchbase.rb +155 -0
  52. data/lib/couchbase/bucket.rb +457 -0
  53. data/lib/couchbase/cluster.rb +119 -0
  54. data/lib/couchbase/connection_pool.rb +58 -0
  55. data/lib/couchbase/constants.rb +12 -0
  56. data/lib/couchbase/result.rb +26 -0
  57. data/lib/couchbase/transcoder.rb +120 -0
  58. data/lib/couchbase/utils.rb +62 -0
  59. data/lib/couchbase/version.rb +21 -0
  60. data/lib/couchbase/view.rb +506 -0
  61. data/lib/couchbase/view_row.rb +272 -0
  62. data/lib/ext/multi_json_fix.rb +56 -0
  63. data/lib/rack/session/couchbase.rb +108 -0
  64. data/tasks/benchmark.rake +6 -0
  65. data/tasks/compile.rake +160 -0
  66. data/tasks/test.rake +100 -0
  67. data/tasks/util.rake +21 -0
  68. data/test/profile/.gitignore +1 -0
  69. data/test/profile/Gemfile +6 -0
  70. data/test/profile/benchmark.rb +195 -0
  71. data/test/setup.rb +178 -0
  72. data/test/test_arithmetic.rb +185 -0
  73. data/test/test_async.rb +316 -0
  74. data/test/test_bucket.rb +276 -0
  75. data/test/test_cas.rb +235 -0
  76. data/test/test_couchbase.rb +77 -0
  77. data/test/test_couchbase_connection_pool.rb +77 -0
  78. data/test/test_couchbase_rails_cache_store.rb +361 -0
  79. data/test/test_delete.rb +120 -0
  80. data/test/test_errors.rb +82 -0
  81. data/test/test_eventmachine.rb +70 -0
  82. data/test/test_format.rb +164 -0
  83. data/test/test_get.rb +407 -0
  84. data/test/test_stats.rb +57 -0
  85. data/test/test_store.rb +216 -0
  86. data/test/test_timer.rb +42 -0
  87. data/test/test_touch.rb +97 -0
  88. data/test/test_unlock.rb +119 -0
  89. data/test/test_utils.rb +58 -0
  90. data/test/test_version.rb +52 -0
  91. metadata +353 -0
@@ -0,0 +1,155 @@
1
+ # Author:: Couchbase <info@couchbase.com>
2
+ # Copyright:: 2011, 2012 Couchbase, Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require 'couchbase/version'
19
+ require 'yaji'
20
+ require 'uri'
21
+ require 'couchbase/transcoder'
22
+ require 'couchbase_ext'
23
+ require 'couchbase/constants'
24
+ require 'couchbase/utils'
25
+ require 'couchbase/bucket'
26
+ require 'couchbase/view_row'
27
+ require 'couchbase/view'
28
+ require 'couchbase/result'
29
+ require 'couchbase/cluster'
30
+
31
+
32
+ # Couchbase ruby client
33
+ module Couchbase
34
+
35
+ if RUBY_VERSION.to_f >= 1.9
36
+ autoload(:ConnectionPool, 'couchbase/connection_pool')
37
+ end
38
+
39
+ class << self
40
+ # The method +connect+ initializes new Bucket instance with all arguments passed.
41
+ #
42
+ # @since 1.0.0
43
+ #
44
+ # @see Bucket#initialize
45
+ #
46
+ # @example Use default values for all options
47
+ # Couchbase.connect
48
+ #
49
+ # @example Establish connection with couchbase default pool and default bucket
50
+ # Couchbase.connect("http://localhost:8091/pools/default")
51
+ #
52
+ # @example Select custom bucket
53
+ # Couchbase.connect("http://localhost:8091/pools/default", :bucket => 'blog')
54
+ #
55
+ # @example Specify bucket credentials
56
+ # Couchbase.connect("http://localhost:8091/pools/default", :bucket => 'blog', :username => 'bucket', :password => 'secret')
57
+ #
58
+ # @example Use URL notation
59
+ # Couchbase.connect("http://bucket:secret@localhost:8091/pools/default/buckets/blog")
60
+ #
61
+ # @return [Bucket] connection instance
62
+ def connect(*options)
63
+ Bucket.new(*(options.flatten))
64
+ end
65
+ alias :new :connect
66
+
67
+ # Default connection options
68
+ #
69
+ # @since 1.1.0
70
+ #
71
+ # @example Using {Couchbase#connection_options} to change the bucket
72
+ # Couchbase.connection_options = {:bucket => 'blog'}
73
+ # Couchbase.bucket.name #=> "blog"
74
+ #
75
+ # @return [Hash, String]
76
+ attr_accessor :connection_options
77
+
78
+ # @private the thread local storage
79
+ def thread_storage
80
+ Thread.current[:couchbase] ||= { :pid => Process.pid, :bucket => {} }
81
+ end
82
+
83
+ # @private resets thread local storage if process ids don't match
84
+ # see 13.3.1: http://www.modrails.com/documentation/Users%20guide%20Apache.html
85
+ def verify_connection!
86
+ reset_thread_storage! if thread_storage[:pid] != Process.pid
87
+ end
88
+
89
+ # @private resets thread local storage
90
+ def reset_thread_storage!
91
+ Thread.current[:couchbase] = nil
92
+ end
93
+
94
+ # The connection instance for current thread
95
+ #
96
+ # @since 1.1.0
97
+ #
98
+ # @see Couchbase.connection_options
99
+ #
100
+ # @example
101
+ # Couchbase.bucket.set("foo", "bar")
102
+ #
103
+ # @example Set connection options using Hash
104
+ # Couchbase.connection_options = {:node_list => ["example.com:8091"]}
105
+ # Couchbase.bucket("slot1").set("foo", "bar")
106
+ # Couchbase.bucket("slot1").bucket #=> "default"
107
+ # Couchbase.connection_options[:bucket] = "test"
108
+ # Couchbase.bucket("slot2").bucket #=> "test"
109
+ #
110
+ # @example Set connection options using URI
111
+ # Couchbase.connection_options = "http://example.com:8091/pools"
112
+ # Couchbase.bucket("slot1").set("foo", "bar")
113
+ # Couchbase.bucket("slot1").bucket #=> "default"
114
+ # Couchbase.connection_options = "http://example.com:8091/pools/buckets/test"
115
+ # Couchbase.bucket("slot2").bucket #=> "test"
116
+ #
117
+ # @example Use named slots to keep a connection
118
+ # Couchbase.connection_options = {
119
+ # :node_list => ["example.com", "example.org"],
120
+ # :bucket => "users"
121
+ # }
122
+ # Couchbase.bucket("users").set("john", {"balance" => 0})
123
+ # Couchbase.connection_options[:bucket] = "orders"
124
+ # Couchbase.bucket("other").set("john:1", {"products" => [42, 66]})
125
+ #
126
+ # @return [Bucket]
127
+ def bucket(name = nil)
128
+ verify_connection!
129
+ name ||= case @connection_options
130
+ when Hash
131
+ @connection_options[:bucket]
132
+ when String
133
+ path = URI.parse(@connection_options).path
134
+ path[%r(^(/pools/([A-Za-z0-9_.-]+)(/buckets/([A-Za-z0-9_.-]+))?)?), 3] || "default"
135
+ else
136
+ "default"
137
+ end
138
+ thread_storage[:bucket][name] ||= connect(connection_options)
139
+ end
140
+
141
+ # Set a connection instance for current thread
142
+ #
143
+ # @since 1.1.0
144
+ #
145
+ # @return [Bucket]
146
+ def bucket=(connection, name = nil)
147
+ verify_connection!
148
+ name ||= @connection_options && @connection_options[:bucket] || "default"
149
+ thread_storage[:bucket][name] = connection
150
+ end
151
+ alias set_bucket bucket=
152
+
153
+ end
154
+
155
+ end
@@ -0,0 +1,457 @@
1
+ # Author:: Couchbase <info@couchbase.com>
2
+ # Copyright:: 2011, 2012 Couchbase, Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ module Couchbase
19
+
20
+ class Bucket
21
+
22
+ # Compare and swap value.
23
+ #
24
+ # @since 1.0.0
25
+ #
26
+ # Reads a key's value from the server and yields it to a block. Replaces
27
+ # the key's value with the result of the block as long as the key hasn't
28
+ # been updated in the meantime, otherwise raises
29
+ # {Couchbase::Error::KeyExists}. CAS stands for "compare and swap", and
30
+ # avoids the need for manual key mutexing. Read more info here:
31
+ #
32
+ # In asynchronous mode it will yield result twice, first for
33
+ # {Bucket#get} with {Result#operation} equal to +:get+ and
34
+ # second time for {Bucket#set} with {Result#operation} equal to +:set+.
35
+ #
36
+ # @see http://couchbase.com/docs/memcached-api/memcached-api-protocol-text_cas.html
37
+ #
38
+ # Setting the +:retry+ option to a positive number will cause this method
39
+ # to rescue the {Couchbase::Error::KeyExists} error that happens when
40
+ # an update collision is detected, and automatically get a fresh copy
41
+ # of the value and retry the block. This will repeat as long as there
42
+ # continues to be conflicts, up to the maximum number of retries specified.
43
+ # For asynchronous mode, this means the block will be yielded once for
44
+ # the initial {Bucket#get}, once for the final {Bucket#set} (successful
45
+ # or last failure), and zero or more additional {Bucket#get} retries
46
+ # in between, up to the maximum allowed by the +:retry+ option.
47
+ #
48
+ # @param [String, Symbol] key
49
+ #
50
+ # @param [Hash] options the options for "swap" part
51
+ # @option options [Fixnum] :ttl (self.default_ttl) the time to live of this key
52
+ # @option options [Symbol] :format (self.default_format) format of the value
53
+ # @option options [Fixnum] :flags (self.default_flags) flags for this key
54
+ # @option options [Fixnum] :retry (0) maximum number of times to autmatically retry upon update collision
55
+ #
56
+ # @yieldparam [Object, Result] value old value in synchronous mode and
57
+ # +Result+ object in asynchronous mode.
58
+ # @yieldreturn [Object] new value.
59
+ #
60
+ # @raise [Couchbase::Error::KeyExists] if the key was updated before the the
61
+ # code in block has been completed (the CAS value has been changed).
62
+ # @raise [ArgumentError] if the block is missing for async mode
63
+ #
64
+ # @example Implement append to JSON encoded value
65
+ #
66
+ # c.default_format = :document
67
+ # c.set("foo", {"bar" => 1})
68
+ # c.cas("foo") do |val|
69
+ # val["baz"] = 2
70
+ # val
71
+ # end
72
+ # c.get("foo") #=> {"bar" => 1, "baz" => 2}
73
+ #
74
+ # @example Append JSON encoded value asynchronously
75
+ #
76
+ # c.default_format = :document
77
+ # c.set("foo", {"bar" => 1})
78
+ # c.run do
79
+ # c.cas("foo") do |val|
80
+ # case val.operation
81
+ # when :get
82
+ # val["baz"] = 2
83
+ # val
84
+ # when :set
85
+ # # verify all is ok
86
+ # puts "error: #{ret.error.inspect}" unless ret.success?
87
+ # end
88
+ # end
89
+ # end
90
+ # c.get("foo") #=> {"bar" => 1, "baz" => 2}
91
+ #
92
+ # @return [Fixnum] the CAS of new value
93
+ def cas(key, options = {})
94
+ retries_remaining = options.delete(:retry) || 0
95
+ if async?
96
+ block = Proc.new
97
+ get(key) do |ret|
98
+ val = block.call(ret) # get new value from caller
99
+ set(ret.key, val, options.merge(:cas => ret.cas, :flags => ret.flags)) do |set_ret|
100
+ if set_ret.error.is_a?(Couchbase::Error::KeyExists) && (retries_remaining > 0)
101
+ cas(key, options.merge(:retry => retries_remaining - 1), &block)
102
+ else
103
+ block.call(set_ret)
104
+ end
105
+ end
106
+ end
107
+ else
108
+ begin
109
+ val, flags, ver = get(key, :extended => true)
110
+ val = yield(val) # get new value from caller
111
+ set(key, val, options.merge(:cas => ver, :flags => flags))
112
+ rescue Couchbase::Error::KeyExists
113
+ if retries_remaining > 0
114
+ retries_remaining -= 1
115
+ retry
116
+ else
117
+ raise
118
+ end
119
+ end
120
+ end
121
+ end
122
+ alias :compare_and_swap :cas
123
+
124
+ # Fetch design docs stored in current bucket
125
+ #
126
+ # @since 1.2.0
127
+ #
128
+ # @return [Hash]
129
+ def design_docs
130
+ req = make_http_request("/pools/default/buckets/#{bucket}/ddocs",
131
+ :type => :management, :extended => true)
132
+ docmap = {}
133
+ req.on_body do |body|
134
+ res = MultiJson.load(body.value)
135
+ res["rows"].each do |obj|
136
+ if obj['doc']
137
+ obj['doc']['value'] = obj['doc'].delete('json')
138
+ end
139
+ doc = DesignDoc.wrap(self, obj)
140
+ key = doc.id.sub(/^_design\//, '')
141
+ next if self.environment == :production && key =~ /dev_/
142
+ docmap[key] = doc
143
+ end
144
+ yield(docmap) if block_given?
145
+ end
146
+ req.continue
147
+ async? ? nil : docmap
148
+ end
149
+
150
+ # Update or create design doc with supplied views
151
+ #
152
+ # @since 1.2.0
153
+ #
154
+ # @param [Hash, IO, String] data The source object containing JSON
155
+ # encoded design document. It must have +_id+ key set, this key
156
+ # should start with +_design/+.
157
+ #
158
+ # @return [true, false]
159
+ def save_design_doc(data)
160
+ attrs = case data
161
+ when String
162
+ MultiJson.load(data)
163
+ when IO
164
+ MultiJson.load(data.read)
165
+ when Hash
166
+ data
167
+ else
168
+ raise ArgumentError, "Document should be Hash, String or IO instance"
169
+ end
170
+ rv = nil
171
+ id = attrs.delete('_id').to_s
172
+ attrs['language'] ||= 'javascript'
173
+ if id !~ /\A_design\//
174
+ rv = Result.new(:operation => :http_request,
175
+ :key => id,
176
+ :error => ArgumentError.new("'_id' key must be set and start with '_design/'."))
177
+ yield rv if block_given?
178
+ raise rv.error unless async?
179
+ end
180
+ req = make_http_request(id, :body => MultiJson.dump(attrs),
181
+ :method => :put, :extended => true)
182
+ req.on_body do |res|
183
+ rv = res
184
+ val = MultiJson.load(res.value)
185
+ if block_given?
186
+ if res.success? && val['error']
187
+ res.error = Error::View.new("save_design_doc", val['error'])
188
+ end
189
+ yield(res)
190
+ end
191
+ end
192
+ req.continue
193
+ unless async?
194
+ rv.success? or raise res.error
195
+ end
196
+ end
197
+
198
+ # Delete design doc with given id and revision.
199
+ #
200
+ # @since 1.2.0
201
+ #
202
+ # @param [String] id Design document id. It might have '_design/'
203
+ # prefix.
204
+ #
205
+ # @param [String] rev Document revision. It uses latest revision if
206
+ # +rev+ parameter is nil.
207
+ #
208
+ # @return [true, false]
209
+ def delete_design_doc(id, rev = nil)
210
+ ddoc = design_docs[id.sub(/^_design\//, '')]
211
+ unless ddoc
212
+ yield nil if block_given?
213
+ return nil
214
+ end
215
+ path = Utils.build_query(ddoc.id, :rev => rev || ddoc.meta['rev'])
216
+ req = make_http_request(path, :method => :delete, :extended => true)
217
+ rv = nil
218
+ req.on_body do |res|
219
+ rv = res
220
+ val = MultiJson.load(res.value)
221
+ if block_given?
222
+ if res.success? && val['error']
223
+ res.error = Error::View.new("delete_design_doc", val['error'])
224
+ end
225
+ yield(res)
226
+ end
227
+ end
228
+ req.continue
229
+ unless async?
230
+ rv.success? or raise res.error
231
+ end
232
+ end
233
+
234
+ # Delete contents of the bucket
235
+ #
236
+ # @see http://www.couchbase.com/docs/couchbase-manual-2.0/restapi-flushing-bucket.html
237
+ #
238
+ # @since 1.2.0.beta
239
+ #
240
+ # @yieldparam [Result] ret the object with +error+, +status+ and +operation+
241
+ # attributes.
242
+ #
243
+ # @raise [Couchbase::Error::Protocol] in case of an error is
244
+ # encountered. Check {Couchbase::Error::Base#status} for detailed code.
245
+ #
246
+ # @return [true] always return true (see raise section)
247
+ #
248
+ # @example Simple flush the bucket
249
+ # c.flush #=> true
250
+ #
251
+ # @example Asynchronous flush
252
+ # c.run do
253
+ # c.flush do |ret|
254
+ # ret.operation #=> :flush
255
+ # ret.success? #=> true
256
+ # ret.status #=> 200
257
+ # end
258
+ # end
259
+ def flush
260
+ if !async? && block_given?
261
+ raise ArgumentError, "synchronous mode doesn't support callbacks"
262
+ end
263
+ req = make_http_request("/pools/default/buckets/#{bucket}/controller/doFlush",
264
+ :type => :management, :method => :post, :extended => true)
265
+ res = nil
266
+ req.on_body do |r|
267
+ res = r
268
+ res.instance_variable_set("@operation", :flush)
269
+ yield(res) if block_given?
270
+ end
271
+ req.continue
272
+ true
273
+ end
274
+
275
+ # Create and register one-shot timer
276
+ #
277
+ # @return [Couchbase::Timer]
278
+ def create_timer(interval, &block)
279
+ Timer.new(self, interval, &block)
280
+ end
281
+
282
+ # Create and register periodic timer
283
+ #
284
+ # @return [Couchbase::Timer]
285
+ def create_periodic_timer(interval, &block)
286
+ Timer.new(self, interval, :periodic => true, &block)
287
+ end
288
+
289
+ # Wait for persistence condition
290
+ #
291
+ # @since 1.2.0.dp6
292
+ #
293
+ # This operation is useful when some confidence needed regarding the
294
+ # state of the keys. With two parameters +:replicated+ and +:persisted+
295
+ # it allows to set up the waiting rule.
296
+ #
297
+ # @param [String, Symbol, Array, Hash] keys The list of the keys to
298
+ # observe. Full form is hash with key-cas value pairs, but there are
299
+ # also shortcuts like just Array of keys or single key. CAS value
300
+ # needed to when you need to ensure that the storage persisted exactly
301
+ # the same version of the key you are asking to observe.
302
+ # @param [Hash] options The options for operation
303
+ # @option options [Fixnum] :timeout The timeout in microseconds
304
+ # @option options [Fixnum] :replicated How many replicas should receive
305
+ # the copy of the key.
306
+ # @option options [Fixnum] :persisted How many nodes should store the
307
+ # key on the disk.
308
+ #
309
+ # @raise [Couchbase::Error::Timeout] if the given time is up
310
+ #
311
+ # @return [Fixnum, Hash<String, Fixnum>] will return CAS value just like
312
+ # mutators or pairs key-cas in case of multiple keys.
313
+ def observe_and_wait(*keys, &block)
314
+ options = {:timeout => default_observe_timeout}
315
+ options.update(keys.pop) if keys.size > 1 && keys.last.is_a?(Hash)
316
+ verify_observe_options(options)
317
+ if block && !async?
318
+ raise ArgumentError, "synchronous mode doesn't support callbacks"
319
+ end
320
+ if keys.size == 0
321
+ raise ArgumentError, "at least one key is required"
322
+ end
323
+ if keys.size == 1 && keys[0].is_a?(Hash)
324
+ key_cas = keys[0]
325
+ else
326
+ key_cas = keys.flatten.reduce({}) do |h, kk|
327
+ h[kk] = nil # set CAS to nil
328
+ h
329
+ end
330
+ end
331
+ if async?
332
+ do_observe_and_wait(key_cas, options, &block)
333
+ else
334
+ res = do_observe_and_wait(key_cas, options, &block) while res.nil?
335
+ unless async?
336
+ if keys.size == 1 && (keys[0].is_a?(String) || keys[0].is_a?(Symbol))
337
+ return res.values.first
338
+ else
339
+ return res
340
+ end
341
+ end
342
+ end
343
+ end
344
+
345
+ private
346
+
347
+ def verify_observe_options(options)
348
+ unless num_replicas
349
+ raise Couchbase::Error::Libcouchbase, "cannot detect number of the replicas"
350
+ end
351
+ unless options[:persisted] || options[:replicated]
352
+ raise ArgumentError, "either :persisted or :replicated option must be set"
353
+ end
354
+ if options[:persisted] && !(1..num_replicas + 1).include?(options[:persisted])
355
+ raise ArgumentError, "persisted number should be in range (1..#{num_replicas + 1})"
356
+ end
357
+ if options[:replicated] && !(1..num_replicas).include?(options[:replicated])
358
+ raise ArgumentError, "replicated number should be in range (1..#{num_replicas})"
359
+ end
360
+ end
361
+
362
+ def do_observe_and_wait(keys, options, &block)
363
+ acc = Hash.new do |h, k|
364
+ h[k] = Hash.new(0)
365
+ h[k][:cas] = [keys[k]] # first position is for master node
366
+ h[k]
367
+ end
368
+ check_condition = lambda do
369
+ ok = catch :break do
370
+ acc.each do |key, stats|
371
+ master = stats[:cas][0]
372
+ if master.nil?
373
+ # master node doesn't have the key
374
+ throw :break
375
+ end
376
+ if options[:persisted] && (stats[:persisted] < options[:persisted] ||
377
+ stats[:cas].count(master) != options[:persisted])
378
+ throw :break
379
+ end
380
+ if options[:replicated] && (stats[:replicated] < options[:replicated] ||
381
+ stats[:cas].count(master) != options[:replicated] + 1)
382
+ throw :break
383
+ end
384
+ end
385
+ true
386
+ end
387
+ if ok
388
+ if async?
389
+ options[:timer].cancel if options[:timer]
390
+ keys.each do |k, _|
391
+ block.call(Result.new(:key => k,
392
+ :cas => acc[k][:cas][0],
393
+ :operation => :observe_and_wait))
394
+ end
395
+ return :async
396
+ else
397
+ return keys.inject({}){|res, (k, _)| res[k] = acc[k][:cas][0]; res}
398
+ end
399
+ else
400
+ options[:timeout] /= 2
401
+ if options[:timeout] > 0
402
+ if async?
403
+ options[:timer] = create_timer(options[:timeout]) do
404
+ do_observe_and_wait(keys, options, &block)
405
+ end
406
+ return :async
407
+ else
408
+ # do wait for timeout
409
+ run { create_timer(options[:timeout]){} }
410
+ # return nil to avoid recursive call
411
+ return nil
412
+ end
413
+ else
414
+ err = Couchbase::Error::Timeout.new("the observe request was timed out")
415
+ err.instance_variable_set("@operation", :observe_and_wait)
416
+ if async?
417
+ keys.each do |k, _|
418
+ block.call(Result.new(:key => k,
419
+ :cas => acc[k][:cas][0],
420
+ :operation => :observe_and_wait,
421
+ :error => err))
422
+ end
423
+ return :async
424
+ else
425
+ err.instance_variable_set("@key", keys.keys)
426
+ raise err
427
+ end
428
+ end
429
+ end
430
+ end
431
+ collect = lambda do |results|
432
+ results.each do |res|
433
+ if res.completed?
434
+ check_condition.call if async?
435
+ else
436
+ if res.from_master?
437
+ acc[res.key][:cas][0] = res.cas
438
+ else
439
+ acc[res.key][:cas] << res.cas
440
+ end
441
+ acc[res.key][res.status] += 1
442
+ if res.status == :persisted
443
+ acc[res.key][:replicated] += 1
444
+ end
445
+ end
446
+ end
447
+ end
448
+ if async?
449
+ observe(keys.keys, options, &collect)
450
+ else
451
+ observe(keys.keys, options).each{|_, v| collect.call(v)}
452
+ check_condition.call
453
+ end
454
+ end
455
+ end
456
+
457
+ end