libcouchbase-mapo 1.4.1

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