analysand 1.0.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.
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/.travis.yml +8 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README +41 -0
- data/Rakefile +8 -0
- data/analysand.gemspec +30 -0
- data/bin/analysand +28 -0
- data/lib/analysand.rb +5 -0
- data/lib/analysand/bulk_response.rb +14 -0
- data/lib/analysand/change_watcher.rb +276 -0
- data/lib/analysand/connection_testing.rb +46 -0
- data/lib/analysand/database.rb +492 -0
- data/lib/analysand/errors.rb +26 -0
- data/lib/analysand/instance.rb +156 -0
- data/lib/analysand/response.rb +45 -0
- data/lib/analysand/version.rb +3 -0
- data/lib/analysand/view_response.rb +24 -0
- data/script/setup_database.rb +45 -0
- data/spec/analysand/a_session_grantor.rb +40 -0
- data/spec/analysand/change_watcher_spec.rb +84 -0
- data/spec/analysand/database_spec.rb +228 -0
- data/spec/analysand/database_writing_spec.rb +478 -0
- data/spec/analysand/instance_spec.rb +86 -0
- data/spec/analysand/response_spec.rb +22 -0
- data/spec/analysand/view_response_spec.rb +33 -0
- data/spec/fixtures/vcr_cassettes/get_session_does_not_refresh_cookie.yml +73 -0
- data/spec/fixtures/vcr_cassettes/get_session_refreshes_cookie.yml +75 -0
- data/spec/fixtures/vcr_cassettes/head_request_with_etag.yml +40 -0
- data/spec/spec_helper.rb +19 -0
- data/spec/support/database_access.rb +40 -0
- data/spec/support/example_isolation.rb +86 -0
- data/spec/support/test_parameters.rb +39 -0
- metadata +276 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 David Yip
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
Analysand - a terrible burden for a couch
|
2
|
+
-----------------------------------------
|
3
|
+
|
4
|
+
Analysand is a CouchDB client library of dubious worth. It was extracted from
|
5
|
+
https://github.com/amvorg-underground/catalog.
|
6
|
+
|
7
|
+
Analysand was written for Ruby 1.9. It is known to work on Ruby 1.9.3-p194 and
|
8
|
+
Rubinius 2.0.0.
|
9
|
+
|
10
|
+
Features:
|
11
|
+
|
12
|
+
* GET, PUT, DELETE on databases
|
13
|
+
* GET, PUT, DELETE, HEAD, COPY on documents
|
14
|
+
* GET, PUT on document attachments
|
15
|
+
* GET on views
|
16
|
+
* POST /_session
|
17
|
+
* POST /_bulk_docs
|
18
|
+
* Celluloid::IO-based change feed watchers
|
19
|
+
* Cookie and HTTP Basic authentication for all of the above
|
20
|
+
* Database objects can be safely shared across threads
|
21
|
+
|
22
|
+
Developing Analysand
|
23
|
+
--------------------
|
24
|
+
|
25
|
+
You'll need a CouchDB >= 1.1.0 instance. I recommend not using a CouchDB
|
26
|
+
instance that you're using for anything else; Analysand requires the presence
|
27
|
+
of specific admin and non-admin users for its test suite.
|
28
|
+
|
29
|
+
See spec/support/test_parameters.rb for usernames, passwords, and connection
|
30
|
+
information.
|
31
|
+
|
32
|
+
Naturally, we hang with all the cool kids:
|
33
|
+
|
34
|
+
* Travis CI: http://travis-ci.org/#!/yipdw/analysand
|
35
|
+
* Code Climate: https://codeclimate.com/github/yipdw/analysand
|
36
|
+
* Gemnasium: https://gemnasium.com/yipdw/analysand
|
37
|
+
|
38
|
+
License
|
39
|
+
-------
|
40
|
+
|
41
|
+
Copyright 2012 David Yip; made available under the MIT license.
|
data/Rakefile
ADDED
data/analysand.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/analysand/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["David Yip"]
|
6
|
+
gem.email = ["yipdw@member.fsf.org"]
|
7
|
+
gem.description = %q{A terrible burden for a couch}
|
8
|
+
gem.summary = %q{A CouchDB client of dubious worth}
|
9
|
+
gem.homepage = ""
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "analysand"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Analysand::VERSION
|
17
|
+
|
18
|
+
gem.add_dependency 'celluloid', '>= 0.12'
|
19
|
+
gem.add_dependency 'celluloid-io'
|
20
|
+
gem.add_dependency 'http_parser.rb'
|
21
|
+
gem.add_dependency 'json'
|
22
|
+
gem.add_dependency 'net-http-persistent'
|
23
|
+
gem.add_dependency 'rack'
|
24
|
+
gem.add_dependency 'yajl-ruby'
|
25
|
+
|
26
|
+
gem.add_development_dependency 'rake'
|
27
|
+
gem.add_development_dependency 'rspec'
|
28
|
+
gem.add_development_dependency 'vcr'
|
29
|
+
gem.add_development_dependency 'webmock'
|
30
|
+
end
|
data/bin/analysand
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
4
|
+
|
5
|
+
require 'analysand/database'
|
6
|
+
require 'irb'
|
7
|
+
require 'uri'
|
8
|
+
|
9
|
+
$URI = URI('http://localhost:5984/analysand_test')
|
10
|
+
|
11
|
+
|
12
|
+
def make_db(uri = $URI)
|
13
|
+
Analysand::Database.new(uri)
|
14
|
+
end
|
15
|
+
|
16
|
+
puts <<-END
|
17
|
+
------------------------------------------------------------------------------
|
18
|
+
Type make_db to make an Analysand::Database object. The default URI is
|
19
|
+
|
20
|
+
#{$URI}
|
21
|
+
|
22
|
+
To point at different databases, supply a URI object to make_db, e.g.
|
23
|
+
|
24
|
+
make_db(URI('https://couchdb.example.org:6984/supersekrit'))"
|
25
|
+
------------------------------------------------------------------------------
|
26
|
+
END
|
27
|
+
|
28
|
+
IRB.start
|
data/lib/analysand.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'analysand/response'
|
2
|
+
|
3
|
+
module Analysand
|
4
|
+
##
|
5
|
+
# A subclass of Response that adjusts success? to check for individual error
|
6
|
+
# records.
|
7
|
+
class BulkResponse < Response
|
8
|
+
def success?
|
9
|
+
body.none? { |r| r.has_key?('error') }
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# vim:ts=2:sw=2:et:tw=78
|
@@ -0,0 +1,276 @@
|
|
1
|
+
require 'celluloid'
|
2
|
+
require 'celluloid/io'
|
3
|
+
require 'analysand/connection_testing'
|
4
|
+
require 'http/parser'
|
5
|
+
require 'net/http'
|
6
|
+
require 'rack/utils'
|
7
|
+
require 'uri'
|
8
|
+
require 'yajl'
|
9
|
+
|
10
|
+
module Analysand
|
11
|
+
##
|
12
|
+
# A Celluloid::IO actor that watches the changes feed of a CouchDB database.
|
13
|
+
# When a change is received, it passes the change to a #process method.
|
14
|
+
#
|
15
|
+
# ChangeWatchers monitor changes using continuous mode and set up a heartbeat
|
16
|
+
# to fire approximately every 10 seconds.
|
17
|
+
#
|
18
|
+
# ChangeWatchers begin watching for changes as soon as they are initialized.
|
19
|
+
# To send a shutdown message:
|
20
|
+
#
|
21
|
+
# a.stop
|
22
|
+
#
|
23
|
+
# The watcher will terminate on the next heartbeat.
|
24
|
+
#
|
25
|
+
#
|
26
|
+
# Failure modes
|
27
|
+
# =============
|
28
|
+
#
|
29
|
+
# ChangeWatcher deals with the following failures in the following ways:
|
30
|
+
#
|
31
|
+
# * If Errno::ECONNREFUSED is raised whilst connecting to CouchDB, it will
|
32
|
+
# retry the connection in 30 seconds.
|
33
|
+
# * If the connection to CouchDB's changes feed is abruptly terminated, it
|
34
|
+
# dies.
|
35
|
+
# * If an exception is raised during HTTP or JSON parsing, it dies.
|
36
|
+
#
|
37
|
+
# Situations where the actor dies should be handled by a supervisor.
|
38
|
+
#
|
39
|
+
#
|
40
|
+
# Example usage
|
41
|
+
# =============
|
42
|
+
#
|
43
|
+
# class Accumulator < Analysand::ChangeWatcher
|
44
|
+
# attr_accessor :results
|
45
|
+
#
|
46
|
+
# def initialize(database)
|
47
|
+
# super(database)
|
48
|
+
#
|
49
|
+
# self.results = []
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# def process(change)
|
53
|
+
# results << change
|
54
|
+
#
|
55
|
+
# # Once a ChangeWatcher has successfully processed a change, it
|
56
|
+
# # SHOULD invoke #change_processed.
|
57
|
+
# change_processed(change)
|
58
|
+
# end
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# a = Accumulator.new('http://localhost:5984/mydb')
|
62
|
+
#
|
63
|
+
# # or with supervision:
|
64
|
+
# a = Accumulator.supervise('http://localhost:5984/mydb')
|
65
|
+
class ChangeWatcher
|
66
|
+
include Celluloid::IO
|
67
|
+
include Celluloid::Logger
|
68
|
+
include ConnectionTesting
|
69
|
+
include Rack::Utils
|
70
|
+
|
71
|
+
# Read at most this many bytes off the socket at a time.
|
72
|
+
QUANTUM = 4096
|
73
|
+
|
74
|
+
##
|
75
|
+
# Checks services. If all services pass muster, enters a read loop.
|
76
|
+
#
|
77
|
+
# The database parameter may be either a URL-as-string or a
|
78
|
+
# Analysand::Database.
|
79
|
+
#
|
80
|
+
# If overriding the initializer, you MUST call super.
|
81
|
+
def initialize(database)
|
82
|
+
@db = database
|
83
|
+
@waiting = {}
|
84
|
+
@http_parser = Http::Parser.new(self)
|
85
|
+
@json_parser = Yajl::Parser.new
|
86
|
+
@json_parser.on_parse_complete = lambda { |doc| process(doc) }
|
87
|
+
|
88
|
+
async.start
|
89
|
+
end
|
90
|
+
|
91
|
+
# The URI of the changes feed. This URI incorporates any changes
|
92
|
+
# made by customize_query.
|
93
|
+
def changes_feed_uri
|
94
|
+
query = {
|
95
|
+
'feed' => 'continuous',
|
96
|
+
'heartbeat' => '10000'
|
97
|
+
}
|
98
|
+
|
99
|
+
customize_query(query)
|
100
|
+
|
101
|
+
uri = (@db.respond_to?(:uri) ? @db.uri : URI(@db)).dup
|
102
|
+
uri.path += '/_changes'
|
103
|
+
uri.query = build_query(query)
|
104
|
+
uri
|
105
|
+
end
|
106
|
+
|
107
|
+
# The connection_ok method is called before connecting to the changes feed.
|
108
|
+
# By default, it checks that there's an HTTP service listening on the
|
109
|
+
# changes feed.
|
110
|
+
#
|
111
|
+
# If the method returns true, then we connect to the changes feed and begin
|
112
|
+
# processing. If it returns false, a warning message is logged and the
|
113
|
+
# connection check will be retried in 30 seconds.
|
114
|
+
#
|
115
|
+
# This method can be overridden if you need to check additional services.
|
116
|
+
# When you override the method, make sure that you don't discard the return
|
117
|
+
# value of the original definition:
|
118
|
+
#
|
119
|
+
# # Wrong
|
120
|
+
# def connection_ok
|
121
|
+
# super
|
122
|
+
# ...
|
123
|
+
# end
|
124
|
+
#
|
125
|
+
# # Right
|
126
|
+
# def connection_ok
|
127
|
+
# ok = super
|
128
|
+
#
|
129
|
+
# ok && my_other_test
|
130
|
+
# end
|
131
|
+
def connection_ok
|
132
|
+
test_http_connection(changes_feed_uri)
|
133
|
+
end
|
134
|
+
|
135
|
+
def start
|
136
|
+
return if @started
|
137
|
+
|
138
|
+
@started = true
|
139
|
+
|
140
|
+
while !connection_ok
|
141
|
+
error "Some services used by #{self.class.name} did not check out ok; will retry in 30 seconds"
|
142
|
+
sleep 30
|
143
|
+
end
|
144
|
+
|
145
|
+
connect
|
146
|
+
|
147
|
+
info "#{self.class} entering read loop"
|
148
|
+
|
149
|
+
@running = true
|
150
|
+
|
151
|
+
while @running
|
152
|
+
@http_parser << @socket.readpartial(QUANTUM)
|
153
|
+
end
|
154
|
+
|
155
|
+
# Once we're done, close things up.
|
156
|
+
@started = false
|
157
|
+
@socket.close
|
158
|
+
end
|
159
|
+
|
160
|
+
def stop
|
161
|
+
@running = false
|
162
|
+
end
|
163
|
+
|
164
|
+
##
|
165
|
+
# Called by Celluloid::IO's actor shutdown code.
|
166
|
+
def finalize
|
167
|
+
@socket.close if @socket && !@socket.closed?
|
168
|
+
end
|
169
|
+
|
170
|
+
##
|
171
|
+
# Can be used to set query parameters. query is a Hash. The query hash
|
172
|
+
# has two default parameters:
|
173
|
+
#
|
174
|
+
# | Key | Value |
|
175
|
+
# | feed | continuous |
|
176
|
+
# | heartbeat | 10000 |
|
177
|
+
#
|
178
|
+
# It is NOT RECOMMENDED that they be changed.
|
179
|
+
#
|
180
|
+
# By default, this does nothing. Provide behavior in a subclass.
|
181
|
+
def customize_query(query)
|
182
|
+
end
|
183
|
+
|
184
|
+
##
|
185
|
+
# Can be used to add headers. req is a Net::HTTP::Get instance.
|
186
|
+
#
|
187
|
+
# By default, this does nothing. Provide behavior in a subclass.
|
188
|
+
def customize_request(req)
|
189
|
+
end
|
190
|
+
|
191
|
+
##
|
192
|
+
# This method should implement your change-processing logic.
|
193
|
+
#
|
194
|
+
# change is a Hash containing keys id, seq, and changes. See [0] for
|
195
|
+
# more information.
|
196
|
+
#
|
197
|
+
# By default, this does nothing. Provide behavior in a subclass.
|
198
|
+
#
|
199
|
+
# [0]: http://guide.couchdb.org/draft/notifications.html#continuous
|
200
|
+
def process(change)
|
201
|
+
end
|
202
|
+
|
203
|
+
class Waiter < Celluloid::Future
|
204
|
+
alias_method :wait, :value
|
205
|
+
end
|
206
|
+
|
207
|
+
##
|
208
|
+
# Returns an object that can be used to block a thread until a document
|
209
|
+
# with the given ID has been processed.
|
210
|
+
#
|
211
|
+
# Intended for testing.
|
212
|
+
def waiter_for(id)
|
213
|
+
@waiting[id] = true
|
214
|
+
|
215
|
+
Waiter.new do
|
216
|
+
loop do
|
217
|
+
break true if !@waiting[id]
|
218
|
+
sleep 0.1
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
##
|
224
|
+
# Notify waiters.
|
225
|
+
def change_processed(change)
|
226
|
+
@waiting.delete(change['id'])
|
227
|
+
end
|
228
|
+
|
229
|
+
##
|
230
|
+
# Http::Parser callback.
|
231
|
+
#
|
232
|
+
# @private
|
233
|
+
def on_headers_complete(parser)
|
234
|
+
status = @http_parser.status_code.to_i
|
235
|
+
|
236
|
+
raise "Request failed: expected status 200, got #{status}" unless status == 200
|
237
|
+
end
|
238
|
+
|
239
|
+
##
|
240
|
+
# Http::Parser callback.
|
241
|
+
#
|
242
|
+
# @private
|
243
|
+
def on_body(chunk)
|
244
|
+
@json_parser << chunk
|
245
|
+
end
|
246
|
+
|
247
|
+
##
|
248
|
+
# @private
|
249
|
+
def connect
|
250
|
+
req = prepare_request
|
251
|
+
uri = changes_feed_uri
|
252
|
+
|
253
|
+
info "#{self.class} connecting to #{req.path}"
|
254
|
+
|
255
|
+
@socket = TCPSocket.new(uri.host, uri.port)
|
256
|
+
|
257
|
+
# Make the request.
|
258
|
+
data = [
|
259
|
+
"GET #{req.path} HTTP/1.1"
|
260
|
+
]
|
261
|
+
|
262
|
+
req.each_header { |k, v| data << "#{k}: #{v}" }
|
263
|
+
|
264
|
+
@socket.write(data.join("\r\n"))
|
265
|
+
@socket.write("\r\n\r\n")
|
266
|
+
end
|
267
|
+
|
268
|
+
##
|
269
|
+
# @private
|
270
|
+
def prepare_request
|
271
|
+
Net::HTTP::Get.new(changes_feed_uri.to_s).tap do |req|
|
272
|
+
customize_request(req)
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|