couchbase 1.1.5 → 1.2.0.beta

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