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.
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.sw?
3
+ .rbx
4
+ .bundle
5
+ .config
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - rbx-19mode
4
+ - 1.9.3
5
+ before_script:
6
+ - "./script/setup_database.rb"
7
+ services:
8
+ - couchdb
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in analysand.gemspec
4
+ gemspec
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.
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new
7
+
8
+ task :default => :spec
@@ -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
@@ -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
@@ -0,0 +1,5 @@
1
+ require "analysand/version"
2
+
3
+ module Analysand
4
+ # Your code goes here...
5
+ end
@@ -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