couchbase 1.3.3 → 1.3.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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)