couchbase 1.3.4-x64-mingw32

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 (92) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.travis.yml +22 -0
  4. data/.yardopts +5 -0
  5. data/CONTRIBUTING.markdown +75 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE +201 -0
  8. data/Makefile +3 -0
  9. data/README.markdown +649 -0
  10. data/RELEASE_NOTES.markdown +796 -0
  11. data/Rakefile +20 -0
  12. data/couchbase.gemspec +49 -0
  13. data/examples/chat-em/Gemfile +7 -0
  14. data/examples/chat-em/README.markdown +45 -0
  15. data/examples/chat-em/server.rb +82 -0
  16. data/examples/chat-goliath-grape/Gemfile +5 -0
  17. data/examples/chat-goliath-grape/README.markdown +50 -0
  18. data/examples/chat-goliath-grape/app.rb +67 -0
  19. data/examples/chat-goliath-grape/config/app.rb +20 -0
  20. data/examples/transcoders/Gemfile +3 -0
  21. data/examples/transcoders/README.markdown +59 -0
  22. data/examples/transcoders/cb-zcat +40 -0
  23. data/examples/transcoders/cb-zcp +45 -0
  24. data/examples/transcoders/gzip_transcoder.rb +49 -0
  25. data/examples/transcoders/options.rb +54 -0
  26. data/ext/couchbase_ext/.gitignore +4 -0
  27. data/ext/couchbase_ext/arguments.c +956 -0
  28. data/ext/couchbase_ext/arithmetic.c +307 -0
  29. data/ext/couchbase_ext/bucket.c +1370 -0
  30. data/ext/couchbase_ext/context.c +65 -0
  31. data/ext/couchbase_ext/couchbase_ext.c +1364 -0
  32. data/ext/couchbase_ext/couchbase_ext.h +644 -0
  33. data/ext/couchbase_ext/delete.c +163 -0
  34. data/ext/couchbase_ext/eventmachine_plugin.c +452 -0
  35. data/ext/couchbase_ext/extconf.rb +168 -0
  36. data/ext/couchbase_ext/get.c +316 -0
  37. data/ext/couchbase_ext/gethrtime.c +129 -0
  38. data/ext/couchbase_ext/http.c +432 -0
  39. data/ext/couchbase_ext/multithread_plugin.c +1090 -0
  40. data/ext/couchbase_ext/observe.c +171 -0
  41. data/ext/couchbase_ext/plugin_common.c +171 -0
  42. data/ext/couchbase_ext/result.c +129 -0
  43. data/ext/couchbase_ext/stats.c +163 -0
  44. data/ext/couchbase_ext/store.c +542 -0
  45. data/ext/couchbase_ext/timer.c +192 -0
  46. data/ext/couchbase_ext/touch.c +186 -0
  47. data/ext/couchbase_ext/unlock.c +176 -0
  48. data/ext/couchbase_ext/utils.c +551 -0
  49. data/ext/couchbase_ext/version.c +142 -0
  50. data/lib/action_dispatch/middleware/session/couchbase_store.rb +38 -0
  51. data/lib/active_support/cache/couchbase_store.rb +430 -0
  52. data/lib/couchbase.rb +155 -0
  53. data/lib/couchbase/bucket.rb +457 -0
  54. data/lib/couchbase/cluster.rb +119 -0
  55. data/lib/couchbase/connection_pool.rb +58 -0
  56. data/lib/couchbase/constants.rb +12 -0
  57. data/lib/couchbase/result.rb +26 -0
  58. data/lib/couchbase/transcoder.rb +120 -0
  59. data/lib/couchbase/utils.rb +62 -0
  60. data/lib/couchbase/version.rb +21 -0
  61. data/lib/couchbase/view.rb +506 -0
  62. data/lib/couchbase/view_row.rb +272 -0
  63. data/lib/ext/multi_json_fix.rb +56 -0
  64. data/lib/rack/session/couchbase.rb +108 -0
  65. data/tasks/benchmark.rake +6 -0
  66. data/tasks/compile.rake +158 -0
  67. data/tasks/test.rake +100 -0
  68. data/tasks/util.rake +21 -0
  69. data/test/profile/.gitignore +1 -0
  70. data/test/profile/Gemfile +6 -0
  71. data/test/profile/benchmark.rb +195 -0
  72. data/test/setup.rb +178 -0
  73. data/test/test_arithmetic.rb +185 -0
  74. data/test/test_async.rb +316 -0
  75. data/test/test_bucket.rb +250 -0
  76. data/test/test_cas.rb +235 -0
  77. data/test/test_couchbase.rb +77 -0
  78. data/test/test_couchbase_connection_pool.rb +77 -0
  79. data/test/test_couchbase_rails_cache_store.rb +361 -0
  80. data/test/test_delete.rb +120 -0
  81. data/test/test_errors.rb +82 -0
  82. data/test/test_eventmachine.rb +70 -0
  83. data/test/test_format.rb +164 -0
  84. data/test/test_get.rb +407 -0
  85. data/test/test_stats.rb +57 -0
  86. data/test/test_store.rb +216 -0
  87. data/test/test_timer.rb +42 -0
  88. data/test/test_touch.rb +97 -0
  89. data/test/test_unlock.rb +119 -0
  90. data/test/test_utils.rb +58 -0
  91. data/test/test_version.rb +52 -0
  92. metadata +336 -0
data/lib/couchbase.rb ADDED
@@ -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