em-couchdb-request 0.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 ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in em-couchdb-request.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Louie Zhao
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.md ADDED
@@ -0,0 +1,29 @@
1
+ # Em::Couchdb::Request
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'em-couchdb-request'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install em-couchdb-request
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'em-couchdb-request/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "em-couchdb-request"
8
+ gem.version = EventMachine::Couchdb::VERSION
9
+ gem.authors = ["Louie Zhao"]
10
+ gem.email = ["louie.zhao@gmail.com"]
11
+ gem.description = %q{EventMachine based, async CouchDB request client}
12
+ gem.summary = %q{EventMachine based, async CouchDB request client}
13
+ gem.homepage = ""
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_dependency "json"
21
+ gem.add_dependency "em-http-request"
22
+ gem.add_development_dependency "rspec"
23
+ end
@@ -0,0 +1,53 @@
1
+ module EventMachine
2
+ module Couchdb
3
+
4
+ class Base
5
+
6
+ attr_reader :connect_options, :request_options
7
+
8
+ REQUEST_OPTIONS = {
9
+ :head => {
10
+ 'content-type' => 'application/json'
11
+ }
12
+ }
13
+
14
+ def log(msg, start_at=Time.now)
15
+ span = "%8.2f" % ((Time.now - start_at) * 1000)
16
+ puts "#{Time.now.strftime("%X")}\t#{span}\t#{msg}"
17
+ end
18
+
19
+ protected
20
+
21
+ def base_uri
22
+ raise NotImplementedError
23
+ end
24
+
25
+ def new_http_request(opts={}, &callback)
26
+ start_at = Time.now
27
+
28
+ url = opts.delete :url
29
+ method = opts.delete(:method) || :get
30
+
31
+ request_options = REQUEST_OPTIONS.merge(@request_options || {})
32
+
33
+ http = EventMachine::HttpRequest.new("#{base_uri}/#{url}", @connect_options).send method, request_options.merge(opts)
34
+
35
+ http.callback {
36
+ resp = JSON.load(http.response) rescue {}
37
+ callback.call resp
38
+ }
39
+
40
+ http.errback {
41
+ log "#{http.error || 'ERROR'}:\tunable to #{method} #{url}", start_at
42
+ }
43
+
44
+ http
45
+ end
46
+
47
+ def paramize(opts)
48
+ opts.collect{ |k, v| "#{k}=#{v}" }.join('&')
49
+ end
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1,50 @@
1
+ module EventMachine
2
+ module Couchdb
3
+
4
+ class Database < Base
5
+
6
+ attr_reader :name
7
+
8
+ def initialize(uri, name, opts={})
9
+ @uri = uri
10
+ @name = name
11
+ @connect_options = opts[:connect_options] || {}
12
+ @request_options = opts[:request_options] || {}
13
+ end
14
+
15
+ def base_uri
16
+ [@uri, @name].join('/')
17
+ end
18
+
19
+ def all_docs(&callback)
20
+ new_http_request :url => "_all_docs", &callback
21
+ end
22
+
23
+ def doc(doc_id, &callback)
24
+ new_http_request :url => doc_id, &callback
25
+ end
26
+
27
+ def save_doc(doc, &callback)
28
+ method = doc['id'] ? :put : :post
29
+ new_http_request :url => doc['id'], :method => method, :body => JSON.dump(doc), &callback
30
+ end
31
+
32
+ def delete_doc(doc, &callback)
33
+ new_http_request :url => "#{doc['id']}?rev=#{doc['rev']}", :method => :delete, &callback
34
+ end
35
+
36
+ def changes(opts, &stream)
37
+ http = new_http_request(:url => "_changes?#{paramize(opts)}") {}
38
+
39
+ http.stream { |chunk|
40
+ data = JSON.load(chunk) rescue {}
41
+ stream.call data
42
+ }
43
+
44
+ http
45
+ end
46
+
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,145 @@
1
+ module EventMachine
2
+ module Couchdb
3
+ module LoadTesting
4
+
5
+ class KeyboardHandler < EM::Connection
6
+ include EM::Protocols::LineText2
7
+
8
+ def initialize(project)
9
+ @project = project
10
+ end
11
+
12
+ def receive_line(data)
13
+ @project.lock = false
14
+ end
15
+ end
16
+
17
+ class Agent
18
+ DEFAULT_OPTIONS = {
19
+ :content_size => 1024 * 1, # size of random bytes written to a random document in the database
20
+ :push_interval => 20, # interval between PUSH
21
+ :poll_interval => 0.001, # interval between generating POLL connections - control the creating speed
22
+ :poll_max_count => 2500, # count of the total POLL connections
23
+ :poll_bulk_count => 500, # size of a POLL GROUP - that is created continously (step by POLL_INTERVAL)
24
+ :poll_bulk_interval => 1200 # interval between POLL GROUP (or continue by pressing any key)
25
+ }
26
+
27
+ # lock to disable creating new poll connection
28
+ attr_accessor :lock
29
+
30
+ def initialize(uri, name, opts={})
31
+ @server = EventMachine::Couchdb::Server.new uri, opts
32
+ @db = @server.em_database(name)
33
+ @docs = []
34
+
35
+ @max_id = 0
36
+ @count = 0
37
+ @lock = false
38
+
39
+ @options = DEFAULT_OPTIONS.merge(opts[:agent_options])
40
+ end
41
+
42
+ def push
43
+ EM.run {
44
+ @db.all_docs { |resp|
45
+ @docs = resp["rows"].collect{ |row| row["id"] }
46
+ }
47
+
48
+ puts ">> Start to push to #{@db.base_uri} every #{@options[:push_interval]} seconds ..."
49
+
50
+ EM.add_periodic_timer(@options[:push_interval]) {
51
+ #
52
+ # --- why get the doc first before updating ---
53
+ #
54
+ # To update an existing document, you also issue a PUT request.
55
+ # In this case, the JSON body must contain a _rev property, which lets CouchDB know which revision the edits are based on.
56
+ # If the revision of the document currently stored in the database doesn't match, then a 409 conflict error is returned.
57
+ begin
58
+ start_at = Time.now
59
+ @db.doc(@docs.sample) { |resp|
60
+ # set the new content
61
+ resp[:random_bytes] = SecureRandom.hex(@options[:content_size])
62
+ # update doc and record the latest revision
63
+ @db.save_doc(resp) { |resp|
64
+ @db.log "PUSH\t#{@options[:content_size]}\t#{resp["rev"]}", start_at
65
+ }
66
+ }
67
+ rescue
68
+ @db.log "PUSH\t#{@options[:content_size]}\t#{resp["rev"]}", start_at
69
+ end
70
+ }
71
+ }
72
+ end
73
+
74
+ def poll
75
+ # http://eventmachine.rubyforge.org/docs/EPOLL.html
76
+ EM.epoll
77
+
78
+ EM.run {
79
+ EM.open_keyboard(KeyboardHandler, self)
80
+
81
+ @server.get_db(@db.name) { |resp|
82
+ # ensure to get something for feedback when connection
83
+ seq = resp["update_seq"] - 1
84
+
85
+ # auto continue for each POLL_BULK_INTERVAL
86
+ b_timer = nil
87
+ if @options[:poll_bulk_interval] > 0
88
+ b_timer = EM.add_periodic_timer(@options[:poll_bulk_interval]) {
89
+ @lock = false
90
+ }
91
+ end
92
+
93
+ puts ">> Start to poll from #{@db.base_uri} (since #{seq}) every #{@options[:poll_interval]} seconds ..."
94
+
95
+ timer = EM.add_periodic_timer(@options[:poll_interval]) {
96
+ new_poll_request(:seq => seq) unless @lock
97
+
98
+ if @max_id % @options[:poll_bulk_count] == 0
99
+ @lock = true
100
+ end
101
+
102
+ if @max_id >= @options[:poll_max_count]
103
+ timer.cancel
104
+ b_timer.try(:cancel)
105
+ end
106
+ }
107
+ }
108
+ }
109
+ end
110
+
111
+ protected
112
+
113
+ def new_poll_request(opts={})
114
+ # simulate 'AUTO INCREMENT ID' for index
115
+ @count += 1
116
+ @max_id += 1
117
+ index = @max_id
118
+ start_at = Time.now
119
+
120
+ # http://wiki.apache.org/couchdb/HTTP%5Fdatabase%5FAPI#Changes
121
+ # heartbeat - overrides any timeout to keep the feed alive indefinitely.
122
+ # num - help to debug
123
+ http = @db.changes({:feed => 'continuous', :heartbeat => 600000, :since => opts[:seq], :num => index}) { |chunk|
124
+ # {"seq"=>76, "id"=>"eventmachine_couchdb_document", "changes"=>[{"rev"=>"40-084b145e56f8f1edd2d4cd94b8a01561"}]}
125
+ rev = (chunk['changes'] || [{}]).last['rev']
126
+ @db.log "[#{index}]\t#{@count}\tPOLL\t#{rev}", start_at
127
+ start_at = Time.now
128
+ }
129
+
130
+ http.callback {
131
+ @count -= 1
132
+ @db.log "[#{index}]\t#{@count}\tE_PO\t#{http.response}", start_at
133
+ EM.stop if @count == 0
134
+ }
135
+
136
+ http.errback {
137
+ @count -= 1
138
+ @db.log "[#{index}]\t#{@count}\tF_PO\t#{http.error || 'ERROR'}", start_at
139
+ EM.stop if @count == 0
140
+ }
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,55 @@
1
+ module EventMachine
2
+ module Couchdb
3
+
4
+ class Server < Base
5
+
6
+ attr_reader :uri
7
+
8
+ def initialize(uri, opts={})
9
+ @uri = uri
10
+ @connect_options = opts[:connect_options] || {}
11
+ @request_options = opts[:request_options] || {}
12
+ end
13
+
14
+ def em_database(db_name)
15
+ Database.new @uri, db_name, {:connect_options => @connect_options, :request_options => @request_options}
16
+ end
17
+
18
+ # Async http request
19
+
20
+ def all_dbs(&callback)
21
+ new_http_request :url => "_all_dbs", &callback
22
+ end
23
+
24
+ def get_db(db_name, &callback)
25
+ new_http_request :url => db_name, &callback
26
+ end
27
+
28
+ def create_db(db_name, &callback)
29
+ new_http_request :url => db_name, :method => :put, &callback
30
+ end
31
+
32
+ def delete_db(db_name, &callback)
33
+ new_http_request :url => db_name, :method => :delete, &callback
34
+ end
35
+
36
+ def ensure_db(db_name, &callback)
37
+ get_db(db_name) { |resp|
38
+ if resp['db_name']
39
+ callback.call
40
+ else
41
+ create_db(db_name, &callback)
42
+ end
43
+ }
44
+ end
45
+
46
+ protected
47
+
48
+ def base_uri
49
+ @uri
50
+ end
51
+
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,5 @@
1
+ module EventMachine
2
+ module Couchdb
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,8 @@
1
+ require "json"
2
+ require "eventmachine"
3
+ require "em-http-request"
4
+
5
+ require "em-couchdb-request/version"
6
+ require "em-couchdb-request/base"
7
+ require "em-couchdb-request/server"
8
+ require "em-couchdb-request/database"
@@ -0,0 +1,92 @@
1
+ require 'helper'
2
+
3
+ describe EventMachine::Couchdb do
4
+
5
+ server = EventMachine::Couchdb::Server.new('http://127.0.0.1:5984')
6
+
7
+ document = {
8
+ :time => Time.now
9
+ }
10
+
11
+ context "database" do
12
+ it "should create/get/delete document" do
13
+ EventMachine.run {
14
+ server.ensure_db(DB_NAME_PRFIX) {
15
+ db = server.em_database(DB_NAME_PRFIX)
16
+ db.save_doc(document) { |resp|
17
+ # {"ok"=>true, "id"=>"03399b2af07128fd8bba5650f301f97f", "rev"=>"1-5f68969911f890fe17b3c440b6e6f517"}
18
+ resp['ok'].should == true
19
+ doc_id = resp['id']
20
+ db.all_docs { |resp|
21
+ # {"total_rows"=>2, "offset"=>0, "rows"=>[{"id"=>"03399b2af07128fd8bba5650f3032057", "key"=>"03399b2af07128fd8bba5650f3032057", "value"=>{"rev"=>"1-bdf934324ba35521452df4abb4d92e30"}}, {"id"=>"eventmachine_couchdb_document", "key"=>"eventmachine_couchdb_document", "value"=>{"rev"=>"16-09f0db6392d6595615ae36bbdc9cc5d0"}}]} to include "03399b2af07128fd8bba5650f3032057"
22
+ resp["rows"].collect{ |row| row["id"] }.should include(doc_id)
23
+ db.doc(doc_id) { |resp|
24
+ # {"_id"=>"03399b2af07128fd8bba5650f301f97f", "_rev"=>"1-5f68969911f890fe17b3c440b6e6f517", "time"=>"2012-11-29 17:36:00 +0800"}
25
+ resp[:time] = Time.now
26
+ db.save_doc(resp) { |resp|
27
+ # {"ok"=>true, "id"=>"03399b2af07128fd8bba5650f301f97f", "rev"=>"2-7e493efb649ace136d0f23c59a089f3d"}
28
+ resp['ok'].should == true
29
+ db.delete_doc(resp) { |resp|
30
+ # {"ok"=>true, "id"=>"03399b2af07128fd8bba5650f301f97f", "rev"=>"3-9de477509c9e66189f33bd7fae9b17db"}
31
+ resp['ok'].should == true
32
+ EventMachine.stop
33
+ }
34
+ }
35
+ }
36
+ }
37
+ }
38
+ }
39
+ }
40
+ end
41
+
42
+ it "should get continuous changes" do
43
+ latest_rev = nil
44
+ check_times = 0
45
+
46
+ EventMachine.run {
47
+ server.ensure_db(DB_NAME_PRFIX) {
48
+ db = server.em_database(DB_NAME_PRFIX)
49
+
50
+ # Get database update_seq and fire the continous poll conncetion to wait for changes
51
+ server.get_db(DB_NAME_PRFIX) { |resp|
52
+ seq = resp["update_seq"]
53
+ db.changes({:feed => 'continuous', :since => seq}) { |chunk|
54
+ # {"seq"=>76, "id"=>"eventmachine_couchdb_document", "changes"=>[{"rev"=>"40-084b145e56f8f1edd2d4cd94b8a01561"}]}
55
+ if rev = (chunk['changes'].first['rev'] rescue nil)
56
+ rev.should == latest_rev
57
+ EventMachine.stop if (check_times += 1) == 4
58
+ end
59
+ }
60
+ }
61
+
62
+ EventMachine.add_periodic_timer(0.5) do
63
+ #
64
+ # --- why get the doc first before updating ---
65
+ #
66
+ # To update an existing document, you also issue a PUT request.
67
+ # In this case, the JSON body must contain a _rev property, which lets CouchDB know which revision the edits are based on.
68
+ # If the revision of the document currently stored in the database doesn't match, then a 409 conflict error is returned.
69
+
70
+ db.doc(DOC_ID_PREFIX) { |resp|
71
+ # set to create the document if not exist
72
+ if resp['error'] == 'not_found'
73
+ resp = document
74
+ resp['id'] = DOC_ID_PREFIX
75
+ end
76
+
77
+ # set the new content
78
+ resp[:time] = Time.now
79
+
80
+ # update doc and record the latest revision
81
+ db.save_doc(resp) { |resp|
82
+ latest_rev = resp['rev']
83
+ }
84
+ }
85
+ end
86
+ }
87
+ }
88
+ end
89
+
90
+ end
91
+
92
+ end
data/spec/helper.rb ADDED
@@ -0,0 +1,4 @@
1
+ require 'em-couchdb-request'
2
+
3
+ DB_NAME_PRFIX = "eventmachine_couchdb_database"
4
+ DOC_ID_PREFIX = "eventmachine_couchdb_document"
@@ -0,0 +1,29 @@
1
+ require 'helper'
2
+
3
+ describe EventMachine::Couchdb do
4
+
5
+ context "server" do
6
+ server = EventMachine::Couchdb::Server.new('http://127.0.0.1:5984')
7
+
8
+ it "should create/get/delete database" do
9
+ db_name = "#{DB_NAME_PRFIX}_#{Time.now.to_i}"
10
+
11
+ EventMachine.run {
12
+ server.create_db(db_name) { |resp|
13
+ resp['ok'].should == true
14
+ server.all_dbs { |resp|
15
+ resp.should include(db_name)
16
+ server.get_db(db_name) { |resp|
17
+ resp['update_seq'].should == 0
18
+ server.delete_db(db_name) { |resp|
19
+ resp['ok'].should == true
20
+ EventMachine.stop
21
+ }
22
+ }
23
+ }
24
+ }
25
+ }
26
+ end
27
+ end
28
+
29
+ end
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: em-couchdb-request
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Louie Zhao
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-12-06 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: json
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: em-http-request
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rspec
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ description: EventMachine based, async CouchDB request client
63
+ email:
64
+ - louie.zhao@gmail.com
65
+ executables: []
66
+ extensions: []
67
+ extra_rdoc_files: []
68
+ files:
69
+ - .gitignore
70
+ - Gemfile
71
+ - LICENSE.txt
72
+ - README.md
73
+ - Rakefile
74
+ - em-couchdb-request.gemspec
75
+ - lib/em-couchdb-request.rb
76
+ - lib/em-couchdb-request/base.rb
77
+ - lib/em-couchdb-request/database.rb
78
+ - lib/em-couchdb-request/load_testing/agent.rb
79
+ - lib/em-couchdb-request/server.rb
80
+ - lib/em-couchdb-request/version.rb
81
+ - spec/database_spec.rb
82
+ - spec/helper.rb
83
+ - spec/server_spec.rb
84
+ homepage: ''
85
+ licenses: []
86
+ post_install_message:
87
+ rdoc_options: []
88
+ require_paths:
89
+ - lib
90
+ required_ruby_version: !ruby/object:Gem::Requirement
91
+ none: false
92
+ requirements:
93
+ - - ! '>='
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ required_rubygems_version: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ requirements: []
103
+ rubyforge_project:
104
+ rubygems_version: 1.8.24
105
+ signing_key:
106
+ specification_version: 3
107
+ summary: EventMachine based, async CouchDB request client
108
+ test_files:
109
+ - spec/database_spec.rb
110
+ - spec/helper.rb
111
+ - spec/server_spec.rb