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
@@ -0,0 +1,147 @@
1
+ /* vim: ft=c et ts=8 sts=4 sw=4 cino=
2
+ *
3
+ * Copyright 2011, 2012 Couchbase, Inc.
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
+ #include "couchbase_ext.h"
19
+
20
+ void
21
+ version_callback(lcb_t handle, const void *cookie, lcb_error_t error, const lcb_server_version_resp_t *resp)
22
+ {
23
+ struct context_st *ctx = (struct context_st *)cookie;
24
+ struct bucket_st *bucket = ctx->bucket;
25
+ VALUE node, val, *rv = ctx->rv, exc, res;
26
+
27
+ node = resp->v.v0.server_endpoint ? STR_NEW_CSTR(resp->v.v0.server_endpoint) : Qnil;
28
+ exc = cb_check_error(error, "failed to get version", node);
29
+ if (exc != Qnil) {
30
+ rb_ivar_set(exc, id_iv_operation, sym_version);
31
+ if (NIL_P(ctx->exception)) {
32
+ ctx->exception = cb_gc_protect(bucket, exc);
33
+ }
34
+ }
35
+
36
+ if (node != Qnil) {
37
+ val = STR_NEW((const char*)resp->v.v0.vstring, resp->v.v0.nvstring);
38
+ if (bucket->async) { /* asynchronous */
39
+ if (ctx->proc != Qnil) {
40
+ res = rb_class_new_instance(0, NULL, cResult);
41
+ rb_ivar_set(res, id_iv_error, exc);
42
+ rb_ivar_set(res, id_iv_operation, sym_version);
43
+ rb_ivar_set(res, id_iv_node, node);
44
+ rb_ivar_set(res, id_iv_value, val);
45
+ cb_proc_call(ctx->proc, 1, res);
46
+ }
47
+ } else { /* synchronous */
48
+ if (NIL_P(exc)) {
49
+ rb_hash_aset(*rv, node, val);
50
+ }
51
+ }
52
+ } else {
53
+ ctx->nqueries--;
54
+ cb_gc_unprotect(bucket, ctx->proc);
55
+ }
56
+
57
+ (void)handle;
58
+ }
59
+
60
+ /*
61
+ * Returns versions of the server for each node in the cluster
62
+ *
63
+ * @since 1.1.0
64
+ *
65
+ * @overload version
66
+ * @yieldparam [Result] ret the object with +error+, +node+, +operation+
67
+ * and +value+ attributes.
68
+ *
69
+ * @return [Hash] node-version pairs
70
+ *
71
+ * @raise [Couchbase::Error::Connect] if connection closed (see {Bucket#reconnect})
72
+ * @raise [ArgumentError] when passing the block in synchronous mode
73
+ *
74
+ * @example Synchronous version request
75
+ * c.version #=> will render version
76
+ *
77
+ * @example Asynchronous version request
78
+ * c.run do
79
+ * c.version do |ret|
80
+ * ret.operation #=> :version
81
+ * ret.success? #=> true
82
+ * ret.node #=> "localhost:11211"
83
+ * ret.value #=> will render version
84
+ * end
85
+ * end
86
+ */
87
+ VALUE
88
+ cb_bucket_version(int argc, VALUE *argv, VALUE self)
89
+ {
90
+ struct bucket_st *bucket = DATA_PTR(self);
91
+ struct context_st *ctx;
92
+ VALUE rv, exc, args, proc;
93
+ lcb_error_t err;
94
+ struct params_st params;
95
+
96
+ if (bucket->handle == NULL) {
97
+ rb_raise(eConnectError, "closed connection");
98
+ }
99
+ rb_scan_args(argc, argv, "0*&", &args, &proc);
100
+ if (!bucket->async && proc != Qnil) {
101
+ rb_raise(rb_eArgError, "synchronous mode doesn't support callbacks");
102
+ }
103
+ memset(&params, 0, sizeof(struct params_st));
104
+ params.type = cmd_version;
105
+ params.bucket = bucket;
106
+ cb_params_build(&params, RARRAY_LEN(args), args);
107
+ ctx = xcalloc(1, sizeof(struct context_st));
108
+ if (ctx == NULL) {
109
+ rb_raise(eClientNoMemoryError, "failed to allocate memory for context");
110
+ }
111
+ rv = rb_hash_new();
112
+ ctx->rv = &rv;
113
+ ctx->bucket = bucket;
114
+ ctx->exception = Qnil;
115
+ ctx->proc = cb_gc_protect(bucket, proc);
116
+ ctx->nqueries = params.cmd.version.num;
117
+ err = lcb_server_versions(bucket->handle, (const void *)ctx,
118
+ params.cmd.version.num, params.cmd.version.ptr);
119
+ exc = cb_check_error(err, "failed to schedule version request", Qnil);
120
+ cb_params_destroy(&params);
121
+ if (exc != Qnil) {
122
+ xfree(ctx);
123
+ rb_exc_raise(exc);
124
+ }
125
+ bucket->nbytes += params.npayload;
126
+ if (bucket->async) {
127
+ maybe_do_loop(bucket);
128
+ return Qnil;
129
+ } else {
130
+ if (ctx->nqueries > 0) {
131
+ /* we have some operations pending */
132
+ lcb_wait(bucket->handle);
133
+ }
134
+ exc = ctx->exception;
135
+ xfree(ctx);
136
+ if (exc != Qnil) {
137
+ cb_gc_unprotect(bucket, exc);
138
+ rb_exc_raise(exc);
139
+ }
140
+ if (bucket->exception != Qnil) {
141
+ rb_exc_raise(bucket->exception);
142
+ }
143
+ return rv;
144
+ }
145
+ }
146
+
147
+
@@ -0,0 +1,38 @@
1
+ require 'active_support/cache'
2
+ require 'action_dispatch/middleware/session/abstract_store'
3
+ require 'rack/session/couchbase'
4
+ require 'couchbase'
5
+
6
+ module ActionDispatch
7
+ module Session
8
+
9
+ # This is Couchbase-powered session store for Rails applications
10
+ #
11
+ # To use it just update your `config/initializers/session_store.rb` file
12
+ #
13
+ # require 'action_dispatch/middleware/session/couchbase_store'
14
+ # AppName::Application.config.session_store :couchbase_store
15
+ #
16
+ # Or remove this file and add following line to your `config/application.rb`:
17
+ #
18
+ # require 'action_dispatch/middleware/session/couchbase_store'
19
+ # config.session_storage = :couchbase_store
20
+ #
21
+ # You can also pass additional options:
22
+ #
23
+ # require 'action_dispatch/middleware/session/couchbase_store'
24
+ # session_options = {
25
+ # :expire_after => 5.minutes,
26
+ # :couchbase => {:bucket => "sessions", :default_format => :marshal}
27
+ # }
28
+ # config.session_storage = :couchbase_store, session_options
29
+ #
30
+ # By default sessions will be serialized to JSON, to allow analyse them
31
+ # using Map/Reduce.
32
+ #
33
+ class CouchbaseStore < Rack::Session::Couchbase
34
+ include Compatibility
35
+ include StaleSessionCheck
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,356 @@
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'
19
+ require 'securerandom'
20
+ require 'active_support/core_ext/array/extract_options'
21
+ require 'active_support/cache'
22
+
23
+ module ActiveSupport
24
+ module Cache
25
+ # This class implements Cache interface for Rails. To use it just
26
+ # put following line in your config/application.rb file:
27
+ #
28
+ # config.cache_store = :couchbase_store
29
+ #
30
+ # You can also pass additional connection options there
31
+ #
32
+ # cache_options = {
33
+ # :bucket => 'protected',
34
+ # :username => 'protected',
35
+ # :password => 'secret',
36
+ # :expires_in => 30.seconds
37
+ # }
38
+ # config.cache_store = :couchbase_store, cache_options
39
+ class CouchbaseStore < Store
40
+
41
+ # Creates a new CouchbaseStore object, with the given options. For
42
+ # more info see {{Couchbase::Bucket#initialize}}
43
+ #
44
+ # ActiveSupport::Cache::CouchbaseStore.new(:bucket => "cache")
45
+ #
46
+ # If no options are specified, then CouchbaseStore will connect to
47
+ # localhost port 8091 (default Couchbase Server port) and will use
48
+ # bucket named "default" which is always open for unauthorized access
49
+ # (if exists).
50
+ def initialize(*args)
51
+ args = [*(args.flatten)]
52
+ options = args.extract_options! || {}
53
+ @raise_errors = !options[:quiet] = !options.delete(:raise_errors)
54
+ options[:default_ttl] ||= options.delete(:expires_in)
55
+ options[:default_format] ||= :marshal
56
+ options[:key_prefix] ||= options.delete(:namespace)
57
+ args.push(options)
58
+ @data = ::Couchbase::Bucket.new(*args)
59
+ end
60
+
61
+ # Fetches data from the cache, using the given key.
62
+ #
63
+ # @since 1.2.0.dp5
64
+ #
65
+ # If there is data in the cache with the given key, then that data is
66
+ # returned. If there is no such data in the cache (a cache miss),
67
+ # then nil will be returned. However, if a block has been passed, that
68
+ # block will be run in the event of a cache miss. The return value of
69
+ # the block will be written to the cache under the given cache key,
70
+ # and that return value will be returned.
71
+ #
72
+ # @param [String] name name for the key
73
+ # @param [Hash] options
74
+ # @option options [true, false] :force if this option is +true+ it
75
+ # will force cache miss.
76
+ # @option options [Fixnum] :expires_in the expiration time on the
77
+ # cache in seconds. Values larger than 30*24*60*60 seconds (30 days)
78
+ # are interpreted as absolute times (from the epoch).
79
+ # @option options [true, false] :unless_exists if this option is +true+
80
+ # it will write value only if the key doesn't exist in the database
81
+ # (it accepts +:unless_exist+ too).
82
+ #
83
+ # @return [Object]
84
+ def fetch(name, options = nil)
85
+ options ||= {}
86
+ name = expanded_key(name)
87
+
88
+ if block_given?
89
+ unless options[:force]
90
+ entry = instrument(:read, name, options) do |payload|
91
+ payload[:super_operation] = :fetch if payload
92
+ read_entry(name, options)
93
+ end
94
+ end
95
+
96
+ if !entry.nil?
97
+ instrument(:fetch_hit, name, options) { |payload| }
98
+ entry
99
+ else
100
+ result = instrument(:generate, name, options) do |payload|
101
+ yield
102
+ end
103
+ write(name, result, options)
104
+ result
105
+ end
106
+ else
107
+ read(name, options)
108
+ end
109
+ end
110
+
111
+ # Writes the value to the cache, with the key
112
+ #
113
+ # @since 1.2.0.dp5
114
+ #
115
+ # @param [String] name name for the key
116
+ # @param [Object] value value of the key
117
+ # @param [Hash] options
118
+ # @option options [Fixnum] :expires_in the expiration time on the
119
+ # cache in seconds. Values larger than 30*24*60*60 seconds (30 days)
120
+ # are interpreted as absolute times (from the epoch).
121
+ #
122
+ # @return [Fixnum, false] false in case of failure and CAS value
123
+ # otherwise (it could be used as true value)
124
+ def write(name, value, options = nil)
125
+ options ||= {}
126
+ name = expanded_key name
127
+ if options.delete(:raw)
128
+ options[:format] = :plain
129
+ value = value.to_s
130
+ value.force_encoding(Encoding::BINARY) if defined?(Encoding)
131
+ end
132
+
133
+ instrument(:write, name, options) do |payload|
134
+ write_entry(name, value, options)
135
+ end
136
+ end
137
+
138
+ # Fetches data from the cache, using the given key.
139
+ #
140
+ # @since 1.2.0.dp5
141
+ #
142
+ # If there is data in the cache with the given key, then that data is
143
+ # returned. Otherwise, nil is returned.
144
+ #
145
+ # @param [String] name name for the key
146
+ # @param [Hash] options
147
+ # @option options [Fixnum] :expires_in the expiration time on the
148
+ # cache in seconds. Values larger than 30*24*60*60 seconds (30 days)
149
+ # are interpreted as absolute times (from the epoch).
150
+ # @option options [true, false] :raw do not marshal the value if this
151
+ # option is +true+
152
+ #
153
+ # @return [Object]
154
+ def read(name, options = nil)
155
+ options ||= {}
156
+ name = expanded_key name
157
+ if options.delete(:raw)
158
+ options[:format] = :plain
159
+ end
160
+
161
+ instrument(:read, name, options) do |payload|
162
+ entry = read_entry(name, options)
163
+ payload[:hit] = !!entry if payload
164
+ entry
165
+ end
166
+ end
167
+
168
+ # Read multiple values at once from the cache.
169
+ #
170
+ # @since 1.2.0.dp5
171
+ #
172
+ # Options can be passed in the last argument.
173
+ #
174
+ # Returns a hash mapping the names provided to the values found.
175
+ #
176
+ # @return [Hash] key-value pairs
177
+ def read_multi(*names)
178
+ options = names.extract_options!
179
+ names = names.flatten.map{|name| expanded_key(name)}
180
+ options[:assemble_hash] = true
181
+ if options.delete(:raw)
182
+ options[:format] = :plain
183
+ end
184
+ instrument(:read_multi, names, options) do
185
+ @data.get(names, options)
186
+ end
187
+ rescue Couchbase::Error::Base => e
188
+ logger.error("#{e.class}: #{e.message}") if logger
189
+ raise if @raise_errors
190
+ false
191
+ end
192
+
193
+ # Return true if the cache contains an entry for the given key.
194
+ #
195
+ # @since 1.2.0.dp5
196
+ #
197
+ # @return [true, false]
198
+ def exists?(name, options = nil)
199
+ options ||= {}
200
+ name = expanded_key name
201
+
202
+ instrument(:exists?, name) do
203
+ !read_entry(name, options).nil?
204
+ end
205
+ end
206
+ alias :exist? :exists?
207
+
208
+ # Deletes an entry in the cache.
209
+ #
210
+ # @since 1.2.0.dp5
211
+ #
212
+ # @return [true, false] true if an entry is deleted
213
+ def delete(name, options = nil)
214
+ options ||= {}
215
+ name = expanded_key name
216
+
217
+ instrument(:delete, name) do
218
+ delete_entry(name, options)
219
+ end
220
+ end
221
+
222
+ # Increment an integer value in the cache.
223
+ #
224
+ # @since 1.2.0.dp5
225
+ #
226
+ # @param [String] name name for the key
227
+ # @param [Fixnum] amount (1) the delta value
228
+ # @param [Hash] options
229
+ # @option options [Fixnum] :expires_in the expiration time on the
230
+ # cache in seconds. Values larger than 30*24*60*60 seconds (30 days)
231
+ # are interpreted as absolute times (from the epoch).
232
+ # @option options [Fixnum] :initial (1) this option allows to initialize
233
+ # the value if the key is missing in the cache
234
+ #
235
+ # @return [Fixnum] new value
236
+ def increment(name, amount = 1, options = nil)
237
+ options ||= {}
238
+ name = expanded_key name
239
+
240
+ if ttl = options.delete(:expires_in)
241
+ options[:ttl] ||= ttl
242
+ end
243
+ options[:create] = true
244
+ instrument(:increment, name, options) do |payload|
245
+ payload[:amount] = amount if payload
246
+ @data.incr(name, amount, options)
247
+ end
248
+ rescue Couchbase::Error::Base => e
249
+ logger.error("#{e.class}: #{e.message}") if logger
250
+ raise if @raise_errors
251
+ false
252
+ end
253
+
254
+ # Decrement an integer value in the cache.
255
+ #
256
+ # @since 1.2.0.dp5
257
+ #
258
+ # @param [String] name name for the key
259
+ # @param [Fixnum] amount (1) the delta value
260
+ # @param [Hash] options
261
+ # @option options [Fixnum] :expires_in the expiration time on the
262
+ # cache in seconds. Values larger than 30*24*60*60 seconds (30 days)
263
+ # are interpreted as absolute times (from the epoch).
264
+ # @option options [Fixnum] :initial this option allows to initialize
265
+ # the value if the key is missing in the cache
266
+ #
267
+ # @return [Fixnum] new value
268
+ def decrement(name, amount = 1, options = nil)
269
+ options ||= {}
270
+ name = expanded_key name
271
+
272
+ if ttl = options.delete(:expires_in)
273
+ options[:ttl] ||= ttl
274
+ end
275
+ options[:create] = true
276
+ instrument(:decrement, name, options) do |payload|
277
+ payload[:amount] = amount if payload
278
+ @data.decr(name, amount, options)
279
+ end
280
+ rescue Couchbase::Error::Base => e
281
+ logger.error("#{e.class}: #{e.message}") if logger
282
+ raise if @raise_errors
283
+ false
284
+ end
285
+
286
+ # Get the statistics from the memcached servers.
287
+ #
288
+ # @since 1.2.0.dp5
289
+ #
290
+ # @return [Hash]
291
+ def stats(*arg)
292
+ @data.stats(*arg)
293
+ end
294
+
295
+ protected
296
+
297
+ # Read an entry from the cache.
298
+ def read_entry(key, options) # :nodoc:
299
+ @data.get(key, options)
300
+ rescue Couchbase::Error::Base => e
301
+ logger.error("#{e.class}: #{e.message}") if logger
302
+ raise if @raise_errors
303
+ nil
304
+ end
305
+
306
+ # Write an entry to the cache.
307
+ def write_entry(key, value, options) # :nodoc:
308
+ method = if options[:unless_exists] || options[:unless_exist]
309
+ :add
310
+ else
311
+ :set
312
+ end
313
+ if ttl = options.delete(:expires_in)
314
+ options[:ttl] ||= ttl
315
+ end
316
+ @data.send(method, key, value, options)
317
+ rescue Couchbase::Error::Base => e
318
+ logger.error("#{e.class}: #{e.message}") if logger
319
+ raise if @raise_errors
320
+ false
321
+ end
322
+
323
+ # Delete an entry from the cache.
324
+ def delete_entry(key, options) # :nodoc:
325
+ @data.delete(key, options)
326
+ rescue Couchbase::Error::Base => e
327
+ logger.error("#{e.class}: #{e.message}") if logger
328
+ raise if @raise_errors
329
+ false
330
+ end
331
+
332
+ private
333
+
334
+ # Expand key to be a consistent string value. Invoke +cache_key+ if
335
+ # object responds to +cache_key+. Otherwise, to_param method will be
336
+ # called. If the key is a Hash, then keys will be sorted alphabetically.
337
+ def expanded_key(key) # :nodoc:
338
+ return key.cache_key.to_s if key.respond_to?(:cache_key)
339
+
340
+ case key
341
+ when Array
342
+ if key.size > 1
343
+ key = key.collect{|element| expanded_key(element)}
344
+ else
345
+ key = key.first
346
+ end
347
+ when Hash
348
+ key = key.sort_by { |k,_| k.to_s }.collect{|k,v| "#{k}=#{v}"}
349
+ end
350
+
351
+ key.respond_to?(:to_param) ? key.to_param : key
352
+ end
353
+
354
+ end
355
+ end
356
+ end