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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7a6b7aaee7e192101dddab520fbdc363a6e7379d1a52f08c36b3c21fe1371f21
|
|
4
|
+
data.tar.gz: 34e0686cb8726fc60d69f0c84457664c07e86fbbfec579aa0a56ad84c131ce1c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6b43b48ccd6bfd844c68712dbd008c4e75cdff27d43c6c0fa885174e35cd170135b9975a352042a1382b29efa587e120648095a6971d089528d06d1227b4eec2
|
|
7
|
+
data.tar.gz: 374986fde329b1519c228a11fa844f140d8a7cf718b54d04b10f39c6071944bfe5852e2699bffdc34768a43720b4b46c52ad11d48dc2b6f481c8756d0767228a
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Dolly
|
|
2
2
|
|
|
3
|
-
The thing you move your couch with...
|
|
3
|
+
CouchDB adapter for Ruby. The thing you move your couch with...
|
|
4
4
|
|
|
5
5
|

|
|
6
6
|
|
|
@@ -9,7 +9,7 @@ The thing you move your couch with...
|
|
|
9
9
|
Add this line to your application's Gemfile:
|
|
10
10
|
|
|
11
11
|
```ruby
|
|
12
|
-
gem '
|
|
12
|
+
gem 'dolly'
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
And then execute:
|
|
@@ -18,61 +18,112 @@ And then execute:
|
|
|
18
18
|
|
|
19
19
|
Or install it yourself as:
|
|
20
20
|
|
|
21
|
-
$ gem install
|
|
21
|
+
$ gem install dolly
|
|
22
|
+
|
|
23
|
+
Configure CouchDB in `config/couchdb.yml` (see the [sample config](config/couchdb.yml) in this repo).
|
|
22
24
|
|
|
23
25
|
## Usage
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
Define documents as usual:
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
```ruby
|
|
30
|
+
class FooBar < Dolly::Document
|
|
31
|
+
property :foo, :bar
|
|
32
|
+
timestamps!
|
|
33
|
+
end
|
|
28
34
|
|
|
29
|
-
|
|
35
|
+
doc = FooBar.create(foo: 'a', bar: 'b')
|
|
36
|
+
FooBar.find(doc.id)
|
|
37
|
+
```
|
|
30
38
|
|
|
31
|
-
|
|
39
|
+
The public `Dolly::Connection` API (`get`, `post`, `put`, `delete`, `request`, `view`, `attach`, `uuids`, etc.) is unchanged for application code.
|
|
32
40
|
|
|
33
|
-
|
|
41
|
+
### Connection reuse (Curb)
|
|
34
42
|
|
|
35
|
-
|
|
43
|
+
Dolly uses [Curb](https://github.com/taf2/curb) under the hood. Newer Curb releases keep HTTP connections alive, which previously leaked one TCP socket per Dolly request and could exhaust CouchDB's accept backlog during bursts (Sidekiq jobs, seeds, Passenger workers).
|
|
36
44
|
|
|
45
|
+
Dolly now reuses **one `Curl::Easy` handle per thread** via internal `Dolly::Curl`
|
|
46
|
+
collaborators (`Connection` for transport/retries, `WriteReconciler` for ambiguous writes):
|
|
37
47
|
|
|
38
|
-
|
|
48
|
+
- Persistent keep-alive to CouchDB without opening a new socket on every call
|
|
49
|
+
- Safe for typical **Passenger** (one request per thread/process) and **Sidekiq** (one job per thread) deployments
|
|
50
|
+
- Not safe under fiber-based servers that interleave multiple Dolly requests on a single thread
|
|
39
51
|
|
|
40
|
-
|
|
52
|
+
### Stale keep-alive handling
|
|
53
|
+
|
|
54
|
+
If CouchDB closes an idle keep-alive socket, Curb may raise a transport error (`GotNothingError`, `RecvError`, `SendError`, `PartialFileError`). Dolly always discards the dead handle, then:
|
|
55
|
+
|
|
56
|
+
| Request | Behavior |
|
|
57
|
+
|---------|----------|
|
|
58
|
+
| `GET`, `HEAD` | Retry once on a fresh connection |
|
|
59
|
+
| Read-only `POST` (`/_find`, `/_all_docs`) | Retry once on a fresh connection |
|
|
60
|
+
| Document `PUT` | Reconcile by reading the document; succeed if content matches, retry once if the write clearly did not land, otherwise raise |
|
|
61
|
+
| Document `DELETE` (with `rev`) | Reconcile by reading the document; succeed if gone/deleted, retry once if the same rev is still present, otherwise raise |
|
|
62
|
+
| `POST /_bulk_docs` | Reconcile via `/_all_docs`; retry only documents that clearly did not apply |
|
|
63
|
+
| Attachments and other writes | Do **not** blind-retry; raise |
|
|
64
|
+
|
|
65
|
+
### `Dolly::AmbiguousWriteError`
|
|
66
|
+
|
|
67
|
+
Raised when a write may or may not have been applied (transport failed after the request was possibly processed, and reconciliation cannot prove the outcome).
|
|
41
68
|
|
|
69
|
+
```ruby
|
|
70
|
+
begin
|
|
71
|
+
doc.save!
|
|
72
|
+
rescue Dolly::AmbiguousWriteError => e
|
|
73
|
+
e.method # => :put
|
|
74
|
+
e.uri # => #<URI::HTTP ...>
|
|
75
|
+
e.cause # => original Curl::Err::*
|
|
76
|
+
end
|
|
77
|
+
```
|
|
42
78
|
|
|
43
|
-
|
|
79
|
+
Handle this in jobs/seeds by reloading or re-checking the document before retrying the business operation.
|
|
44
80
|
|
|
45
|
-
|
|
46
|
-
`brew services uninstall couchdb`
|
|
81
|
+
## Development
|
|
47
82
|
|
|
48
|
-
|
|
83
|
+
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
49
84
|
|
|
50
|
-
|
|
85
|
+
```bash
|
|
86
|
+
bundle exec rake test
|
|
87
|
+
```
|
|
51
88
|
|
|
52
|
-
|
|
89
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
53
90
|
|
|
54
|
-
|
|
91
|
+
## Contributing
|
|
55
92
|
|
|
93
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/amco/dolly.
|
|
56
94
|
|
|
57
|
-
|
|
95
|
+
## Migrating from CouchDB 1.x to 2.x
|
|
58
96
|
|
|
59
|
-
|
|
97
|
+
[Official docs](https://docs.couchdb.org/en/2.3.1/install/index.html)
|
|
60
98
|
|
|
61
|
-
|
|
99
|
+
You will need to uninstall the couchdb service with brew:
|
|
62
100
|
|
|
63
|
-
|
|
101
|
+
```bash
|
|
102
|
+
brew services stop couchdb
|
|
103
|
+
brew services uninstall couchdb
|
|
104
|
+
```
|
|
64
105
|
|
|
65
|
-
|
|
106
|
+
Download the application from http://couchdb.apache.org/#download, launch Fauxton, and check your installation.
|
|
66
107
|
|
|
67
|
-
|
|
108
|
+
Copy [this file](https://github.com/apache/couchdb/blob/master/rel/overlay/bin/couchup) into your filesystem, make it executable, and run it:
|
|
68
109
|
|
|
69
|
-
|
|
110
|
+
```bash
|
|
111
|
+
chmod +x couchup.py
|
|
112
|
+
./couchup.py -h
|
|
113
|
+
```
|
|
70
114
|
|
|
115
|
+
You might need Python 3, pip3, and:
|
|
71
116
|
|
|
117
|
+
```bash
|
|
118
|
+
pip3 install requests progressbar2
|
|
72
119
|
```
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
120
|
+
|
|
121
|
+
Move your `.couch` files into the specified `database_dir` in your [Fauxton config](http://127.0.0.1:5984/_utils/#_config/couchdb@localhost), then:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
./couchup list # Shows your unmigrated 1.x databases
|
|
125
|
+
./couchup replicate -a # Replicates your 1.x DBs to 2.x
|
|
126
|
+
./couchup rebuild -a # Optional; starts rebuilding your views
|
|
127
|
+
./couchup delete -a # Deletes your 1.x DBs (careful!)
|
|
128
|
+
./couchup list # Should show no remaining databases!
|
|
78
129
|
```
|
data/lib/dolly/connection.rb
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'curb'
|
|
4
|
-
require 'oj'
|
|
5
3
|
require 'cgi'
|
|
6
4
|
require 'net/http'
|
|
7
5
|
require 'dolly/request_header'
|
|
8
6
|
require 'dolly/exceptions'
|
|
9
7
|
require 'dolly/configuration'
|
|
8
|
+
require 'dolly/curl'
|
|
10
9
|
require 'refinements/string_refinements'
|
|
11
|
-
require 'dolly/framework_helper'
|
|
12
10
|
|
|
13
11
|
module Dolly
|
|
14
12
|
class Connection
|
|
15
13
|
include Dolly::Configuration
|
|
16
|
-
include Dolly::
|
|
14
|
+
include Dolly::Curl::ResponseFormatter
|
|
17
15
|
attr_reader :db, :app_env
|
|
18
16
|
|
|
19
17
|
DEFAULT_HEADER = { 'Content-Type' => 'application/json', 'Accept' => 'application/json' }
|
|
@@ -78,7 +76,7 @@ module Dolly
|
|
|
78
76
|
body = fetch_body(data)
|
|
79
77
|
uri = URI("#{base_uri}#{db_resource}")
|
|
80
78
|
|
|
81
|
-
conn =
|
|
79
|
+
conn = curl_connection.request(method, uri, body) do |curl|
|
|
82
80
|
if env['username'] && !env['username'].empty?
|
|
83
81
|
curl.http_auth_types = :basic
|
|
84
82
|
curl.username = env['username']
|
|
@@ -88,11 +86,20 @@ module Dolly
|
|
|
88
86
|
headers.each { |k, v| curl.headers[k] = v } unless !headers || headers.empty?
|
|
89
87
|
end
|
|
90
88
|
|
|
89
|
+
return conn unless conn.is_a?(::Curl::Easy)
|
|
90
|
+
|
|
91
91
|
response_format(conn, method)
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
private
|
|
95
95
|
|
|
96
|
+
def curl_connection
|
|
97
|
+
@curl_connection ||= Dolly::Curl::Connection.new(
|
|
98
|
+
db_name: db_name,
|
|
99
|
+
reader: Dolly::Curl::Reader.new(self)
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
|
|
96
103
|
def fetch_headers(data)
|
|
97
104
|
return unless data.is_a?(Hash)
|
|
98
105
|
Dolly::HeaderRequest.new(data&.delete(:headers))
|
|
@@ -105,28 +112,6 @@ module Dolly
|
|
|
105
112
|
data&.merge!(data&.delete(:query) || {})
|
|
106
113
|
end
|
|
107
114
|
|
|
108
|
-
def curl_method_call(method, uri, data, &block)
|
|
109
|
-
return Curl::Easy.http_head(uri.to_s, &block) if method.to_sym == :head
|
|
110
|
-
return Curl.delete(uri.to_s, &block) if method.to_sym == :delete
|
|
111
|
-
return Curl.send(method, uri, data, &block) if method.to_sym == :get
|
|
112
|
-
Curl.send(method, uri.to_s, data.to_json, &block)
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
def response_format(res, method)
|
|
116
|
-
raise Dolly::ResourceNotFound if res.status.to_i == 404
|
|
117
|
-
raise Dolly::ServerError.new(res.status.to_i) if (400..600).include? res.status.to_i
|
|
118
|
-
return res.header_str if method == :head
|
|
119
|
-
|
|
120
|
-
data = Oj.load(res.body_str, symbol_keys: true)
|
|
121
|
-
return data unless rails?
|
|
122
|
-
return data.with_indifferent_access if data.is_a?(Hash)
|
|
123
|
-
data
|
|
124
|
-
rescue Oj::ParseError
|
|
125
|
-
res.body_str
|
|
126
|
-
ensure
|
|
127
|
-
GC.start if res&.body_str&.length&.to_i > 250000
|
|
128
|
-
end
|
|
129
|
-
|
|
130
115
|
def values_to_json hash
|
|
131
116
|
hash.each_with_object({}) { |(k,v), h| h[k] = v.is_a?(Numeric) ? v : v.to_json }
|
|
132
117
|
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'curb'
|
|
4
|
+
require 'cgi'
|
|
5
|
+
require 'dolly/curl/stale_connection'
|
|
6
|
+
require 'dolly/curl/write_reconciler'
|
|
7
|
+
|
|
8
|
+
module Dolly
|
|
9
|
+
module Curl
|
|
10
|
+
# Thread-local Curb transport with stale-connection handling.
|
|
11
|
+
#
|
|
12
|
+
# Reuses one ::Curl::Easy handle per thread, retries safe reads once, and
|
|
13
|
+
# delegates ambiguous writes to WriteReconciler.
|
|
14
|
+
#
|
|
15
|
+
# Assumes one in-flight request per thread (not safe under fiber-based servers).
|
|
16
|
+
#
|
|
17
|
+
# +reader+ must respond to:
|
|
18
|
+
# - fetch_document(doc_id) -> Hash or nil
|
|
19
|
+
# - fetch_documents_by_ids(ids) -> Hash keyed by id
|
|
20
|
+
class Connection
|
|
21
|
+
MAX_STALE_RETRIES = 1
|
|
22
|
+
THREAD_STORE_KEY = :dolly_curl_easy
|
|
23
|
+
READ_ONLY_POST_SUFFIXES = %w[/_find /_all_docs].freeze
|
|
24
|
+
|
|
25
|
+
attr_reader :db_name, :reader
|
|
26
|
+
|
|
27
|
+
def initialize(db_name:, reader:, reconciler: nil)
|
|
28
|
+
@db_name = db_name
|
|
29
|
+
@reader = reader
|
|
30
|
+
@reconciler = reconciler
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def request(method, uri, data = nil, retries = 0, &block)
|
|
34
|
+
perform(method, uri, data, &block)
|
|
35
|
+
rescue *StaleConnection::ERRORS => error
|
|
36
|
+
discard_handle
|
|
37
|
+
|
|
38
|
+
if safe_to_retry?(method, uri)
|
|
39
|
+
raise error if retries >= MAX_STALE_RETRIES
|
|
40
|
+
|
|
41
|
+
return request(method, uri, data, retries + 1, &block)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
reconciler.reconcile(method, uri, data, error, &block)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def perform(method, uri, data = nil, &block)
|
|
48
|
+
handle = prepare_handle(uri, &block)
|
|
49
|
+
dispatch_http(handle, method, data)
|
|
50
|
+
handle
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def discard_handle
|
|
54
|
+
store = Thread.current[THREAD_STORE_KEY]
|
|
55
|
+
return unless store
|
|
56
|
+
|
|
57
|
+
handle = store.delete(self.class)
|
|
58
|
+
handle&.close
|
|
59
|
+
rescue StandardError
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def thread_local_handle
|
|
64
|
+
store = Thread.current[THREAD_STORE_KEY] ||= {}
|
|
65
|
+
handle = store[self.class]
|
|
66
|
+
return handle unless handle.nil? || mocked_curl_class?(handle)
|
|
67
|
+
|
|
68
|
+
begin
|
|
69
|
+
handle&.close
|
|
70
|
+
rescue StandardError
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
store[self.class] = ::Curl::Easy.new
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def mocked_curl_class?(handle)
|
|
80
|
+
!handle.instance_of?(::Curl::Easy)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def reconciler
|
|
84
|
+
@reconciler ||= WriteReconciler.new(
|
|
85
|
+
transport: self,
|
|
86
|
+
reader: reader,
|
|
87
|
+
db_name: db_name
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def prepare_handle(uri)
|
|
92
|
+
handle = thread_local_handle
|
|
93
|
+
handle.reset
|
|
94
|
+
handle.url = uri.to_s
|
|
95
|
+
handle.headers['Content-Type'] = 'application/json'
|
|
96
|
+
handle.headers['Accept'] = 'application/json'
|
|
97
|
+
yield handle if block_given?
|
|
98
|
+
handle
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def dispatch_http(handle, method, data)
|
|
102
|
+
case method.to_sym
|
|
103
|
+
when :head then handle.http_head
|
|
104
|
+
when :delete then handle.http_delete
|
|
105
|
+
when :get then http_get(handle, data)
|
|
106
|
+
when :post then handle.http_post(payload_json(data))
|
|
107
|
+
when :put then handle.http_put(payload_json(data))
|
|
108
|
+
else
|
|
109
|
+
raise ArgumentError, "unsupported HTTP method: #{method}"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def http_get(handle, data)
|
|
114
|
+
apply_get_query!(handle, data)
|
|
115
|
+
handle.http_get
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def payload_json(data)
|
|
119
|
+
return data if data.is_a?(String)
|
|
120
|
+
|
|
121
|
+
data.to_json
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def apply_get_query!(handle, data)
|
|
125
|
+
return if data.nil? || data.empty?
|
|
126
|
+
|
|
127
|
+
query = data.map do |key, value|
|
|
128
|
+
"#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"
|
|
129
|
+
end.join('&')
|
|
130
|
+
separator = handle.url.include?('?') ? '&' : '?'
|
|
131
|
+
handle.url = "#{handle.url}#{separator}#{query}"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def safe_to_retry?(method, uri)
|
|
135
|
+
case method.to_sym
|
|
136
|
+
when :get, :head
|
|
137
|
+
true
|
|
138
|
+
when :post
|
|
139
|
+
read_only_post?(uri)
|
|
140
|
+
else
|
|
141
|
+
false
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def read_only_post?(uri)
|
|
146
|
+
READ_ONLY_POST_SUFFIXES.any? { |suffix| uri.path.end_with?(suffix) }
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cgi'
|
|
4
|
+
require 'oj'
|
|
5
|
+
|
|
6
|
+
module Dolly
|
|
7
|
+
module Curl
|
|
8
|
+
# Document identity, URI parsing, and equality helpers for write reconciliation.
|
|
9
|
+
class DocumentHelper
|
|
10
|
+
IGNORED_COMPARE_KEYS = %i[_rev].freeze
|
|
11
|
+
|
|
12
|
+
def initialize(db_name:)
|
|
13
|
+
@db_name = db_name
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def id_from_uri(uri)
|
|
17
|
+
prefix = "/#{@db_name}/"
|
|
18
|
+
path = uri.path
|
|
19
|
+
return nil unless path.start_with?(prefix)
|
|
20
|
+
|
|
21
|
+
rest = path[prefix.length..-1]
|
|
22
|
+
return nil if rest.nil? || rest.empty?
|
|
23
|
+
return nil if rest.start_with?('_')
|
|
24
|
+
return nil if rest.include?('/')
|
|
25
|
+
|
|
26
|
+
CGI.unescape(rest)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def rev_from_uri(uri)
|
|
30
|
+
return nil unless uri.query
|
|
31
|
+
|
|
32
|
+
CGI.parse(uri.query)['rev']&.first
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def normalize(data)
|
|
36
|
+
return data.transform_keys(&:to_sym) if data.is_a?(Hash)
|
|
37
|
+
return nil if data.nil?
|
|
38
|
+
|
|
39
|
+
parsed = data.is_a?(String) ? Oj.load(data, symbol_keys: true) : nil
|
|
40
|
+
parsed.is_a?(Hash) ? parsed : nil
|
|
41
|
+
rescue Oj::ParseError
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def matches?(stored, intended)
|
|
46
|
+
intended.each do |key, expected|
|
|
47
|
+
key = key.to_sym
|
|
48
|
+
next if IGNORED_COMPARE_KEYS.include?(key)
|
|
49
|
+
return false unless value(stored, key) == expected
|
|
50
|
+
end
|
|
51
|
+
true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def value(doc, key)
|
|
55
|
+
return doc[key] if doc.key?(key)
|
|
56
|
+
return doc[key.to_s] if doc.key?(key.to_s)
|
|
57
|
+
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def id(doc)
|
|
62
|
+
value(doc, :_id)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def revision(doc)
|
|
66
|
+
value(doc, :_rev)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def deleted?(doc)
|
|
70
|
+
value(doc, :_deleted)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def docs_from_bulk_payload(data)
|
|
74
|
+
payload = normalize(data)
|
|
75
|
+
return [] unless payload
|
|
76
|
+
|
|
77
|
+
Array(payload[:docs]).map { |doc| normalize(doc) }.compact
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cgi'
|
|
4
|
+
require 'dolly/exceptions'
|
|
5
|
+
|
|
6
|
+
module Dolly
|
|
7
|
+
module Curl
|
|
8
|
+
# Reads CouchDB state for write reconciliation via Connection's public API.
|
|
9
|
+
class Reader
|
|
10
|
+
def initialize(connection)
|
|
11
|
+
@connection = connection
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def fetch_document(doc_id)
|
|
15
|
+
@connection.request(:get, CGI.escape(doc_id.to_s))
|
|
16
|
+
rescue Dolly::ResourceNotFound
|
|
17
|
+
nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def fetch_documents_by_ids(ids)
|
|
21
|
+
response = @connection.post('_all_docs', keys: ids, query: { include_docs: true })
|
|
22
|
+
rows = response[:rows] || []
|
|
23
|
+
rows.each_with_object({}) do |row, memo|
|
|
24
|
+
memo[row[:id] || row[:key]] = row
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'oj'
|
|
4
|
+
require 'dolly/framework_helper'
|
|
5
|
+
require 'dolly/exceptions'
|
|
6
|
+
|
|
7
|
+
module Dolly
|
|
8
|
+
module Curl
|
|
9
|
+
module ResponseFormatter
|
|
10
|
+
include Dolly::FrameworkHelper
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def response_format(res, method)
|
|
15
|
+
status = res.status.to_i
|
|
16
|
+
raise Dolly::ResourceNotFound if status == 404
|
|
17
|
+
raise Dolly::ServerError.new(status) if (400..600).include?(status)
|
|
18
|
+
return res.header_str if method == :head
|
|
19
|
+
|
|
20
|
+
data = Oj.load(res.body_str, symbol_keys: true)
|
|
21
|
+
return data unless rails?
|
|
22
|
+
return data.with_indifferent_access if data.is_a?(Hash)
|
|
23
|
+
|
|
24
|
+
data
|
|
25
|
+
rescue Oj::ParseError
|
|
26
|
+
res.body_str
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'curb'
|
|
4
|
+
|
|
5
|
+
module Dolly
|
|
6
|
+
module Curl
|
|
7
|
+
module StaleConnection
|
|
8
|
+
ERRORS = [
|
|
9
|
+
::Curl::Err::GotNothingError,
|
|
10
|
+
::Curl::Err::RecvError,
|
|
11
|
+
::Curl::Err::SendError,
|
|
12
|
+
::Curl::Err::PartialFileError
|
|
13
|
+
].freeze
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|