dolly 3.1.5 → 3.2.0
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.
- checksums.yaml +4 -4
- data/README.md +82 -31
- data/lib/dolly/connection.rb +12 -27
- data/lib/dolly/curl/connection.rb +150 -0
- data/lib/dolly/curl/document_helper.rb +81 -0
- data/lib/dolly/curl/reader.rb +29 -0
- data/lib/dolly/curl/response_formatter.rb +30 -0
- data/lib/dolly/curl/stale_connection.rb +16 -0
- data/lib/dolly/curl/write_reconciler.rb +205 -0
- data/lib/dolly/curl.rb +17 -0
- data/lib/dolly/document.rb +1 -1
- data/lib/dolly/exceptions.rb +16 -0
- data/lib/dolly/version.rb +1 -1
- data/test/bulk_document_test.rb +67 -0
- data/test/connection_test.rb +145 -0
- data/test/curl/connection_test.rb +455 -0
- data/test/mango_index_test.rb +2 -2
- metadata +24 -12
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'test_helper'
|
|
4
|
+
|
|
5
|
+
class Dolly::Curl::ConnectionTest < Test::Unit::TestCase
|
|
6
|
+
setup do
|
|
7
|
+
clear_thread_local_curl!
|
|
8
|
+
@reader = Object.new
|
|
9
|
+
def @reader.fetch_document(*)
|
|
10
|
+
nil
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def @reader.fetch_documents_by_ids(*)
|
|
14
|
+
{}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
@curl = Dolly::Curl::Connection.new(db_name: 'test', reader: @reader)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
teardown do
|
|
21
|
+
clear_thread_local_curl!
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
test 'reuses the same curl handle on a thread' do
|
|
25
|
+
first = @curl.thread_local_handle
|
|
26
|
+
second = @curl.thread_local_handle
|
|
27
|
+
|
|
28
|
+
assert_same first, second
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
test 'uses separate curl handles per thread' do
|
|
32
|
+
handles = Queue.new
|
|
33
|
+
|
|
34
|
+
threads = 2.times.map do
|
|
35
|
+
Thread.new do
|
|
36
|
+
curl = Dolly::Curl::Connection.new(db_name: 'test', reader: stub_everything('reader'))
|
|
37
|
+
handles << curl.thread_local_handle
|
|
38
|
+
ensure
|
|
39
|
+
store = Thread.current[Dolly::Curl::Connection::THREAD_STORE_KEY]
|
|
40
|
+
next unless store
|
|
41
|
+
|
|
42
|
+
store.each_value { |handle| handle.close rescue nil }
|
|
43
|
+
Thread.current[Dolly::Curl::Connection::THREAD_STORE_KEY] = nil
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
threads.each(&:join)
|
|
47
|
+
|
|
48
|
+
collected = []
|
|
49
|
+
collected << handles.pop until handles.empty?
|
|
50
|
+
|
|
51
|
+
assert_equal 2, collected.map(&:object_id).uniq.size
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
test 'discard_handle removes and closes the handle' do
|
|
55
|
+
curl_easy = @curl.thread_local_handle
|
|
56
|
+
curl_easy.expects(:close).once
|
|
57
|
+
|
|
58
|
+
@curl.discard_handle
|
|
59
|
+
|
|
60
|
+
store = Thread.current[Dolly::Curl::Connection::THREAD_STORE_KEY]
|
|
61
|
+
assert_nil store[Dolly::Curl::Connection]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
test 'retries once after a stale connection error on get' do
|
|
65
|
+
response = build_curl_response(status: 200, body: '{"ok":true}')
|
|
66
|
+
|
|
67
|
+
@curl.stubs(:perform)
|
|
68
|
+
.raises(Curl::Err::GotNothingError)
|
|
69
|
+
.then.returns(response)
|
|
70
|
+
@curl.expects(:discard_handle).once
|
|
71
|
+
|
|
72
|
+
result = @curl.request(:get, URI('http://localhost:5984/test/doc'), {})
|
|
73
|
+
|
|
74
|
+
assert_equal response, result
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
test 'raises after stale connection retries are exhausted on get' do
|
|
78
|
+
@curl.stubs(:perform).raises(Curl::Err::RecvError)
|
|
79
|
+
@curl.expects(:discard_handle).twice
|
|
80
|
+
|
|
81
|
+
assert_raises(Curl::Err::RecvError) do
|
|
82
|
+
@curl.request(:get, URI('http://localhost:5984/test/doc'), {})
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
test 'retries once after SendError on read-only post' do
|
|
87
|
+
response = build_curl_response(status: 200, body: '{"docs":[]}')
|
|
88
|
+
|
|
89
|
+
@curl.stubs(:perform)
|
|
90
|
+
.raises(Curl::Err::SendError)
|
|
91
|
+
.then.returns(response)
|
|
92
|
+
@curl.expects(:discard_handle).once
|
|
93
|
+
|
|
94
|
+
result = @curl.request(
|
|
95
|
+
:post,
|
|
96
|
+
URI('http://localhost:5984/test/_find'),
|
|
97
|
+
{ selector: { type: 'doc' } }
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
assert_equal response, result
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
test 'retries once after PartialFileError on _all_docs post' do
|
|
104
|
+
response = build_curl_response(status: 200, body: '{"rows":[]}')
|
|
105
|
+
|
|
106
|
+
@curl.stubs(:perform)
|
|
107
|
+
.raises(Curl::Err::PartialFileError)
|
|
108
|
+
.then.returns(response)
|
|
109
|
+
@curl.expects(:discard_handle).once
|
|
110
|
+
|
|
111
|
+
result = @curl.request(
|
|
112
|
+
:post,
|
|
113
|
+
URI('http://localhost:5984/test/_all_docs'),
|
|
114
|
+
{ keys: ['doc/1'] }
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
assert_equal response, result
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
test 'stale connection retry creates a fresh curl handle' do
|
|
121
|
+
first_handle = @curl.thread_local_handle
|
|
122
|
+
attempts = 0
|
|
123
|
+
|
|
124
|
+
@curl.stubs(:perform).with do |*_args|
|
|
125
|
+
attempts += 1
|
|
126
|
+
raise Curl::Err::GotNothingError if attempts == 1
|
|
127
|
+
|
|
128
|
+
true
|
|
129
|
+
end.returns(build_curl_response(status: 200, body: '{"ok":true}'))
|
|
130
|
+
|
|
131
|
+
@curl.request(:get, URI('http://localhost:5984/test/doc'), {})
|
|
132
|
+
second_handle = @curl.thread_local_handle
|
|
133
|
+
|
|
134
|
+
assert_equal 2, attempts
|
|
135
|
+
refute_same first_handle, second_handle
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
test 'discard_handle is safe when no handle exists' do
|
|
139
|
+
assert_nil @curl.discard_handle
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
test 'discard_handle ignores errors while closing handle' do
|
|
143
|
+
curl_easy = @curl.thread_local_handle
|
|
144
|
+
curl_easy.stubs(:close).raises(StandardError, 'broken handle')
|
|
145
|
+
|
|
146
|
+
assert_nil @curl.discard_handle
|
|
147
|
+
assert_nil Thread.current[Dolly::Curl::Connection::THREAD_STORE_KEY][Dolly::Curl::Connection]
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
test 'shared curl connection instances reuse one handle per thread' do
|
|
151
|
+
other = Dolly::Curl::Connection.new(db_name: 'test', reader: stub_everything('reader'))
|
|
152
|
+
|
|
153
|
+
assert_same @curl.thread_local_handle, other.thread_local_handle
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
test 'perform raises for unsupported http methods' do
|
|
157
|
+
error = assert_raises(ArgumentError) do
|
|
158
|
+
@curl.send(:perform, :patch, URI('http://localhost:5984/test/doc'), {})
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
assert_match(/unsupported HTTP method/, error.message)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
test 'apply_get_query! is a no-op for nil or empty data' do
|
|
165
|
+
curl = Curl::Easy.new
|
|
166
|
+
curl.url = 'http://localhost:5984/test/doc'
|
|
167
|
+
|
|
168
|
+
@curl.send(:apply_get_query!, curl, nil)
|
|
169
|
+
assert_equal 'http://localhost:5984/test/doc', curl.url
|
|
170
|
+
|
|
171
|
+
@curl.send(:apply_get_query!, curl, {})
|
|
172
|
+
assert_equal 'http://localhost:5984/test/doc', curl.url
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
test 'apply_get_query! appends encoded query parameters' do
|
|
176
|
+
curl = Curl::Easy.new
|
|
177
|
+
curl.url = 'http://localhost:5984/test/doc'
|
|
178
|
+
|
|
179
|
+
@curl.send(:apply_get_query!, curl, { 'a' => '1', :b => 'two' })
|
|
180
|
+
|
|
181
|
+
assert_equal 'http://localhost:5984/test/doc?a=1&b=two', curl.url
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
test 'apply_get_query! uses ampersand when url already has query string' do
|
|
185
|
+
curl = Curl::Easy.new
|
|
186
|
+
curl.url = 'http://localhost:5984/test/doc?rev=1'
|
|
187
|
+
|
|
188
|
+
@curl.send(:apply_get_query!, curl, { include_docs: true })
|
|
189
|
+
|
|
190
|
+
assert_equal 'http://localhost:5984/test/doc?rev=1&include_docs=true', curl.url
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
test 'payload_json returns strings unchanged' do
|
|
194
|
+
payload = '{"ok":true}'
|
|
195
|
+
|
|
196
|
+
assert_equal payload, @curl.send(:payload_json, payload)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
test 'payload_json encodes hashes as json' do
|
|
200
|
+
assert_equal '{"ok":true}', @curl.send(:payload_json, ok: true)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
test 'put raises AmbiguousWriteError and does not blind retry' do
|
|
204
|
+
put_uri = URI('http://localhost:5984/test/doc%2F1')
|
|
205
|
+
put_calls = 0
|
|
206
|
+
|
|
207
|
+
@curl.stubs(:perform).with do |method, *_|
|
|
208
|
+
put_calls += 1 if method.to_sym == :put
|
|
209
|
+
true
|
|
210
|
+
end.raises(Curl::Err::RecvError)
|
|
211
|
+
@reader.stubs(:fetch_document).returns(nil)
|
|
212
|
+
@curl.expects(:discard_handle).at_least_once
|
|
213
|
+
|
|
214
|
+
error = assert_raises(Dolly::AmbiguousWriteError) do
|
|
215
|
+
@curl.request(:put, put_uri, { _id: 'doc/1', _rev: '1-abc', foo: 'bar' })
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
assert_equal 1, put_calls
|
|
219
|
+
assert_equal :put, error.method
|
|
220
|
+
assert_equal put_uri, error.uri
|
|
221
|
+
assert_kind_of Curl::Err::RecvError, error.cause
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
test 'put reconciles when committed document matches payload' do
|
|
225
|
+
put_uri = URI('http://localhost:5984/test/doc%2F1')
|
|
226
|
+
payload = { _id: 'doc/1', foo: 'bar' }
|
|
227
|
+
stored = { _id: 'doc/1', _rev: '2-new', foo: 'bar' }
|
|
228
|
+
|
|
229
|
+
@curl.stubs(:perform).raises(Curl::Err::RecvError)
|
|
230
|
+
@reader.stubs(:fetch_document).returns(stored)
|
|
231
|
+
|
|
232
|
+
result = @curl.request(:put, put_uri, payload)
|
|
233
|
+
|
|
234
|
+
assert_equal({ ok: true, id: 'doc/1', rev: '2-new' }, result)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
test 'put retries once when create never landed' do
|
|
238
|
+
put_uri = URI('http://localhost:5984/test/doc%2F1')
|
|
239
|
+
payload = { _id: 'doc/1', foo: 'bar' }
|
|
240
|
+
response = build_curl_response(status: 201, body: '{"ok":true,"id":"doc/1","rev":"1-abc"}')
|
|
241
|
+
put_calls = 0
|
|
242
|
+
|
|
243
|
+
@curl.stubs(:perform).with do |method, *_|
|
|
244
|
+
put_calls += 1 if method.to_sym == :put
|
|
245
|
+
true
|
|
246
|
+
end.raises(Curl::Err::GotNothingError).then.returns(response)
|
|
247
|
+
@reader.stubs(:fetch_document).returns(nil)
|
|
248
|
+
|
|
249
|
+
result = @curl.request(:put, put_uri, payload)
|
|
250
|
+
|
|
251
|
+
assert_equal response, result
|
|
252
|
+
assert_equal 2, put_calls
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
test 'put retries once when source revision is unchanged' do
|
|
256
|
+
put_uri = URI('http://localhost:5984/test/doc%2F1')
|
|
257
|
+
payload = { _id: 'doc/1', _rev: '1-abc', foo: 'baz' }
|
|
258
|
+
stored = { _id: 'doc/1', _rev: '1-abc', foo: 'bar' }
|
|
259
|
+
response = build_curl_response(status: 201, body: '{"ok":true,"id":"doc/1","rev":"2-def"}')
|
|
260
|
+
put_calls = 0
|
|
261
|
+
|
|
262
|
+
@curl.stubs(:perform).with do |method, *_|
|
|
263
|
+
put_calls += 1 if method.to_sym == :put
|
|
264
|
+
true
|
|
265
|
+
end.raises(Curl::Err::PartialFileError).then.returns(response)
|
|
266
|
+
@reader.stubs(:fetch_document).returns(stored)
|
|
267
|
+
|
|
268
|
+
result = @curl.request(:put, put_uri, payload)
|
|
269
|
+
|
|
270
|
+
assert_equal response, result
|
|
271
|
+
assert_equal 2, put_calls
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
test 'put raises AmbiguousWriteError when stored content conflicts' do
|
|
275
|
+
put_uri = URI('http://localhost:5984/test/doc%2F1')
|
|
276
|
+
payload = { _id: 'doc/1', _rev: '1-abc', foo: 'bar' }
|
|
277
|
+
stored = { _id: 'doc/1', _rev: '2-other', foo: 'other' }
|
|
278
|
+
|
|
279
|
+
@curl.stubs(:perform).raises(Curl::Err::RecvError)
|
|
280
|
+
@reader.stubs(:fetch_document).returns(stored)
|
|
281
|
+
|
|
282
|
+
assert_raises(Dolly::AmbiguousWriteError) do
|
|
283
|
+
@curl.request(:put, put_uri, payload)
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
test 'attachment put raises AmbiguousWriteError without blind retry' do
|
|
288
|
+
put_uri = URI('http://localhost:5984/test/doc%2F1/attachment.txt')
|
|
289
|
+
put_calls = 0
|
|
290
|
+
|
|
291
|
+
@curl.stubs(:perform).with do |method, *_|
|
|
292
|
+
put_calls += 1 if method.to_sym == :put
|
|
293
|
+
true
|
|
294
|
+
end.raises(Curl::Err::RecvError)
|
|
295
|
+
|
|
296
|
+
assert_raises(Dolly::AmbiguousWriteError) do
|
|
297
|
+
@curl.request(:put, put_uri, { _body: 'payload' })
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
assert_equal 1, put_calls
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
test 'delete reconciles when document is gone' do
|
|
304
|
+
delete_uri = URI('http://localhost:5984/test/doc%2F1?rev=1-abc')
|
|
305
|
+
|
|
306
|
+
@curl.stubs(:perform).raises(Curl::Err::RecvError)
|
|
307
|
+
@reader.stubs(:fetch_document).returns(nil)
|
|
308
|
+
|
|
309
|
+
result = @curl.request(:delete, delete_uri, nil)
|
|
310
|
+
|
|
311
|
+
assert_equal({ ok: true, id: 'doc/1' }, result)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
test 'delete retries once when same revision is still present' do
|
|
315
|
+
delete_uri = URI('http://localhost:5984/test/doc%2F1?rev=1-abc')
|
|
316
|
+
stored = { _id: 'doc/1', _rev: '1-abc', foo: 'bar' }
|
|
317
|
+
response = build_curl_response(status: 200, body: '{"ok":true}')
|
|
318
|
+
delete_calls = 0
|
|
319
|
+
|
|
320
|
+
@curl.stubs(:perform).with do |method, *_|
|
|
321
|
+
delete_calls += 1 if method.to_sym == :delete
|
|
322
|
+
true
|
|
323
|
+
end.raises(Curl::Err::GotNothingError).then.returns(response)
|
|
324
|
+
@reader.stubs(:fetch_document).returns(stored)
|
|
325
|
+
|
|
326
|
+
result = @curl.request(:delete, delete_uri, nil)
|
|
327
|
+
|
|
328
|
+
assert_equal response, result
|
|
329
|
+
assert_equal 2, delete_calls
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
test 'delete raises AmbiguousWriteError when revision changed' do
|
|
333
|
+
delete_uri = URI('http://localhost:5984/test/doc%2F1?rev=1-abc')
|
|
334
|
+
stored = { _id: 'doc/1', _rev: '2-new', foo: 'bar' }
|
|
335
|
+
|
|
336
|
+
@curl.stubs(:perform).raises(Curl::Err::RecvError)
|
|
337
|
+
@reader.stubs(:fetch_document).returns(stored)
|
|
338
|
+
|
|
339
|
+
assert_raises(Dolly::AmbiguousWriteError) do
|
|
340
|
+
@curl.request(:delete, delete_uri, nil)
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
test 'unknown write post raises AmbiguousWriteError without blind retry' do
|
|
345
|
+
post_uri = URI('http://localhost:5984/test/_index')
|
|
346
|
+
post_calls = 0
|
|
347
|
+
|
|
348
|
+
@curl.stubs(:perform).with do |method, *_|
|
|
349
|
+
post_calls += 1 if method.to_sym == :post
|
|
350
|
+
true
|
|
351
|
+
end.raises(Curl::Err::RecvError)
|
|
352
|
+
|
|
353
|
+
error = assert_raises(Dolly::AmbiguousWriteError) do
|
|
354
|
+
@curl.request(:post, post_uri, { name: 'demo', index: { fields: ['_id'] } })
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
assert_equal 1, post_calls
|
|
358
|
+
assert_equal :post, error.method
|
|
359
|
+
assert_kind_of Curl::Err::RecvError, error.cause
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
test 'bulk docs reconciles when all documents are already committed' do
|
|
363
|
+
bulk_uri = URI('http://localhost:5984/test/_bulk_docs')
|
|
364
|
+
docs = [
|
|
365
|
+
{ _id: 'doc/1', name: 'a' },
|
|
366
|
+
{ _id: 'doc/2', name: 'b' }
|
|
367
|
+
]
|
|
368
|
+
post_calls = 0
|
|
369
|
+
|
|
370
|
+
@curl.stubs(:perform).with do |method, uri, *_|
|
|
371
|
+
post_calls += 1 if method.to_sym == :post && uri.path.end_with?('/_bulk_docs')
|
|
372
|
+
true
|
|
373
|
+
end.raises(Curl::Err::RecvError)
|
|
374
|
+
@reader.stubs(:fetch_documents_by_ids).returns(
|
|
375
|
+
'doc/1' => { id: 'doc/1', doc: { _id: 'doc/1', _rev: '1-a', name: 'a' } },
|
|
376
|
+
'doc/2' => { id: 'doc/2', doc: { _id: 'doc/2', _rev: '1-b', name: 'b' } }
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
result = @curl.request(:post, bulk_uri, { docs: docs })
|
|
380
|
+
|
|
381
|
+
assert_equal(
|
|
382
|
+
[
|
|
383
|
+
{ ok: true, id: 'doc/1', rev: '1-a' },
|
|
384
|
+
{ ok: true, id: 'doc/2', rev: '1-b' }
|
|
385
|
+
],
|
|
386
|
+
result
|
|
387
|
+
)
|
|
388
|
+
assert_equal 1, post_calls
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
test 'bulk docs retries only unapplied documents' do
|
|
392
|
+
bulk_uri = URI('http://localhost:5984/test/_bulk_docs')
|
|
393
|
+
docs = [
|
|
394
|
+
{ _id: 'doc/1', name: 'a' },
|
|
395
|
+
{ _id: 'doc/2', name: 'b' }
|
|
396
|
+
]
|
|
397
|
+
pending_payloads = []
|
|
398
|
+
retry_response = build_curl_response(
|
|
399
|
+
status: 200,
|
|
400
|
+
body: '[{"ok":true,"id":"doc/2","rev":"1-b"}]'
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
@curl.stubs(:perform).with do |method, uri, data, *_|
|
|
404
|
+
if method.to_sym == :post && uri.path.end_with?('/_bulk_docs')
|
|
405
|
+
pending_payloads << data
|
|
406
|
+
raise Curl::Err::RecvError if pending_payloads.size == 1
|
|
407
|
+
end
|
|
408
|
+
true
|
|
409
|
+
end.returns(retry_response)
|
|
410
|
+
@reader.stubs(:fetch_documents_by_ids).returns(
|
|
411
|
+
'doc/1' => { id: 'doc/1', doc: { _id: 'doc/1', _rev: '1-a', name: 'a' } },
|
|
412
|
+
'doc/2' => { id: 'doc/2', error: 'not_found' }
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
result = @curl.request(:post, bulk_uri, { docs: docs })
|
|
416
|
+
|
|
417
|
+
assert_equal [{ docs: [{ _id: 'doc/2', name: 'b' }] }], pending_payloads.drop(1)
|
|
418
|
+
assert_equal(
|
|
419
|
+
[
|
|
420
|
+
{ ok: true, id: 'doc/1', rev: '1-a' },
|
|
421
|
+
{ ok: true, id: 'doc/2', rev: '1-b' }
|
|
422
|
+
],
|
|
423
|
+
result
|
|
424
|
+
)
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
test 'bulk docs raises AmbiguousWriteError on conflicting content' do
|
|
428
|
+
bulk_uri = URI('http://localhost:5984/test/_bulk_docs')
|
|
429
|
+
docs = [{ _id: 'doc/1', name: 'a' }]
|
|
430
|
+
|
|
431
|
+
@curl.stubs(:perform).raises(Curl::Err::RecvError)
|
|
432
|
+
@reader.stubs(:fetch_documents_by_ids).returns(
|
|
433
|
+
'doc/1' => { id: 'doc/1', doc: { _id: 'doc/1', _rev: '1-a', name: 'other' } }
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
assert_raises(Dolly::AmbiguousWriteError) do
|
|
437
|
+
@curl.request(:post, bulk_uri, { docs: docs })
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
private
|
|
442
|
+
|
|
443
|
+
def build_curl_response(status:, body:, headers: '')
|
|
444
|
+
stub(status: status.to_s, body_str: body, header_str: headers)
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def clear_thread_local_curl!
|
|
448
|
+
store = Thread.current[Dolly::Curl::Connection::THREAD_STORE_KEY]
|
|
449
|
+
return unless store
|
|
450
|
+
|
|
451
|
+
store.each_value { |handle| handle.close rescue nil }
|
|
452
|
+
store.clear
|
|
453
|
+
Thread.current[Dolly::Curl::Connection::THREAD_STORE_KEY] = nil
|
|
454
|
+
end
|
|
455
|
+
end
|
data/test/mango_index_test.rb
CHANGED
|
@@ -32,7 +32,7 @@ class MangoIndexTest < Test::Unit::TestCase
|
|
|
32
32
|
]}.to_json)
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
test '#create_in_database' do
|
|
35
|
+
test '#create_in_database skips when migrations are disabled' do
|
|
36
36
|
assert_equal "Migrations for design_skipped skiped.", Dolly::MangoIndex.create_in_database(
|
|
37
37
|
:design_skipped,
|
|
38
38
|
"demo",
|
|
@@ -40,7 +40,7 @@ class MangoIndexTest < Test::Unit::TestCase
|
|
|
40
40
|
)
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
test '#create_in_database' do
|
|
43
|
+
test '#create_in_database posts index to database' do
|
|
44
44
|
stub_request(:post, "http://localhost:5984/test/_index").
|
|
45
45
|
to_return(status: 200, body: "Design doc run", headers: {})
|
|
46
46
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dolly
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- javierg
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-07-23 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: oj
|
|
@@ -28,16 +28,16 @@ dependencies:
|
|
|
28
28
|
name: curb
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
30
30
|
requirements:
|
|
31
|
-
- -
|
|
31
|
+
- - "~>"
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: 0
|
|
33
|
+
version: '1.0'
|
|
34
34
|
type: :runtime
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
|
-
- -
|
|
38
|
+
- - "~>"
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: 0
|
|
40
|
+
version: '1.0'
|
|
41
41
|
- !ruby/object:Gem::Dependency
|
|
42
42
|
name: bundler
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -122,7 +122,8 @@ dependencies:
|
|
|
122
122
|
- - ">="
|
|
123
123
|
- !ruby/object:Gem::Version
|
|
124
124
|
version: '0'
|
|
125
|
-
description:
|
|
125
|
+
description: CouchDB adapter for Ruby with thread-local Curb connections, stale keep-alive
|
|
126
|
+
recovery, and safe write reconciliation.
|
|
126
127
|
email:
|
|
127
128
|
- javierg@amcoonline.net
|
|
128
129
|
executables: []
|
|
@@ -137,6 +138,13 @@ files:
|
|
|
137
138
|
- lib/dolly/collection.rb
|
|
138
139
|
- lib/dolly/configuration.rb
|
|
139
140
|
- lib/dolly/connection.rb
|
|
141
|
+
- lib/dolly/curl.rb
|
|
142
|
+
- lib/dolly/curl/connection.rb
|
|
143
|
+
- lib/dolly/curl/document_helper.rb
|
|
144
|
+
- lib/dolly/curl/reader.rb
|
|
145
|
+
- lib/dolly/curl/response_formatter.rb
|
|
146
|
+
- lib/dolly/curl/stale_connection.rb
|
|
147
|
+
- lib/dolly/curl/write_reconciler.rb
|
|
140
148
|
- lib/dolly/depracated_database.rb
|
|
141
149
|
- lib/dolly/document.rb
|
|
142
150
|
- lib/dolly/document_creation.rb
|
|
@@ -164,6 +172,8 @@ files:
|
|
|
164
172
|
- lib/refinements/string_refinements.rb
|
|
165
173
|
- lib/tasks/db.rake
|
|
166
174
|
- test/bulk_document_test.rb
|
|
175
|
+
- test/connection_test.rb
|
|
176
|
+
- test/curl/connection_test.rb
|
|
167
177
|
- test/document_test.rb
|
|
168
178
|
- test/document_type_test.rb
|
|
169
179
|
- test/inheritance_test.rb
|
|
@@ -178,7 +188,7 @@ files:
|
|
|
178
188
|
homepage: https://www.amco.me
|
|
179
189
|
licenses: []
|
|
180
190
|
metadata: {}
|
|
181
|
-
post_install_message:
|
|
191
|
+
post_install_message:
|
|
182
192
|
rdoc_options: []
|
|
183
193
|
require_paths:
|
|
184
194
|
- lib
|
|
@@ -193,10 +203,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
193
203
|
- !ruby/object:Gem::Version
|
|
194
204
|
version: '0'
|
|
195
205
|
requirements: []
|
|
196
|
-
rubygems_version: 3.
|
|
197
|
-
signing_key:
|
|
206
|
+
rubygems_version: 3.2.0
|
|
207
|
+
signing_key:
|
|
198
208
|
specification_version: 4
|
|
199
|
-
summary:
|
|
209
|
+
summary: CouchDB adapter for Ruby
|
|
200
210
|
test_files:
|
|
201
211
|
- test/partitioned_document_test.rb
|
|
202
212
|
- test/slugable_document_test.rb
|
|
@@ -205,8 +215,10 @@ test_files:
|
|
|
205
215
|
- test/mango_index_test.rb
|
|
206
216
|
- test/bulk_document_test.rb
|
|
207
217
|
- test/view_query_test.rb
|
|
218
|
+
- test/curl/connection_test.rb
|
|
208
219
|
- test/mango_test.rb
|
|
209
220
|
- test/support/test.txt
|
|
221
|
+
- test/connection_test.rb
|
|
210
222
|
- test/test_helper.rb
|
|
211
223
|
- test/document_test.rb
|
|
212
224
|
- test/property_manager_test.rb
|