couchbase 1.1.5-x86-mingw32 → 1.2.0.beta-x86-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 (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 +102 -12
@@ -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