couchbase 1.3.3 → 1.3.4

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.
@@ -55,6 +55,7 @@ module ActiveSupport
55
55
  options[:default_ttl] ||= options.delete(:expires_in)
56
56
  options[:default_format] ||= :marshal
57
57
  options[:key_prefix] ||= options.delete(:namespace)
58
+ @key_prefix = options[:key_prefix]
58
59
  options[:connection_pool] ||= options.delete(:connection_pool)
59
60
  args.push(options)
60
61
 
@@ -348,7 +349,7 @@ module ActiveSupport
348
349
  # object responds to +cache_key+. Otherwise, to_param method will be
349
350
  # called. If the key is a Hash, then keys will be sorted alphabetically.
350
351
  def expanded_key(key) # :nodoc:
351
- return key.cache_key.to_s if key.respond_to?(:cache_key)
352
+ return validate_key(key.cache_key.to_s) if key.respond_to?(:cache_key)
352
353
 
353
354
  case key
354
355
  when Array
@@ -361,7 +362,22 @@ module ActiveSupport
361
362
  key = key.sort_by { |k,_| k.to_s }.collect{|k,v| "#{k}=#{v}"}
362
363
  end
363
364
 
364
- key.respond_to?(:to_param) ? key.to_param : key
365
+ validate_key(key.respond_to?(:to_param) ? key.to_param : key)
366
+ end
367
+
368
+ def validate_key(key)
369
+ if key_with_prefix(key).length > 250
370
+ key = "#{key[0, max_length_before_prefix]}:md5:#{Digest::MD5.hexdigest(key)}"
371
+ end
372
+ return key
373
+ end
374
+
375
+ def key_with_prefix(key)
376
+ (ns = @key_prefix) ? "#{ns}#{key}" : key
377
+ end
378
+
379
+ def max_length_before_prefix
380
+ @max_length_before_prefix ||= 212 - (@key_prefix || '').size
365
381
  end
366
382
 
367
383
  module Threadsafe
@@ -35,12 +35,23 @@ module Couchbase
35
35
  #
36
36
  # @see http://couchbase.com/docs/memcached-api/memcached-api-protocol-text_cas.html
37
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
+ #
38
48
  # @param [String, Symbol] key
39
49
  #
40
50
  # @param [Hash] options the options for "swap" part
41
51
  # @option options [Fixnum] :ttl (self.default_ttl) the time to live of this key
42
52
  # @option options [Symbol] :format (self.default_format) format of the value
43
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
44
55
  #
45
56
  # @yieldparam [Object, Result] value old value in synchronous mode and
46
57
  # +Result+ object in asynchronous mode.
@@ -80,16 +91,32 @@ module Couchbase
80
91
  #
81
92
  # @return [Fixnum] the CAS of new value
82
93
  def cas(key, options = {})
94
+ retries_remaining = options.delete(:retry) || 0
83
95
  if async?
84
96
  block = Proc.new
85
97
  get(key) do |ret|
86
98
  val = block.call(ret) # get new value from caller
87
- set(ret.key, val, options.merge(:cas => ret.cas, :flags => ret.flags), &block)
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
88
106
  end
89
107
  else
90
- val, flags, ver = get(key, :extended => true)
91
- val = yield(val) # get new value from caller
92
- set(key, val, options.merge(:cas => ver, :flags => flags))
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
93
120
  end
94
121
  end
95
122
  alias :compare_and_swap :cas
@@ -42,11 +42,19 @@ module Couchbase
42
42
  # bucket. Possible values are "memcached" and "couchbase".
43
43
  # @option options [Fixnum] :ram_quota (100) The RAM quota in megabytes.
44
44
  # @option options [Fixnum] :replica_number (1) The number of replicas of
45
- # each document
45
+ # each document. Minimum 0, maximum 3.
46
46
  # @option options [String] :auth_type ("sasl") The authentication type.
47
47
  # Possible values are "sasl" and "none". Note you should specify free
48
48
  # port for "none"
49
49
  # @option options [Fixnum] :proxy_port The port for moxi
50
+ # @option options [true, false] :replica_index (true) Disable or
51
+ # enable indexes for bucket replicas
52
+ # @option options [true, false] :flush_enabled (false) Enables the
53
+ # 'flush all' functionality on the specified bucket.
54
+ # @option options [true, false] :parallel_db_and_view_compaction (false)
55
+ # Indicates whether database and view files on disk can be
56
+ # compacted simultaneously
57
+ #
50
58
  def create_bucket(name, options = {})
51
59
  defaults = {
52
60
  :type => "couchbase",
@@ -54,7 +62,10 @@ module Couchbase
54
62
  :replica_number => 1,
55
63
  :auth_type => "sasl",
56
64
  :sasl_password => "",
57
- :proxy_port => nil
65
+ :proxy_port => nil,
66
+ :flush_enabled => false,
67
+ :replica_index => true,
68
+ :parallel_db_and_view_compaction => false
58
69
  }
59
70
  options = defaults.merge(options)
60
71
  params = {"name" => name}
@@ -64,6 +75,9 @@ module Couchbase
64
75
  params["authType"] = options[:auth_type]
65
76
  params["saslPassword"] = options[:sasl_password]
66
77
  params["proxyPort"] = options[:proxy_port]
78
+ params["flushEnabled"] = !!options[:flush_enabled]
79
+ params["replicaIndex"] = !!options[:replica_index]
80
+ params["parallelDBAndViewCompaction"] = !!options[:parallel_db_and_view_compaction]
67
81
  payload = Utils.encode_params(params.reject!{|k, v| v.nil?})
68
82
  request = @connection.make_http_request("/pools/default/buckets",
69
83
  :content_type => "application/x-www-form-urlencoded",
@@ -17,5 +17,5 @@
17
17
 
18
18
  # Couchbase ruby client
19
19
  module Couchbase
20
- VERSION = "1.3.3"
20
+ VERSION = "1.3.4"
21
21
  end
data/tasks/compile.rake CHANGED
@@ -30,6 +30,36 @@ version_router = lambda do |t|
30
30
  end
31
31
  end
32
32
 
33
+ class Platform
34
+ attr_reader :name, :host, :versions
35
+
36
+ def initialize(params)
37
+ @name = params[:name]
38
+ @host = params[:host]
39
+ @versions = params[:versions]
40
+ end
41
+
42
+ def each_version
43
+ @versions.each do |v|
44
+ yield(v, v[/\d\.\d\.\d/])
45
+ end
46
+ end
47
+
48
+ def short_versions
49
+ res = []
50
+ each_version do |long, short|
51
+ res << short
52
+ end
53
+ res
54
+ end
55
+ end
56
+
57
+ recent = "2.0.0-p353"
58
+ CROSS_PLATFORMS = [
59
+ Platform.new(:name => 'x64-mingw32', :host => 'x86_64-w64-mingw32', :versions => %w(1.9.3-p484 2.0.0-p353 2.1.0)),
60
+ Platform.new(:name => 'x86-mingw32', :host => 'i686-w64-mingw32', :versions => %w(1.8.7-p374 1.9.3-p484 2.0.0-p353 2.1.0)),
61
+ ]
62
+
33
63
  # Setup compile tasks. Configuration can be passed via ENV.
34
64
  # Example:
35
65
  # rake compile with_libcouchbase_include=/opt/couchbase/include
@@ -41,13 +71,14 @@ end
41
71
  #
42
72
  Rake::ExtensionTask.new("couchbase_ext", gemspec) do |ext|
43
73
  ext.cross_compile = true
44
- ext.cross_platform = ENV['HOST'] || "i386-mingw32"
74
+ ext.cross_platform = ENV['TARGET']
45
75
  if ENV['RUBY_CC_VERSION']
46
76
  ext.lib_dir = "lib/couchbase"
47
77
  end
48
78
  ext.cross_compiling do |spec|
49
79
  spec.files.delete("lib/couchbase/couchbase_ext.so")
50
- spec.files.push("lib/couchbase_ext.rb", Dir["lib/couchbase/1.{8,9}/couchbase_ext.so"])
80
+ spec.files.push("lib/couchbase_ext.rb", Dir["lib/couchbase/*/couchbase_ext.so"])
81
+ spec.files.push(Dir["lib/couchbase/*/couchbase_ext.so"])
51
82
  file "#{ext.tmp_dir}/#{ext.cross_platform}/stage/lib/couchbase_ext.rb", &version_router
52
83
  end
53
84
 
@@ -66,19 +97,13 @@ end
66
97
 
67
98
  require 'rubygems/package_task'
68
99
  Gem::PackageTask.new(gemspec) do |pkg|
69
- pkg.need_tar = true
100
+ pkg.need_tar = pkg.need_zip = false
70
101
  end
71
102
 
72
103
  require 'mini_portile'
73
104
  require 'rake/extensioncompiler'
74
105
 
75
106
  class MiniPortile
76
- alias :initialize_with_default_host :initialize
77
- def initialize(name, version)
78
- initialize_with_default_host(name, version)
79
- @host = ENV['HOST'] || Rake::ExtensionCompiler.mingw_host
80
- end
81
-
82
107
  alias :cook_without_checkpoint :cook
83
108
  def cook
84
109
  checkpoint = "ports/.#{name}-#{version}-#{host}.installed"
@@ -89,13 +114,30 @@ class MiniPortile
89
114
  end
90
115
  end
91
116
 
92
- namespace :ports do
93
- directory "ports"
117
+ file "lib/couchbase_ext.rb", &version_router
94
118
 
95
- task :libcouchbase => ["ports"] do
96
- recipe = MiniPortile.new "libcouchbase", "2.1.3"
119
+ desc "Package gem for windows"
120
+ task "package:windows" => ["package", "lib/couchbase_ext.rb"] do
121
+ vars = [
122
+ 'CC',
123
+ 'CFLAGS',
124
+ 'CPATH',
125
+ 'CPP',
126
+ 'CPPFLAGS',
127
+ 'LDFLAGS',
128
+ 'LIBRARY_PATH',
129
+ 'PATH'
130
+ ].reduce({}) do |h, v|
131
+ h[v] = ENV[v]
132
+ h
133
+ end
134
+ CROSS_PLATFORMS.each do |platform|
135
+ ENV['TARGET'] = platform.name
136
+ rm_rf("tmp/ ports/")
137
+ mkdir_p("ports")
138
+ recipe = MiniPortile.new("libcouchbase", "2.2.0_30_gc87bec4")
139
+ recipe.host = platform.host
97
140
  recipe.files << "http://packages.couchbase.com/clients/c/libcouchbase-#{recipe.version}.tar.gz"
98
-
99
141
  recipe.configure_options.push("--disable-debug",
100
142
  "--disable-dependency-tracking",
101
143
  "--disable-couchbasemock",
@@ -105,16 +147,12 @@ namespace :ports do
105
147
  "--disable-tools")
106
148
  recipe.cook
107
149
  recipe.activate
150
+ platform.each_version do |long, short|
151
+ sh("env RUBY_CC_VERSION=#{short} RBENV_VERSION=#{long} rbenv exec rake cross compile")
152
+ end
153
+ vars.each do |k, v|
154
+ ENV[k] = v
155
+ end
156
+ sh("env RUBY_CC_VERSION=#{platform.short_versions.join(":")} RBENV_VERSION=#{recent} rbenv exec rake cross native gem")
108
157
  end
109
158
  end
110
-
111
- file "lib/couchbase_ext.rb", &version_router
112
- task :cross => ["lib/couchbase_ext.rb", "ports:libcouchbase"]
113
-
114
- desc "Package gem for windows"
115
- task "package:windows" => :package do
116
- sh("env RUBY_CC_VERSION=1.8.7 RBENV_VERSION=1.8.7-p370 rbenv exec bundle exec rake cross compile")
117
- sh("env RUBY_CC_VERSION=1.9.2 RBENV_VERSION=1.9.2-p320 rbenv exec bundle exec rake cross compile")
118
- sh("env RUBY_CC_VERSION=2.0.0 RBENV_VERSION=2.0.0-p247 rbenv exec bundle exec rake cross compile")
119
- sh("env RUBY_CC_VERSION=1.8.7:1.9.2:2.0.0 RBENV_VERSION=1.9.2-p320 rbenv exec bundle exec rake cross native gem")
120
- end
@@ -173,4 +173,13 @@ class TestArithmetic < MiniTest::Test
173
173
  assert_equal [2, 2], connection.decr(uniq_id(:foo), uniq_id(:bar), :delta => 10).values.sort
174
174
  assert_equal [1, 1], connection.decr(uniq_id(:foo), uniq_id(:bar)).values.sort
175
175
  end
176
+
177
+ def test_it_returns_cas_value_in_extended_mode
178
+ connection = Couchbase.new(:hostname => @mock.host, :port => @mock.port)
179
+ orig_cas = connection.set(uniq_id(:foo), 1)
180
+ val, cas = connection.incr(uniq_id(:foo), :extended => true)
181
+ assert_equal 2, val
182
+ assert cas.is_a?(Numeric), "CAS should be numeric value: #{cas.inspect}"
183
+ refute_equal orig_cas, cas
184
+ end
176
185
  end
data/test/test_cas.rb CHANGED
@@ -40,6 +40,69 @@ class TestCas < MiniTest::Test
40
40
  assert_equal expected, val
41
41
  end
42
42
 
43
+ def test_compare_and_swap_collision
44
+ connection = Couchbase.new(:hostname => @mock.host, :port => @mock.port,
45
+ :default_format => :document)
46
+ connection.set(uniq_id, {"bar" => 1})
47
+ assert_raises(Couchbase::Error::KeyExists) do
48
+ connection.cas(uniq_id) do |val|
49
+ # Simulate collision with a separate writer. This will
50
+ # change the CAS value to be different than what #cas just loaded.
51
+ connection.set(uniq_id, {"bar" => 2})
52
+
53
+ # Complete the modification we desire, which should fail when set.
54
+ val["baz"] = 3
55
+ val
56
+ end
57
+ end
58
+ end
59
+
60
+ def test_compare_and_swap_retry
61
+ connection = Couchbase.new(:hostname => @mock.host, :port => @mock.port,
62
+ :default_format => :document)
63
+ connection.set(uniq_id, {"bar" => 1})
64
+ calls = 0
65
+ connection.cas(uniq_id, :retry => 1) do |val|
66
+ calls += 1
67
+ if calls == 1
68
+ # Simulate collision with a separate writer. This will
69
+ # change the CAS value to be different than what #cas just loaded.
70
+ # Only do this the first time this block is executed.
71
+ connection.set(uniq_id, {"bar" => 2})
72
+ end
73
+
74
+ # Complete the modification we desire, which should fail when set.
75
+ val["baz"] = 3
76
+ val
77
+ end
78
+ assert_equal 2, calls
79
+ val = connection.get(uniq_id)
80
+ expected = {"bar" => 2, "baz" => 3}
81
+ assert_equal expected, val
82
+ end
83
+
84
+ def test_compare_and_swap_too_many_retries
85
+ connection = Couchbase.new(:hostname => @mock.host, :port => @mock.port,
86
+ :default_format => :document)
87
+ connection.set(uniq_id, {"bar" => 0})
88
+ calls = 0
89
+ assert_raises(Couchbase::Error::KeyExists) do
90
+ connection.cas(uniq_id, :retry => 10) do |val|
91
+ calls += 1
92
+
93
+ # Simulate collision with a separate writer. This will
94
+ # change the CAS value to be different than what #cas just loaded.
95
+ # Do it every time so we just keep retrying and failing.
96
+ connection.set(uniq_id, {"bar" => calls})
97
+
98
+ # Complete the modification we desire, which should fail when set.
99
+ val["baz"] = 3
100
+ val
101
+ end
102
+ end
103
+ assert_equal 11, calls
104
+ end
105
+
43
106
  def test_compare_and_swap_async
44
107
  connection = Couchbase.new(:hostname => @mock.host, :port => @mock.port,
45
108
  :default_format => :document)
@@ -66,6 +129,101 @@ class TestCas < MiniTest::Test
66
129
  assert_equal expected, val
67
130
  end
68
131
 
132
+ def test_compare_and_swap_async_collision
133
+ connection = Couchbase.new(:hostname => @mock.host, :port => @mock.port,
134
+ :default_format => :document)
135
+ connection.set(uniq_id, {"bar" => 1})
136
+ calls = 0
137
+ connection.run do |conn|
138
+ conn.cas(uniq_id) do |ret|
139
+ calls += 1
140
+ case ret.operation
141
+ when :get
142
+ new_val = ret.value
143
+
144
+ # Simulate collision with a separate writer. This will
145
+ # change the CAS value to be different than what #cas just loaded.
146
+ connection.set(uniq_id, {"bar" => 2})
147
+
148
+
149
+ # Complete the modification we desire, which should fail when set.
150
+ new_val["baz"] = 3
151
+ new_val
152
+ when :set
153
+ assert ret.error.is_a? Couchbase::Error::KeyExists
154
+ else
155
+ flunk "Unexpected operation: #{ret.operation.inspect}"
156
+ end
157
+ end
158
+ end
159
+ assert_equal 2, calls
160
+ end
161
+
162
+ def test_compare_and_swap_async_retry
163
+ connection = Couchbase.new(:hostname => @mock.host, :port => @mock.port,
164
+ :default_format => :document)
165
+ connection.set(uniq_id, {"bar" => 1})
166
+ calls = 0
167
+ connection.run do |conn|
168
+ conn.cas(uniq_id, :retry => 1) do |ret|
169
+ calls += 1
170
+ case ret.operation
171
+ when :get
172
+ new_val = ret.value
173
+
174
+ if calls == 1
175
+ # Simulate collision with a separate writer. This will
176
+ # change the CAS value to be different than what #cas just loaded.
177
+ # Only do this the first time this block is executed.
178
+ connection.set(uniq_id, {"bar" => 2})
179
+ end
180
+
181
+ # Complete the modification we desire, which should fail when set.
182
+ new_val["baz"] = 3
183
+ new_val
184
+ when :set
185
+ assert ret.success?
186
+ else
187
+ flunk "Unexpected operation: #{ret.operation.inspect}"
188
+ end
189
+ end
190
+ end
191
+ assert_equal 3, calls
192
+ val = connection.get(uniq_id)
193
+ expected = {"bar" => 2, "baz" => 3}
194
+ assert_equal expected, val
195
+ end
196
+
197
+ def test_compare_and_swap_async_too_many_retries
198
+ connection = Couchbase.new(:hostname => @mock.host, :port => @mock.port,
199
+ :default_format => :document)
200
+ connection.set(uniq_id, {"bar" => 0})
201
+ calls = 0
202
+ connection.run do |conn|
203
+ conn.cas(uniq_id, :retry => 10) do |ret|
204
+ calls += 1
205
+ case ret.operation
206
+ when :get
207
+ new_val = ret.value
208
+
209
+ # Simulate collision with a separate writer. This will
210
+ # change the CAS value to be different than what #cas just loaded.
211
+ # Do it every time so we just keep retrying and failing.
212
+ connection.set(uniq_id, {"bar" => calls})
213
+
214
+ # Complete the modification we desire, which should fail when set.
215
+ new_val["baz"] = 3
216
+ new_val
217
+ when :set
218
+ assert ret.error.is_a? Couchbase::Error::KeyExists
219
+ else
220
+ flunk "Unexpected operation: #{ret.operation.inspect}"
221
+ end
222
+ end
223
+ end
224
+ assert_equal 12, calls
225
+ end
226
+
69
227
  def test_flags_replication
70
228
  connection = Couchbase.new(:hostname => @mock.host, :port => @mock.port,
71
229
  :default_format => :document)