libcouchbase-mapo 1.4.1

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 (104) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/.gitmodules +3 -0
  4. data/.rspec +1 -0
  5. data/.travis.yml +38 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE +24 -0
  8. data/README.md +445 -0
  9. data/Rakefile +76 -0
  10. data/ext/README.md +6 -0
  11. data/ext/Rakefile +19 -0
  12. data/lib/libcouchbase.rb +40 -0
  13. data/lib/libcouchbase/bucket.rb +825 -0
  14. data/lib/libcouchbase/callbacks.rb +69 -0
  15. data/lib/libcouchbase/connection.rb +886 -0
  16. data/lib/libcouchbase/design_docs.rb +92 -0
  17. data/lib/libcouchbase/error.rb +68 -0
  18. data/lib/libcouchbase/ext/libcouchbase.rb +1175 -0
  19. data/lib/libcouchbase/ext/libcouchbase/cmdbase.rb +23 -0
  20. data/lib/libcouchbase/ext/libcouchbase/cmdcounter.rb +36 -0
  21. data/lib/libcouchbase/ext/libcouchbase/cmdendure.rb +26 -0
  22. data/lib/libcouchbase/ext/libcouchbase/cmdfts.rb +24 -0
  23. data/lib/libcouchbase/ext/libcouchbase/cmdget.rb +30 -0
  24. data/lib/libcouchbase/ext/libcouchbase/cmdgetreplica.rb +49 -0
  25. data/lib/libcouchbase/ext/libcouchbase/cmdhttp.rb +58 -0
  26. data/lib/libcouchbase/ext/libcouchbase/cmdn1ql.rb +40 -0
  27. data/lib/libcouchbase/ext/libcouchbase/cmdobseqno.rb +33 -0
  28. data/lib/libcouchbase/ext/libcouchbase/cmdobserve.rb +30 -0
  29. data/lib/libcouchbase/ext/libcouchbase/cmdstore.rb +40 -0
  30. data/lib/libcouchbase/ext/libcouchbase/cmdstoredur.rb +45 -0
  31. data/lib/libcouchbase/ext/libcouchbase/cmdsubdoc.rb +61 -0
  32. data/lib/libcouchbase/ext/libcouchbase/cmdverbosity.rb +29 -0
  33. data/lib/libcouchbase/ext/libcouchbase/cmdviewquery.rb +61 -0
  34. data/lib/libcouchbase/ext/libcouchbase/contigbuf.rb +14 -0
  35. data/lib/libcouchbase/ext/libcouchbase/create_st.rb +15 -0
  36. data/lib/libcouchbase/ext/libcouchbase/create_st0.rb +23 -0
  37. data/lib/libcouchbase/ext/libcouchbase/create_st1.rb +26 -0
  38. data/lib/libcouchbase/ext/libcouchbase/create_st2.rb +32 -0
  39. data/lib/libcouchbase/ext/libcouchbase/create_st3.rb +26 -0
  40. data/lib/libcouchbase/ext/libcouchbase/crst_u.rb +20 -0
  41. data/lib/libcouchbase/ext/libcouchbase/durability_opts_st_v.rb +11 -0
  42. data/lib/libcouchbase/ext/libcouchbase/durability_opts_t.rb +14 -0
  43. data/lib/libcouchbase/ext/libcouchbase/durabilityopt_sv0.rb +63 -0
  44. data/lib/libcouchbase/ext/libcouchbase/enums.rb +1007 -0
  45. data/lib/libcouchbase/ext/libcouchbase/fragbuf.rb +18 -0
  46. data/lib/libcouchbase/ext/libcouchbase/ftshandle.rb +7 -0
  47. data/lib/libcouchbase/ext/libcouchbase/histogram.rb +34 -0
  48. data/lib/libcouchbase/ext/libcouchbase/http_request_t.rb +7 -0
  49. data/lib/libcouchbase/ext/libcouchbase/keybuf.rb +20 -0
  50. data/lib/libcouchbase/ext/libcouchbase/multicmd_ctx.rb +30 -0
  51. data/lib/libcouchbase/ext/libcouchbase/mutation_token.rb +17 -0
  52. data/lib/libcouchbase/ext/libcouchbase/n1qlhandle.rb +7 -0
  53. data/lib/libcouchbase/ext/libcouchbase/n1qlparams.rb +7 -0
  54. data/lib/libcouchbase/ext/libcouchbase/respbase.rb +29 -0
  55. data/lib/libcouchbase/ext/libcouchbase/respcounter.rb +32 -0
  56. data/lib/libcouchbase/ext/libcouchbase/respendure.rb +49 -0
  57. data/lib/libcouchbase/ext/libcouchbase/respfts.rb +40 -0
  58. data/lib/libcouchbase/ext/libcouchbase/respget.rb +44 -0
  59. data/lib/libcouchbase/ext/libcouchbase/resphttp.rb +48 -0
  60. data/lib/libcouchbase/ext/libcouchbase/respmcversion.rb +38 -0
  61. data/lib/libcouchbase/ext/libcouchbase/respn1ql.rb +41 -0
  62. data/lib/libcouchbase/ext/libcouchbase/respobseqno.rb +52 -0
  63. data/lib/libcouchbase/ext/libcouchbase/respobserve.rb +41 -0
  64. data/lib/libcouchbase/ext/libcouchbase/respserverbase.rb +32 -0
  65. data/lib/libcouchbase/ext/libcouchbase/respstats.rb +38 -0
  66. data/lib/libcouchbase/ext/libcouchbase/respstore.rb +32 -0
  67. data/lib/libcouchbase/ext/libcouchbase/respstoredur.rb +38 -0
  68. data/lib/libcouchbase/ext/libcouchbase/respsubdoc.rb +35 -0
  69. data/lib/libcouchbase/ext/libcouchbase/respviewquery.rb +67 -0
  70. data/lib/libcouchbase/ext/libcouchbase/sdentry.rb +22 -0
  71. data/lib/libcouchbase/ext/libcouchbase/sdspec.rb +31 -0
  72. data/lib/libcouchbase/ext/libcouchbase/t.rb +7 -0
  73. data/lib/libcouchbase/ext/libcouchbase/valbuf.rb +22 -0
  74. data/lib/libcouchbase/ext/libcouchbase/valbuf_u_buf.rb +14 -0
  75. data/lib/libcouchbase/ext/libcouchbase/viewhandle.rb +7 -0
  76. data/lib/libcouchbase/ext/libcouchbase_libuv.rb +22 -0
  77. data/lib/libcouchbase/ext/tasks.rb +39 -0
  78. data/lib/libcouchbase/n1ql.rb +78 -0
  79. data/lib/libcouchbase/query_full_text.rb +147 -0
  80. data/lib/libcouchbase/query_n1ql.rb +123 -0
  81. data/lib/libcouchbase/query_view.rb +135 -0
  82. data/lib/libcouchbase/results_fiber.rb +281 -0
  83. data/lib/libcouchbase/results_native.rb +220 -0
  84. data/lib/libcouchbase/subdoc_request.rb +139 -0
  85. data/lib/libcouchbase/version.rb +5 -0
  86. data/libcouchbase.gemspec +68 -0
  87. data/spec/bucket_spec.rb +290 -0
  88. data/spec/connection_spec.rb +257 -0
  89. data/spec/design_docs_spec.rb +31 -0
  90. data/spec/error_spec.rb +26 -0
  91. data/spec/fts_spec.rb +135 -0
  92. data/spec/n1ql_spec.rb +206 -0
  93. data/spec/results_libuv_spec.rb +244 -0
  94. data/spec/results_native_spec.rb +259 -0
  95. data/spec/seed/2016-10-25T043505Z/2016-10-25T043505Z-full/bucket-default/design.json +1 -0
  96. data/spec/seed/2016-10-25T043505Z/2016-10-25T043505Z-full/bucket-default/node-127.0.0.1%3A8091/data-0000.cbb +0 -0
  97. data/spec/seed/2016-10-25T043505Z/2016-10-25T043505Z-full/bucket-default/node-127.0.0.1%3A8091/failover.json +1 -0
  98. data/spec/seed/2016-10-25T043505Z/2016-10-25T043505Z-full/bucket-default/node-127.0.0.1%3A8091/meta.json +1 -0
  99. data/spec/seed/2016-10-25T043505Z/2016-10-25T043505Z-full/bucket-default/node-127.0.0.1%3A8091/seqno.json +1 -0
  100. data/spec/seed/2016-10-25T043505Z/2016-10-25T043505Z-full/bucket-default/node-127.0.0.1%3A8091/snapshot_markers.json +1 -0
  101. data/spec/subdoc_spec.rb +192 -0
  102. data/spec/view_spec.rb +201 -0
  103. data/windows_build.md +36 -0
  104. metadata +265 -0
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true, encoding: ASCII-8BIT
2
+
3
+ require 'concurrent'
4
+ require 'ffi'
5
+
6
+ module Libcouchbase
7
+ module Callbacks
8
+
9
+
10
+ private
11
+
12
+
13
+ module ClassMethods
14
+ def dispatch_callback(func_name, lookup, args)
15
+ instance_id = __send__(lookup, *args)
16
+ inst = @callback_lookup[instance_id]
17
+ inst.__send__(func_name, *args) if inst.respond_to?(func_name, true)
18
+ end
19
+
20
+ def define_callback(function:, params: [:pointer, :int, :pointer], ret_val: :void, lookup: :default_lookup)
21
+ @callback_funcs[function] = ::FFI::Function.new(ret_val, params) do |*args|
22
+ dispatch_callback(function, lookup, args)
23
+ end
24
+ end
25
+
26
+ # Much like include to support inheritance properly
27
+ # We keep existing callbacks and inherit the lookup (as this will never clash)
28
+ def inherited(subclass)
29
+ subclass.instance_variable_set(:@callback_funcs, {}.merge(@callback_funcs))
30
+ subclass.instance_variable_set(:@callback_lookup, @callback_lookup)
31
+ subclass.instance_variable_set(:@callback_lock, @callback_lock)
32
+ end
33
+
34
+
35
+ # Provide accessor methods to the class level instance variables
36
+ attr_reader :callback_lookup, :callback_funcs, :callback_lock
37
+
38
+
39
+ # This function is used to work out the instance the callback is for
40
+ def default_lookup(req, *args)
41
+ req.address
42
+ end
43
+ end
44
+
45
+ def self.included(base)
46
+ base.instance_variable_set(:@callback_funcs, {})
47
+ base.instance_variable_set(:@callback_lookup, ::Concurrent::Hash.new)
48
+ base.instance_variable_set(:@callback_lock, ::Mutex.new)
49
+ base.extend(ClassMethods)
50
+ end
51
+
52
+
53
+ def callback(name, instance_id = @ref)
54
+ klass = self.class
55
+ klass.callback_lock.synchronize do
56
+ klass.callback_lookup[instance_id] = self
57
+ end
58
+ klass.callback_funcs[name]
59
+ end
60
+
61
+ def cleanup_callbacks(instance_id = @ref)
62
+ klass = self.class
63
+ klass.callback_lock.synchronize do
64
+ inst = klass.callback_lookup[instance_id]
65
+ klass.callback_lookup.delete(instance_id) if inst == self
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,886 @@
1
+ # frozen_string_literal: true, encoding: ASCII-8BIT
2
+
3
+ require 'json'
4
+
5
+
6
+ # Not required on jruby - buckets are cleaned up by GC
7
+ unless defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
8
+ at_exit do
9
+ GC.start
10
+ connections = []
11
+ ObjectSpace.each_object(::Libcouchbase::Connection).each do |connection|
12
+ next unless connection.reactor.running?
13
+ connections << connection
14
+ begin
15
+ connection.destroy
16
+ rescue => e
17
+ end
18
+ end
19
+ sleep 2 if connections.length > 0
20
+ connections.each { |c| c.reactor.stop }
21
+ end
22
+ end
23
+
24
+
25
+ module Libcouchbase
26
+ Response = Struct.new(:callback, :key, :cas, :value, :metadata)
27
+ HttpResponse = Struct.new(:callback, :status, :headers, :body, :request)
28
+
29
+ class Connection
30
+ include Callbacks
31
+ define_callback function: :bootstrap_callback, params: [:pointer, Ext::ErrorT.native_type]
32
+
33
+ # This is common for all standard request types
34
+ define_callback function: :callback_get
35
+ define_callback function: :callback_unlock
36
+ define_callback function: :callback_store
37
+ define_callback function: :callback_storedur
38
+ define_callback function: :callback_counter
39
+ define_callback function: :callback_touch
40
+ define_callback function: :callback_remove
41
+ define_callback function: :callback_cbflush
42
+ define_callback function: :callback_http
43
+ define_callback function: :callback_sdlookup # subdoc lookup
44
+ define_callback function: :callback_sdmutate
45
+
46
+ # These are passed with the request
47
+ define_callback function: :viewquery_callback
48
+ define_callback function: :n1ql_callback
49
+ define_callback function: :fts_callback
50
+
51
+
52
+ Request = Struct.new(:cmd, :defer, :key, :value) do
53
+ # We need to hold a reference to c-strings so they are not GC'd
54
+ def ref(string)
55
+ @refs ||= []
56
+ str = FFI::MemoryPointer.from_string(string)
57
+ @refs << str
58
+ str
59
+ end
60
+ end
61
+
62
+
63
+ def initialize(hosts: Defaults.host, bucket: Defaults.bucket, username: Defaults.username, password: Defaults.password, thread: nil, **opts)
64
+ # build host string http://docs.couchbase.com/sdk-api/couchbase-c-client-2.5.6/group__lcb-init.html
65
+ hosts = Array(hosts).flatten.join(',')
66
+ connstr = "couchbase://#{hosts}/#{bucket}"
67
+ connstr = "#{connstr}?#{opts.map { |k, v| "#{k}=#{v}" }.join('&') }" unless opts.empty?
68
+
69
+ # It's good to know
70
+ @bucket = bucket
71
+
72
+ # Configure the event loop settings
73
+ @reactor = thread || ::Libuv::Reactor.current || ::Libuv::Reactor.new
74
+ @reactor.on_program_interrupt { destroy }
75
+ @io_ptr = FFI::MemoryPointer.new :pointer, 1
76
+
77
+ # Configure Libuv plugin
78
+ @io_opts = Ext::Libuv::UVOptions.new
79
+ @io_opts[:version] = 0
80
+ @io_opts[:loop] = @reactor.handle
81
+ @io_opts[:start_stop_noop] = 1 # We want to control the start and stopping of the loop
82
+
83
+ err = Ext::Libuv.create_libuv_io_opts(0, @io_ptr, @io_opts)
84
+ if err != :success
85
+ raise Error.lookup(err), 'failed to allocate IO plugin'
86
+ end
87
+
88
+ # Configure the connection to the database
89
+ @connection = Ext::CreateSt.new
90
+ @connection[:version] = 3
91
+ @connection[:v][:v3][:connstr] = FFI::MemoryPointer.from_string(connstr)
92
+ uname = (username && !username.to_s.empty?) ? username.to_s : bucket.to_s
93
+ @connection[:v][:v3][:username] = FFI::MemoryPointer.from_string(uname)
94
+ @connection[:v][:v3][:passwd] = FFI::MemoryPointer.from_string(password) if password
95
+ @connection[:v][:v3][:io] = @io_ptr.get_pointer(0)
96
+ @handle_ptr = FFI::MemoryPointer.new :pointer, 1
97
+ end
98
+
99
+
100
+ attr_reader :requests, :handle, :bucket, :reactor
101
+
102
+ def get_callback(cb)
103
+ callback(cb)
104
+ end
105
+
106
+
107
+ def connect(defer: nil, flush_enabled: false)
108
+ raise 'already connected' if @handle || @bootstrap_defer
109
+ @bootstrap_defer = defer || @reactor.defer
110
+ promise = @bootstrap_defer.promise
111
+
112
+ @reactor.schedule {
113
+ @flush_enabled = flush_enabled
114
+
115
+ @requests = {}
116
+
117
+ # Create a library handle
118
+ # the create call allocates the memory and updates our pointer
119
+ err = Ext.create(@handle_ptr, @connection)
120
+ if err != :success
121
+ @bootstrap_defer.reject(Error.lookup(err).new('failed to create instance'))
122
+ handle_destroyed
123
+ else
124
+ # We extract the pointer and create the handle structure
125
+ @ref = @handle_ptr.get_pointer(0).address
126
+ @handle = Ext::T.new @handle_ptr.get_pointer(0)
127
+
128
+ # Register the callbacks we are interested in
129
+ Ext.set_bootstrap_callback(@handle, callback(:bootstrap_callback))
130
+
131
+ Ext.install_callback3(@handle, Ext::CALLBACKTYPE[:callback_get], callback(:callback_get))
132
+ Ext.install_callback3(@handle, Ext::CALLBACKTYPE[:callback_unlock], callback(:callback_unlock))
133
+ Ext.install_callback3(@handle, Ext::CALLBACKTYPE[:callback_store], callback(:callback_store))
134
+ Ext.install_callback3(@handle, Ext::CALLBACKTYPE[:callback_storedur], callback(:callback_storedur))
135
+ Ext.install_callback3(@handle, Ext::CALLBACKTYPE[:callback_counter], callback(:callback_counter))
136
+ Ext.install_callback3(@handle, Ext::CALLBACKTYPE[:callback_touch], callback(:callback_touch))
137
+ Ext.install_callback3(@handle, Ext::CALLBACKTYPE[:callback_remove], callback(:callback_remove))
138
+ Ext.install_callback3(@handle, Ext::CALLBACKTYPE[:callback_http], callback(:callback_http))
139
+ Ext.install_callback3(@handle, Ext::CALLBACKTYPE[:callback_sdlookup], callback(:callback_sdlookup))
140
+ Ext.install_callback3(@handle, Ext::CALLBACKTYPE[:callback_sdmutate], callback(:callback_sdmutate))
141
+ Ext.install_callback3(@handle, Ext::CALLBACKTYPE[:callback_cbflush], callback(:callback_cbflush)) if @flush_enabled
142
+
143
+ # Configure safe retries
144
+ # LCB_RETRYOPT_CREATE = Proc.new { |mode, policy| ((mode << 16) | policy) }
145
+ # val = LCB_RETRYOPT_CREATE(LCB_RETRY_ON_SOCKERR, LCB_RETRY_CMDS_SAFE);
146
+ # ::Libcouchbase::Ext.cntl_setu32(handle, LCB_CNTL_RETRYMODE, val)
147
+ retry_config = (1 << 16) | 3
148
+ ::Libcouchbase::Ext.cntl_setu32(@handle, 0x24, (1 << 16) | 3)
149
+
150
+ # Connect to the database
151
+ err = Ext.connect(@handle)
152
+ if err != :success
153
+ @bootstrap_defer.reject(Error.lookup(err).new('failed to schedule connect'))
154
+ Ext.destroy(@handle)
155
+ handle_destroyed
156
+ end
157
+ end
158
+ }
159
+
160
+ promise
161
+ end
162
+
163
+ # http://docs.couchbase.com/sdk-api/couchbase-c-client-2.6.2/group__lcb-cntl.html
164
+ def configure(setting, value)
165
+ # Ensure it is thread safe
166
+ defer = @reactor.defer
167
+ @reactor.schedule {
168
+ if @handle
169
+ err = Ext.cntl_string(@handle, setting.to_s, value.to_s)
170
+ if err == :success
171
+ defer.resolve(self)
172
+ else
173
+ defer.reject(Error.lookup(err).new("failed to configure #{setting}=#{value}"))
174
+ end
175
+ else
176
+ defer.reject(RuntimeError.new('not connected'))
177
+ end
178
+ }
179
+
180
+ defer.promise
181
+ end
182
+
183
+ def destroy
184
+ return @destroy_defer.promise if @destroy_defer
185
+
186
+ # Ensure it is thread safe
187
+ defer = @reactor.defer
188
+ if @handle
189
+ @reactor.schedule {
190
+ if @destroy_defer.nil?
191
+ @destroy_defer = defer
192
+ Ext.destroy(@handle)
193
+ handle_destroyed
194
+ defer.resolve(nil)
195
+ else
196
+ defer.resolve(@destroy_defer.promise)
197
+ end
198
+ }
199
+ else
200
+ defer.resolve(nil)
201
+ end
202
+ defer.promise
203
+ end
204
+
205
+ def get_server_list
206
+ defer = @reactor.defer
207
+
208
+ # Ensure it is thread safe
209
+ @reactor.schedule {
210
+ if @handle
211
+ nodes = Ext.get_num_nodes(@handle)
212
+ list = []
213
+ count = 0
214
+
215
+ while count <= nodes
216
+ list << Ext.get_node(@handle, :node_data, count)
217
+ count += 1
218
+ end
219
+
220
+ defer.resolve(list.uniq)
221
+ else
222
+ defer.reject(RuntimeError.new('not connected'))
223
+ end
224
+ }
225
+
226
+ defer.promise
227
+ end
228
+
229
+ def get_num_replicas
230
+ defer = @reactor.defer
231
+
232
+ # Ensure it is thread safe
233
+ @reactor.schedule {
234
+ if @handle
235
+ defer.resolve(Ext.get_num_replicas(@handle))
236
+ else
237
+ defer.reject(RuntimeError.new('not connected'))
238
+ end
239
+ }
240
+
241
+ defer.promise
242
+ end
243
+
244
+ def get_num_nodes
245
+ defer = @reactor.defer
246
+
247
+ # Ensure it is thread safe
248
+ @reactor.schedule {
249
+ if @handle
250
+ defer.resolve(Ext.get_num_nodes(@handle))
251
+ else
252
+ defer.reject(RuntimeError.new('not connected'))
253
+ end
254
+ }
255
+
256
+ defer.promise
257
+ end
258
+
259
+ # http://docs.couchbase.com/sdk-api/couchbase-c-client-2.6.2/group__lcb-store.html
260
+ # http://docs.couchbase.com/sdk-api/couchbase-c-client-2.6.2/group__lcb-durability.html
261
+ def store(key, value,
262
+ defer: nil,
263
+ operation: :set,
264
+ expire_in: nil,
265
+ expire_at: nil,
266
+ ttl: nil,
267
+ persist_to: 0,
268
+ replicate_to: 0,
269
+ cas: nil,
270
+ flags: 0,
271
+ **opts)
272
+ raise 'not connected' unless @handle
273
+ defer ||= @reactor.defer
274
+
275
+ # Check if this should be a durable operation
276
+ durable = (persist_to | replicate_to) != 0
277
+ if durable
278
+ cmd = Ext::CMDSTOREDUR.new
279
+ cmd[:persist_to] = persist_to
280
+ cmd[:replicate_to] = replicate_to
281
+ else
282
+ cmd = Ext::CMDSTORE.new
283
+ end
284
+ cmd[:operation] = operation
285
+ cmd[:flags] = flags
286
+
287
+ str_value = begin
288
+ [value].to_json[1...-1]
289
+ rescue
290
+ [value.respond_to?(:to_str) ? value.to_str : value.to_s].to_json[1...-1]
291
+ end
292
+
293
+ req = Request.new(cmd, defer)
294
+ req.value = value
295
+ cmd_set_value(req, cmd, str_value)
296
+ key = cmd_set_key(req, cmd, key)
297
+
298
+ cmd[:cas] = cas if cas
299
+ expire_in ||= ttl
300
+ cmd[:exptime] = expire_in ? expires_in(expire_in) : expire_at.to_i
301
+
302
+ @reactor.schedule {
303
+ pointer = cmd.to_ptr
304
+ @requests[pointer.address] = req
305
+ check_error(key, defer, durable ? Ext.storedur3(@handle, pointer, cmd) : Ext.store3(@handle, pointer, cmd))
306
+ }
307
+
308
+ defer.promise
309
+ end
310
+
311
+ # http://docs.couchbase.com/sdk-api/couchbase-c-client-2.6.2/group__lcb-get.html
312
+ def get(key, defer: nil, lock: false, cas: nil, **opts)
313
+ raise 'not connected' unless @handle
314
+ defer ||= @reactor.defer
315
+
316
+ cmd = Ext::CMDGET.new
317
+ req = Request.new(cmd, defer)
318
+ key = cmd_set_key(req, cmd, key)
319
+ cmd[:cas] = cas if cas
320
+
321
+ # exptime == the lock expire time
322
+ if lock
323
+ time = lock == true ? 30 : lock.to_i
324
+ time = 30 if time > 30 || time < 0
325
+
326
+ # We only want to lock if time is between 1 and 30
327
+ if time > 0
328
+ cmd[:exptime] = time
329
+ cmd[:lock] = 1
330
+ end
331
+ end
332
+
333
+ @reactor.schedule {
334
+ pointer = cmd.to_ptr
335
+ @requests[pointer.address] = req
336
+ check_error key, defer, Ext.get3(@handle, pointer, cmd)
337
+ }
338
+
339
+ defer.promise
340
+ end
341
+
342
+ # http://docs.couchbase.com/sdk-api/couchbase-c-client-2.6.2/group__lcb-lock.html
343
+ def unlock(key, cas: , **opts)
344
+ raise 'not connected' unless @handle
345
+ defer ||= @reactor.defer
346
+
347
+ cmd = Ext::CMDBASE.new
348
+ req = Request.new(cmd, defer)
349
+ key = cmd_set_key(req, cmd, key)
350
+ cmd[:cas] = cas
351
+
352
+ @reactor.schedule {
353
+ pointer = cmd.to_ptr
354
+ @requests[pointer.address] = Request.new(cmd, defer, key)
355
+ check_error key, defer, Ext.unlock3(@handle, pointer, cmd)
356
+ }
357
+
358
+ defer.promise
359
+ end
360
+
361
+ # http://docs.couchbase.com/sdk-api/couchbase-c-client-2.6.2/group__lcb-remove.html
362
+ def remove(key, defer: nil, cas: nil, **opts)
363
+ raise 'not connected' unless @handle
364
+ defer ||= @reactor.defer
365
+
366
+ cmd = Ext::CMDBASE.new
367
+ req = Request.new(cmd, defer)
368
+ key = cmd_set_key(req, cmd, key)
369
+ cmd[:cas] = cas if cas
370
+
371
+ @reactor.schedule {
372
+ pointer = cmd.to_ptr
373
+ @requests[pointer.address] = req
374
+ check_error key, defer, Ext.remove3(@handle, pointer, cmd)
375
+ }
376
+
377
+ defer.promise
378
+ end
379
+
380
+ # http://docs.couchbase.com/sdk-api/couchbase-c-client-2.6.2/group__lcb-counter.html
381
+ def counter(key, delta: 1, initial: nil, expire_in: nil, ttl: nil, expire_at: nil, cas: nil, **opts)
382
+ raise 'not connected' unless @handle
383
+ defer ||= @reactor.defer
384
+
385
+ cmd = Ext::CMDCOUNTER.new
386
+ req = Request.new(cmd, defer)
387
+ key = cmd_set_key(req, cmd, key)
388
+
389
+ cmd[:cas] = cas if cas
390
+ expire_in ||= ttl
391
+ cmd[:exptime] = expire_in ? expires_in(expire_in) : expire_at.to_i
392
+ cmd[:delta] = delta
393
+ if initial
394
+ cmd[:initial] = initial
395
+ cmd[:create] = 1
396
+ end
397
+
398
+ @reactor.schedule {
399
+ pointer = cmd.to_ptr
400
+ @requests[pointer.address] = req
401
+ check_error key, defer, Ext.counter3(@handle, pointer, cmd)
402
+ }
403
+
404
+ defer.promise
405
+ end
406
+
407
+ # http://docs.couchbase.com/sdk-api/couchbase-c-client-2.6.2/group__lcb-touch.html
408
+ def touch(key, expire_in: nil, ttl: nil, expire_at: nil, cas: nil, **opts)
409
+ raise 'not connected' unless @handle
410
+ raise ArgumentError.new('requires either expire_in or expire_at to be set') unless expire_in || expire_at
411
+ defer ||= @reactor.defer
412
+
413
+ cmd = Ext::CMDBASE.new
414
+ req = Request.new(cmd, defer)
415
+ key = cmd_set_key(req, cmd, key)
416
+
417
+ cmd[:cas] = cas if cas
418
+ expire_in ||= ttl
419
+ cmd[:exptime] = expire_in ? expires_in(expire_in) : expire_at.to_i
420
+
421
+ @reactor.schedule {
422
+ pointer = cmd.to_ptr
423
+ @requests[pointer.address] = req
424
+ check_error key, defer, Ext.touch3(@handle, pointer, cmd)
425
+ }
426
+
427
+ defer.promise
428
+ end
429
+
430
+ def subdoc(request, expire_in: nil, ttl: nil, expire_at: nil, cas: nil, **opts)
431
+ raise 'not connected' unless @handle
432
+ defer ||= @reactor.defer
433
+
434
+ cmd = Ext::CMDSUBDOC.new
435
+ req = Request.new(cmd, defer, request.key, request)
436
+ key = cmd_set_key(req, cmd, request.key)
437
+
438
+ cmd[:multimode] = request.mode == :mutate ? Ext::CMDSUBDOC::SDMULTI_MODE_MUTATE : Ext::CMDSUBDOC::SDMULTI_MODE_LOOKUP
439
+ cmd[:specs], cmd[:nspecs] = request.to_specs_array
440
+
441
+ cmd[:cas] = cas if cas
442
+ expire_in ||= ttl
443
+ cmd[:exptime] = expire_in ? expires_in(expire_in) : expire_at.to_i
444
+
445
+ @reactor.schedule {
446
+ pointer = cmd.to_ptr
447
+ @requests[pointer.address] = req
448
+ check_error(key, defer, Ext.subdoc3(@handle, pointer, cmd), subdoc: true)
449
+ request.free_memory
450
+ }
451
+
452
+ defer.promise
453
+ end
454
+
455
+ # http://docs.couchbase.com/sdk-api/couchbase-c-client-2.6.2/group__lcb-flush.html
456
+ def flush(defer: nil)
457
+ raise 'not connected' unless @handle
458
+ raise 'flush not enabled' unless @flush_enabled
459
+ defer ||= @reactor.defer
460
+
461
+ cmd = Ext::CMDBASE.new
462
+
463
+ @reactor.schedule {
464
+ pointer = cmd.to_ptr
465
+ @requests[pointer.address] = Request.new(cmd, defer, :flush)
466
+ check_error :flush, defer, Ext.cbflush3(@handle, pointer, cmd)
467
+ }
468
+
469
+ defer.promise
470
+ end
471
+
472
+
473
+ CMDHTTP_F_STREAM = 1<<16 # Stream the response (not used, we're only making simple requests)
474
+ CMDHTTP_F_CASTMO = 1<<17 # If specified, the lcb_CMDHTTP::cas field becomes the timeout
475
+ CMDHTTP_F_NOUPASS = 1<<18 # If specified, do not inject authentication header into the request.
476
+ HttpBodyRequired = [:put, :post].freeze
477
+
478
+ # http://docs.couchbase.com/sdk-api/couchbase-c-client-2.6.2/group__lcb-http.html
479
+ def http(path,
480
+ type: :view,
481
+ method: :get,
482
+ body: nil,
483
+ content_type: 'application/json',
484
+ defer: nil,
485
+ timeout: nil,
486
+ username: nil,
487
+ password: nil,
488
+ no_auth: false,
489
+ **opts)
490
+ raise 'not connected' unless @handle
491
+ raise 'unsupported request type' unless Ext::HttpTypeT[type]
492
+ raise 'unsupported HTTP method' unless Ext::HttpMethodT[method]
493
+ body_content = if HttpBodyRequired.include? method
494
+ raise 'no HTTP body provided' unless body
495
+ if body.is_a? String
496
+ body
497
+ else
498
+ # This will raise an error if not valid json
499
+ JSON.generate([body])[1..-2]
500
+ end
501
+ end
502
+
503
+ defer ||= @reactor.defer
504
+
505
+ cmd = Ext::CMDHTTP.new
506
+ req = Request.new(cmd, defer)
507
+ req.value = {
508
+ path: path,
509
+ method: method,
510
+ body: body,
511
+ content_type: content_type,
512
+ type: type,
513
+ no_auth: no_auth
514
+ }
515
+ cmd_set_key(req, cmd, path)
516
+
517
+ if timeout
518
+ cmd[:cas] = timeout
519
+ cmd[:cmdflags] |= CMDHTTP_F_CASTMO
520
+ end
521
+ cmd[:cmdflags] |= CMDHTTP_F_NOUPASS if no_auth
522
+ cmd[:type] = type
523
+ cmd[:method] = method
524
+
525
+ if body_content
526
+ cmd[:body] = req.ref(body_content)
527
+ cmd[:nbody] = body_content.bytesize
528
+ end
529
+ cmd[:content_type] = req.ref(content_type) if content_type
530
+ cmd[:username] = req.ref(username) if username
531
+ cmd[:password] = req.ref(password) if password
532
+
533
+
534
+ @reactor.schedule {
535
+ pointer = cmd.to_ptr
536
+ @requests[pointer.address] = req
537
+ check_error path, defer, Ext.http3(@handle, pointer, cmd)
538
+ }
539
+
540
+ defer.promise
541
+ end
542
+
543
+ def query_view(design, view, **opts)
544
+ QueryView.new(self, @reactor, design, view, **opts)
545
+ end
546
+
547
+ def full_text_search(index, **opts)
548
+ opts[:indexName] = index
549
+ QueryFullText.new(self, @reactor, **opts)
550
+ end
551
+
552
+ def n1ql_query(n1ql, **opts)
553
+ QueryN1QL.new(self, @reactor, n1ql, **opts)
554
+ end
555
+
556
+ def parse_document(raw_string)
557
+ val = begin
558
+ JSON.parse("[#{raw_string}]", DECODE_OPTIONS)[0]
559
+ rescue
560
+ raw_string
561
+ end
562
+ val
563
+ end
564
+
565
+
566
+ private
567
+
568
+
569
+ def cmd_set_key(req, cmd, value)
570
+ key = value.to_s
571
+ cmd[:key][:type] = :kv_copy
572
+ str = req.ref(key)
573
+ req.key = value
574
+ cmd[:key][:contig][:bytes] = str
575
+ cmd[:key][:contig][:nbytes] = key.bytesize
576
+ key
577
+ end
578
+
579
+ def cmd_set_value(req, cmd, value)
580
+ val = value.to_s
581
+ cmd[:value][:vtype] = :kv_copy
582
+ str = req.ref(val)
583
+ cmd[:value][:u_buf][:contig][:bytes] = str
584
+ cmd[:value][:u_buf][:contig][:nbytes] = val.bytesize
585
+ end
586
+
587
+ # 30 days in seconds
588
+ MAX_EXPIRY = 2_592_000
589
+
590
+ def expires_in(time)
591
+ period = time.to_i
592
+ if period > MAX_EXPIRY
593
+ Time.now.to_i + period
594
+ else
595
+ period
596
+ end
597
+ end
598
+
599
+ def check_error(key, defer, err, subdoc: false)
600
+ if err != :success
601
+ error = Error.lookup(err).new("request not scheduled for #{key}")
602
+ backtrace = caller
603
+ error.set_backtrace backtrace
604
+ defer.reject error
605
+ end
606
+ end
607
+
608
+ def handle_destroyed
609
+ @bootstrap_defer = nil
610
+ @handle = nil
611
+
612
+ # TODO:: cleanup IO opts?
613
+ cleanup_callbacks
614
+
615
+ @requests.each_value do |req|
616
+ err = Error::Sockshutdown.new('handle destroyed')
617
+ if req.is_a? Request
618
+ req.defer.reject(err)
619
+ else
620
+ # this is a view, n1ql or full text query
621
+ req.error(err)
622
+ end
623
+ end
624
+ @requests = nil
625
+ end
626
+
627
+ def bootstrap_callback(handle, error_code)
628
+ error_name = Ext::ErrorT[error_code]
629
+
630
+ if error_code == Ext::ErrorT[:success]
631
+ @bootstrap_defer.resolve(self)
632
+ @bootstrap_defer = nil
633
+ else
634
+ @bootstrap_defer.reject(Error.lookup(error_code).new("bootstrap failed #{error_code}: #{error_name}"))
635
+ handle_destroyed
636
+ end
637
+ end
638
+
639
+ # ==================
640
+ # Response Callbacks
641
+ # ==================
642
+ DECODE_OPTIONS = {
643
+ symbolize_names: true
644
+ }.freeze
645
+
646
+ def callback_get(handle, type, response)
647
+ resp = Ext::RESPGET.new response
648
+ resp_callback_common(resp, :callback_get) do |req, cb|
649
+ raw_string = resp[:value].read_string(resp[:nvalue])
650
+ val = parse_document(raw_string)
651
+ Response.new(cb, req.key, resp[:cas], val, {flags: resp[:itmflags]})
652
+ end
653
+ end
654
+
655
+ def callback_store(handle, type, response)
656
+ resp = Ext::RESPSTORE.new response
657
+ resp_callback_common(resp, :callback_store) do |req, cb|
658
+ Response.new(cb, req.key, resp[:cas], req.value)
659
+ end
660
+ end
661
+
662
+ Durability = Struct.new(:nresponses, :exists_master, :persisted_master, :npersisted, :nreplicated, :error)
663
+
664
+ def callback_storedur(handle, type, response)
665
+ resp = Ext::RESPSTOREDUR.new response
666
+ resp_callback_common(resp, :callback_storedur) do |req, cb|
667
+ info = resp[:dur_resp]
668
+ dur = Durability.new(
669
+ info[:nresponses],
670
+ info[:exists_master],
671
+ info[:persisted_master],
672
+ info[:npersisted],
673
+ info[:nreplicated],
674
+ info[:rc]
675
+ )
676
+ Response.new(cb, req.key, resp[:cas], req.value, dur)
677
+ end
678
+ end
679
+
680
+ def callback_counter(handle, type, response)
681
+ resp = Ext::RESPCOUNTER.new response
682
+ resp_callback_common(resp, :callback_counter) do |req, cb|
683
+ Response.new(cb, req.key, resp[:cas], resp[:value])
684
+ end
685
+ end
686
+
687
+ def callback_touch(handle, type, response)
688
+ resp = Ext::RESPBASE.new response
689
+ resp_callback_common(resp, :callback_touch) do |req, cb|
690
+ Response.new(cb, req.key, resp[:cas])
691
+ end
692
+ end
693
+
694
+ def callback_remove(handle, type, response)
695
+ resp = Ext::RESPBASE.new response
696
+ resp_callback_common(resp, :callback_remove) do |req, cb|
697
+ Response.new(cb, req.key, resp[:cas])
698
+ end
699
+ end
700
+
701
+ def callback_unlock(handle, type, response)
702
+ resp = Ext::RESPBASE.new response
703
+ resp_callback_common(resp, :callback_unlock) do |req, cb|
704
+ Response.new(cb, req.key, resp[:cas])
705
+ end
706
+ end
707
+
708
+ def callback_sdlookup(handle, type, response)
709
+ resp = Ext::RESPSUBDOC.new response
710
+ resp_callback_common(resp, :callback_sdlookup) do |req, cb|
711
+ subdoc_common(resp, req, cb)
712
+ end
713
+ end
714
+
715
+ # Only counter returns a result
716
+ def callback_sdmutate(handle, type, response)
717
+ resp = Ext::RESPSUBDOC.new response
718
+ resp_callback_common(resp, :callback_sdmutate) do |req, cb|
719
+ subdoc_common(resp, req, cb)
720
+ end
721
+ end
722
+
723
+ def subdoc_common(resp, req, cb)
724
+ iterval = FFI::MemoryPointer.new(:ulong, 1)
725
+ cur_res = Ext::SDENTRY.new
726
+ values = []
727
+ index = 0
728
+
729
+ ignore = req.value.ignore
730
+ mutation = req.value.mode == :mutate
731
+
732
+ loop do
733
+ check = Ext.sdresult_next(resp, cur_res, iterval)
734
+ break if check == 0
735
+
736
+ if cur_res[:status] == :success
737
+ count = cur_res[:nvalue]
738
+ if count > 0
739
+ result = cur_res[:value].read_string(count)
740
+ else
741
+ result = true # success response
742
+ end
743
+ result = "[#{result}]"
744
+ values << JSON.parse(result, DECODE_OPTIONS)[0]
745
+ elsif cur_res[:status] == :subdoc_path_enoent && ignore[mutation ? cur_res[:index] : index]
746
+ values << nil
747
+ else
748
+ values << Error.lookup(cur_res[:status]).new("Subdoc #{cb} failed for #{req.key} index #{mutation ? cur_res[:index] : index}")
749
+ end
750
+
751
+ index += 1
752
+ end
753
+
754
+ # Return the single result instead of an array if single
755
+ is_single = resp[:rflags] & Ext::RESPFLAGS[:resp_f_sdsingle] > 0
756
+ if is_single
757
+ values = values.first
758
+ elsif values.empty? # multiple mutate arrays should return true (same as a single mutate)
759
+ values = true
760
+ end
761
+
762
+ Response.new(cb, req.key, resp[:cas], values)
763
+ end
764
+
765
+ def callback_cbflush(handle, type, response)
766
+ resp = Ext::RESPBASE.new response
767
+ resp_callback_common(resp, :callback_cbflush) do |req, cb|
768
+ Response.new(cb)
769
+ end
770
+ end
771
+
772
+ def callback_http(handle, type, response)
773
+ resp = Ext::RESPHTTP.new response
774
+ resp_callback_common(resp, :callback_http) do |req, cb|
775
+ headers = {}
776
+ head_ptr = resp[:headers]
777
+ if not head_ptr.null?
778
+ head_ptr.get_array_of_string(0).each_slice(2) do |key, value|
779
+ headers[key] = value
780
+ end
781
+ end
782
+ body = body_text(resp)
783
+
784
+ if (200...300).include? resp[:htstatus]
785
+ HttpResponse.new(cb, resp[:htstatus], headers, body, req.value)
786
+ else
787
+ err = Error::HttpResponseError.new "non success response for #{req.key}"
788
+ err.code = resp[:htstatus]
789
+ err.headers = headers
790
+ err.body = body
791
+ req.defer.reject(err)
792
+ end
793
+ end
794
+ end
795
+
796
+ def resp_callback_common(resp, callback)
797
+ req = @requests.delete(resp[:cookie].address)
798
+ if req
799
+ begin
800
+ # Errors will be provided in the response
801
+ if resp[:rc] == :success || resp[:rc] == :subdoc_multi_failure
802
+ req.defer.resolve(yield(req, callback))
803
+ else
804
+ lookup = resp[:rc]
805
+ req.defer.reject(Error.lookup(lookup).new("#{callback} failed for #{req.key} with #{lookup}"))
806
+ end
807
+ rescue => e
808
+ req.defer.reject(e)
809
+ end
810
+ else
811
+ @reactor.log IOError.new("received #{callback} for unknown request")
812
+ end
813
+ end
814
+ # ======================
815
+ # End Response Callbacks
816
+ # ======================
817
+
818
+ # http://docs.couchbase.com/sdk-api/couchbase-c-client-2.6.2/group__lcb-view-api.html
819
+ def viewquery_callback(handle, type, row)
820
+ row_data = Ext::RESPVIEWQUERY.new row
821
+ view = @requests[row_data[:cookie].address]
822
+
823
+ if row_data[:rc] == :success
824
+ if (row_data[:rflags] & Ext::RESPFLAGS[:resp_f_final]) > 0
825
+ # We can assume this is JSON
826
+ view.received_final(JSON.parse(row_data[:value].read_string(row_data[:nvalue]), DECODE_OPTIONS))
827
+ else
828
+ view.received(row_data)
829
+ end
830
+ else
831
+ error_klass = Error.lookup(row_data[:rc])
832
+ if error_klass == Error::HttpError
833
+ http_resp = row_data[:htresp]
834
+ view.error error_klass.new(body_text(http_resp))
835
+ else
836
+ view.error error_klass.new
837
+ end
838
+ end
839
+ end
840
+
841
+ # N1QL query response
842
+ # @see http://docs.couchbase.com/sdk-api/couchbase-c-client-2.6.2/group__lcb-n1ql-api.html
843
+ def n1ql_callback(handle, type, row)
844
+ query_callback_common Ext::RESPN1QL.new(row)
845
+ end
846
+
847
+ # Full text search
848
+ # @see http://docs.couchbase.com/sdk-api/couchbase-c-client-2.6.2/group__lcb-cbft-api.html
849
+ def fts_callback(handle, type, row)
850
+ query_callback_common Ext::RESPFTS.new(row)
851
+ end
852
+
853
+ # Common code to process both N1QL and FTS callbacks
854
+ def query_callback_common(row_data)
855
+ view = @requests[row_data[:cookie].address]
856
+
857
+ if row_data[:rc] == :success
858
+ value = JSON.parse(row_data[:row].read_string(row_data[:nrow]), DECODE_OPTIONS)
859
+
860
+ if (row_data[:rflags] & Ext::RESPFLAGS[:resp_f_final]) > 0
861
+ # We can assume this is JSON
862
+ view.received_final(value)
863
+ else
864
+ view.received(value)
865
+ end
866
+ else
867
+ error_klass = Error.lookup(row_data[:rc])
868
+ if error_klass == Error::HttpError
869
+ http_resp = row_data[:htresp]
870
+ view.error error_klass.new(body_text(http_resp))
871
+ else
872
+ view.error error_klass.new
873
+ end
874
+ end
875
+ end
876
+
877
+ # Extracts the body content of a HTTP response
878
+ def body_text(http_resp)
879
+ if http_resp[:nbody] > 0
880
+ http_resp[:body].read_string(http_resp[:nbody])
881
+ else
882
+ ''
883
+ end
884
+ end
885
+ end
886
+ end