em-pg 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source "http://rubygems.org"
2
+
3
+ group :test do
4
+ gem "minitest"
5
+ gem "minitest-reporters"
6
+ gem "debugger"
7
+ gem "minitest-em-sync"
8
+ end
9
+
10
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,44 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ em-pg (0.1.0)
5
+ eventmachine (>= 0.12)
6
+ pg (>= 0.14)
7
+
8
+ GEM
9
+ remote: http://rubygems.org/
10
+ specs:
11
+ ansi (1.4.3)
12
+ builder (3.1.4)
13
+ columnize (0.3.6)
14
+ debugger (1.2.2)
15
+ columnize (>= 0.3.1)
16
+ debugger-linecache (~> 1.1.1)
17
+ debugger-ruby_core_source (~> 1.1.5)
18
+ debugger-linecache (1.1.2)
19
+ debugger-ruby_core_source (>= 1.1.1)
20
+ debugger-ruby_core_source (1.1.5)
21
+ eventmachine (1.0.0)
22
+ hashie (1.2.0)
23
+ minitest (4.1.0)
24
+ minitest-em-sync (0.1.0)
25
+ eventmachine (>= 0.12)
26
+ minitest-reporters (0.12.0)
27
+ ansi
28
+ builder
29
+ minitest (>= 2.12, < 5.0)
30
+ powerbar
31
+ pg (0.14.1)
32
+ powerbar (1.0.11)
33
+ ansi (~> 1.4.0)
34
+ hashie (>= 1.1.0)
35
+
36
+ PLATFORMS
37
+ ruby
38
+
39
+ DEPENDENCIES
40
+ debugger
41
+ em-pg!
42
+ minitest
43
+ minitest-em-sync
44
+ minitest-reporters
data/README.md ADDED
@@ -0,0 +1,91 @@
1
+ EM::PG
2
+ ======
3
+
4
+ One more EventMachine wrapper for Postgresql [pg-lib](https://github.com/ged/ruby-pg).
5
+
6
+ Features
7
+ --------
8
+
9
+ * Fully async including connect and right usage of #busy and #consume_input;
10
+ * All results are standard deferrable objects;
11
+ * Distinct exceptions hierarchy;
12
+ * Wrapper for [Green](https://github.com/prepor/green) and adapter fot [Sequel](http://sequel.rubyforge.org/).
13
+
14
+ Usage
15
+ -----
16
+
17
+ ```ruby
18
+ gem "em-pg"
19
+ ```
20
+
21
+ ```ruby
22
+ require "em/pg"
23
+
24
+ EM.run do
25
+ db = EM::PG.new host: "localhost", port: 5432, dbname: "test", user: "postgres", password: "postgres"
26
+ db.callback do
27
+ q = db.send_query "select 1"
28
+ q.callback do |res|
29
+ puts "RESULT: #{res.inspect}"
30
+ EM.stop
31
+ end
32
+ q.errback do |e|
33
+ raise e
34
+ end
35
+ end
36
+
37
+ db.errback do |e|
38
+ raise e
39
+ end
40
+ end
41
+ ```
42
+
43
+ To all errbacks pass one argument, instance of EM::PG::Error. So it easy to write common handlers and wrappers for something like EM-Synchrony and Green.
44
+
45
+ ### Supported methods
46
+
47
+ * `send_query`
48
+ * `send_prepare`
49
+ * `send_query_prepared`
50
+ * `send_describe_prepared`
51
+ * `send_describe_portal`
52
+
53
+ All have same semantics as in pg-lib, but result is a Deferrable object
54
+
55
+ ### Disconnects
56
+
57
+ On disconnect all current queries will be failed with exception DisconnectError. You also can pass :on_disconnect callback with options, wich will be called before queries errbacks.
58
+
59
+ EM::PG doesn't have reconnect strategy, you should handle disconnects by youself.
60
+
61
+ ### Logging
62
+
63
+ You can pass :logger option or set `EM::PG.logger` for all instances.
64
+
65
+ Exceptions
66
+ ----------
67
+
68
+ ```
69
+ EM::PG::Error
70
+ ConnectionRefusedError
71
+ DisconnectError
72
+ BadStateError
73
+ UnexpectedStateError
74
+ BadConnectionStatusError
75
+ BadPollStatusError
76
+ PGError
77
+ ```
78
+
79
+ * ConnectionRefusedError - can't connect. Have field `.message` with reason;
80
+ * DisconnectError - connection disconnected. Will be raised on all uncompleted queries;
81
+ * BadStateError - you try do something while a wrong state. For example send query on not connected client;
82
+ * UnexpectedStateError - something gone wrong :(
83
+ * PGError - original PG exceptions was raised. Have field `.original`.
84
+
85
+ See also
86
+ --------
87
+
88
+ * [em-pg-client](https://github.com/royaltm/ruby-em-pg-client) - good wrapper around pg-lib with reconnects.
89
+ * [em-postgres](https://github.com/jtoy/em-postgres)
90
+ * [em-postgresql-sequel](https://github.com/jzimmek/em-postgresql-sequel)
91
+
data/Rakefile ADDED
@@ -0,0 +1,121 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'date'
4
+
5
+ #############################################################################
6
+ #
7
+ # Helper functions
8
+ #
9
+ #############################################################################
10
+
11
+ def name
12
+ @name ||= Dir['*.gemspec'].first.split('.').first
13
+ end
14
+
15
+ def version
16
+ line = File.read("lib/em/pg.rb")[/^\s*VERSION\s*=\s*.*/]
17
+ line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1]
18
+ end
19
+
20
+ def date
21
+ Date.today.to_s
22
+ end
23
+
24
+ def rubyforge_project
25
+ name
26
+ end
27
+
28
+ def gemspec_file
29
+ "#{name}.gemspec"
30
+ end
31
+
32
+ def gem_file
33
+ "#{name}-#{version}.gem"
34
+ end
35
+
36
+ def replace_header(head, header_name)
37
+ head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{send(header_name)}'"}
38
+ end
39
+
40
+ #############################################################################
41
+ #
42
+ # Standard tasks
43
+ #
44
+ #############################################################################
45
+
46
+ task :default => :spec
47
+
48
+ require 'rake/testtask'
49
+ Rake::TestTask.new(:spec) do |test|
50
+ test.libs << 'lib' << 'spec'
51
+ test.pattern = 'spec/**/*_spec.rb'
52
+ test.verbose = true
53
+ end
54
+
55
+ desc "Open an irb session preloaded with this library"
56
+ task :console do
57
+ sh "irb -rubygems -r ./lib/#{name}.rb"
58
+ end
59
+
60
+ #############################################################################
61
+ #
62
+ # Custom tasks (add your own tasks here)
63
+ #
64
+ #############################################################################
65
+
66
+
67
+
68
+ #############################################################################
69
+ #
70
+ # Packaging tasks
71
+ #
72
+ #############################################################################
73
+
74
+ desc "Create tag v#{version} and build and push #{gem_file} to Rubygems"
75
+ task :release => :build do
76
+ unless `git branch` =~ /^\* master$/
77
+ puts "You must be on the master branch to release!"
78
+ exit!
79
+ end
80
+ sh "git commit --allow-empty -a -m 'Release #{version}'"
81
+ sh "git tag v#{version}"
82
+ sh "git push origin master"
83
+ sh "git push origin v#{version}"
84
+ sh "gem push pkg/#{name}-#{version}.gem"
85
+ end
86
+
87
+ desc "Build #{gem_file} into the pkg directory"
88
+ task :build => :gemspec do
89
+ sh "mkdir -p pkg"
90
+ sh "gem build #{gemspec_file}"
91
+ sh "mv #{gem_file} pkg"
92
+ end
93
+
94
+ desc "Generate #{gemspec_file}"
95
+ task :gemspec do
96
+ # read spec file and split out manifest section
97
+ spec = File.read(gemspec_file)
98
+ head, manifest, tail = spec.split(" # = MANIFEST =\n")
99
+
100
+ # replace name version and date
101
+ replace_header(head, :name)
102
+ replace_header(head, :version)
103
+ replace_header(head, :date)
104
+ #comment this out if your rubyforge_project has a different name
105
+ replace_header(head, :rubyforge_project)
106
+
107
+ # determine file list from git ls-files
108
+ files = `git ls-files`.
109
+ split("\n").
110
+ sort.
111
+ reject { |file| file =~ /^\./ }.
112
+ reject { |file| file =~ /^(rdoc|pkg)/ }.
113
+ map { |file| " #{file}" }.
114
+ join("\n")
115
+
116
+ # piece file back together and write
117
+ manifest = " s.files = %w[\n#{files}\n ]\n"
118
+ spec = [head, manifest, tail].join(" # = MANIFEST =\n")
119
+ File.open(gemspec_file, 'w') { |io| io.write(spec) }
120
+ puts "Updated #{gemspec_file}"
121
+ end
data/em-pg.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ spec = Gem::Specification.new do |s|
2
+ s.name = 'em-pg'
3
+ s.version = '0.1.0'
4
+ s.date = '2013-01-28'
5
+ s.summary = 'Async PostgreSQL client API for Ruby/EventMachine'
6
+ s.email = "ceo@prepor.ru"
7
+ s.homepage = "http://github.com/prepor/em-postgres"
8
+ s.description = 'Async PostgreSQL client API for Ruby/EventMachine'
9
+ s.has_rdoc = false
10
+ s.authors = ["Andrew Rudenko"]
11
+ s.add_dependency('eventmachine', '>= 0.12')
12
+ s.add_dependency('pg', '>= 0.14')
13
+
14
+ # = MANIFEST =
15
+ s.files = %w[
16
+ Gemfile
17
+ Gemfile.lock
18
+ README.md
19
+ Rakefile
20
+ em-pg.gemspec
21
+ lib/em/pg.rb
22
+ spec/em/pg_spec.rb
23
+ spec/spec_helper.rb
24
+ ]
25
+ # = MANIFEST =
26
+ end
data/lib/em/pg.rb ADDED
@@ -0,0 +1,220 @@
1
+ require 'eventmachine'
2
+ require 'pg'
3
+ require 'logger'
4
+
5
+ module EM
6
+ class PG
7
+ VERSION = '0.1.0'
8
+ include EM::Deferrable
9
+
10
+ class Error < RuntimeError
11
+ def initialize(params = {})
12
+ params.each { |k, v| self.send("#{k}=", v) }
13
+ end
14
+ end
15
+ class ConnectionRefusedError < Error
16
+ attr_accessor :message
17
+ end
18
+ class DisconnectError < Error; end
19
+ class BadStateError < Error
20
+ attr_accessor :state
21
+ end
22
+ class UnexpectedStateError < Error; end
23
+ class BadConnectionStatusError < UnexpectedStateError; end
24
+ class BadPollStatusError < UnexpectedStateError; end
25
+ class PGError < Error
26
+ attr_accessor :original
27
+ end
28
+
29
+ class Watcher < EM::Connection
30
+ attr_accessor :postgres
31
+ def initialize(postgres)
32
+ @postgres = postgres
33
+ end
34
+
35
+ def notify_readable
36
+ @postgres.handle
37
+ end
38
+
39
+ def notify_writable
40
+ self.notify_writable = false
41
+ @postgres.handle
42
+ end
43
+
44
+ def unbind
45
+ @postgres.unbind
46
+ end
47
+
48
+ end
49
+
50
+
51
+ class Query
52
+ include EM::Deferrable
53
+ attr_accessor :method, :args
54
+ def initialize(method, args)
55
+ @method, @args = method, args
56
+ end
57
+ end
58
+
59
+ class << self
60
+ attr_accessor :logger
61
+ end
62
+ self.logger = Logger.new(STDOUT)
63
+
64
+ attr_accessor :pg, :conn, :opts, :state, :logger, :watcher, :on_disconnect
65
+ def initialize(opts)
66
+ opts = opts.dup
67
+ @logger = opts.delete(:logger) || EM::Postgres.logger
68
+ @on_disconnect = opts.delete(:on_disconnect)
69
+ @opts = opts
70
+ @state = :connecting
71
+
72
+ @pg = ::PG::Connection.connect_start(@opts)
73
+ @queue = []
74
+
75
+ @watcher = EM.watch(@pg.socket, Watcher, self)
76
+ @watcher.notify_readable = true
77
+ check_connect
78
+ end
79
+
80
+ def handle
81
+ case @state
82
+ when :connecting
83
+ check_connect
84
+ when :waiting
85
+ consume_result do |res|
86
+ result_for_query res
87
+ end
88
+ else # try check result, may be it close-message
89
+ consume_result do |res|
90
+ if res.is_a? Exception
91
+ unbind res
92
+ else
93
+ error "Result in unexpected state #{@state}: #{res.inspect}"
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+ def check_connect
100
+ status = @pg.connect_poll
101
+ case status
102
+ when ::PG::PGRES_POLLING_OK
103
+ if pg.status == ::PG::CONNECTION_OK
104
+ connected
105
+ elsif pg.status == ::PG::CONNECTION_BAD
106
+ connection_refused
107
+ else
108
+ raise BadConnectionStatusError.new
109
+ end
110
+ when ::PG::PGRES_POLLING_READING
111
+ when ::PG::PGRES_POLLING_WRITING
112
+ @watcher.notify_writable = true
113
+ when ::PG::PGRES_POLLING_FAILED
114
+ @watcher.detach
115
+ connection_refused
116
+ else
117
+ raise BadPollStatsError.new
118
+ end
119
+ end
120
+
121
+ [:send_query, :send_prepare, :send_query_prepared, :send_describe_prepared, :send_describe_portal].each do |m|
122
+ define_method(m) do |*args|
123
+ make_query(m, *args)
124
+ end
125
+ end
126
+
127
+ def make_query(m, *args)
128
+ q = Query.new m, args
129
+ case @state
130
+ when :waiting
131
+ add_to_queue q
132
+ when :connected
133
+ run_query! q
134
+ else
135
+ q.fail BadStateError.new(state: @state)
136
+ end
137
+ q
138
+ end
139
+
140
+ def add_to_queue(query)
141
+ @queue << query
142
+ end
143
+
144
+ def run_query!(q)
145
+ @current_query = q
146
+ @state = :waiting
147
+ debug(["EM::PG", q.method, q.args])
148
+ @pg.send(q.method, *q.args)
149
+ end
150
+
151
+ def try_next_from_queue
152
+ q = @queue.shift
153
+ if q
154
+ run_query! q
155
+ end
156
+ end
157
+
158
+ def consume_result(&clb)
159
+ begin
160
+ @pg.consume_input # can raise exceptins
161
+ if @pg.is_busy
162
+ else
163
+ clb.call @pg.get_last_result # can raise exceptions
164
+ end
165
+ rescue ::PG::Error => e
166
+ clb.call PGError.new(original: e)
167
+ end
168
+ end
169
+
170
+ def result_for_query(res)
171
+ @state = :connected
172
+ q = @current_query
173
+ @current_query = nil
174
+ if res.is_a? Exception
175
+ q.fail res
176
+ else
177
+ q.succeed res
178
+ end
179
+ try_next_from_queue
180
+ end
181
+
182
+ def connected
183
+ @state = :connected
184
+ succeed :connected
185
+ end
186
+
187
+ def connection_refused
188
+ @state = :connection_refused
189
+ logger.error [:connection_refused, @pg.error_message]
190
+ fail ConnectionRefusedError.new(message: @pg.error_message)
191
+ end
192
+
193
+ def unbind(reason = nil)
194
+ return if @state == :disconnected
195
+ logger.error [:disconnected, reason]
196
+ @state = :disconnected
197
+ @watcher.detach
198
+ @on_disconnect.call if @on_disconnect
199
+ fail_queries DisconnectError.new
200
+ end
201
+
202
+ def close
203
+ @state = :closed
204
+ @watcher.detach
205
+ @pg.finish
206
+ fail_queries :closed
207
+ end
208
+
209
+ def fail_queries(exc)
210
+ @current_query.fail exc if @current_query
211
+ @queue.each { |q| q.fail exc }
212
+ end
213
+
214
+ [:trace, :debug, :info, :warn, :error, :fatal].each do |m|
215
+ define_method(m) do |*args, &blk|
216
+ logger.send(m, *args, &blk)
217
+ end
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,83 @@
1
+ require "spec_helper"
2
+ describe EM::PG do
3
+ let(:db_options) do
4
+ {}
5
+ end
6
+ let(:db) do
7
+ EM::PG.new(DB_CONFIG.merge(db_options)).tap { |o| sync o }
8
+ end
9
+
10
+ include Minitest::EMSync
11
+
12
+ it "should invoke errback on connection failure" do
13
+ conn = EM::PG.new(DB_CONFIG.merge user: "unexist")
14
+ proc { sync conn }.must_raise EM::PG::ConnectionRefusedError
15
+ end
16
+
17
+ describe "with on_disconnect" do
18
+ let(:m) { EM::DefaultDeferrable.new }
19
+
20
+ let(:db_options) do
21
+ { on_disconnect: proc { m.succeed } }
22
+ end
23
+
24
+ it "should invoke on_disconnect" do
25
+ db
26
+ EM.next_tick { db.unbind } # don't known how to emulate real disconnect
27
+ sync m
28
+ end
29
+ end
30
+
31
+ it "should fail current queries on disconnect" do
32
+ q1 = db.send_query("select pg_sleep(10);")
33
+ q2 = db.send_query("select pg_sleep(10);")
34
+ EM.next_tick { db.unbind }
35
+ proc { sync q1 }.must_raise EM::PG::DisconnectError
36
+ proc { sync q2 }.must_raise EM::PG::DisconnectError
37
+ end
38
+
39
+ describe "successful connection" do
40
+ after do
41
+ db.close
42
+ end
43
+
44
+ it "should create a new connection" do
45
+ db.state.must_equal :connected
46
+ end
47
+
48
+ it "should execute sql" do
49
+ query = db.send_query("select 1;")
50
+ res = sync query
51
+ res.first["?column?"].must_equal "1"
52
+ end
53
+
54
+ it "allow custom error callbacks for each query" do
55
+ query = db.send_query("select 1 from")
56
+ proc { sync query }.must_raise EM::PG::PGError
57
+ end
58
+
59
+ it "queue up large amount of queries and execute them in order" do
60
+ results = []
61
+ m = EM::DefaultDeferrable.new
62
+ 100.times do |i|
63
+ db.send_query("select #{i} AS x;").callback do |res|
64
+ results << res.first["x"].to_i
65
+ if results.size == 100
66
+ results.reduce(0, &:+).must_equal 100.times.reduce(0, &:+)
67
+ m.succeed
68
+ end
69
+ end
70
+ end
71
+ sync m
72
+ end
73
+
74
+ describe "not yet connected" do
75
+ let(:db) { EM::PG.new DB_CONFIG }
76
+ it "should fail query" do
77
+ q1 = db.send_query("select 1;")
78
+ proc { sync q1 }.must_raise EM::PG::BadStateError
79
+ end
80
+ end
81
+ end
82
+
83
+ end
@@ -0,0 +1,23 @@
1
+ ENV['BUNDLE_GEMFILE'] = File.expand_path('../../Gemfile', __FILE__)
2
+
3
+ require 'bundler/setup'
4
+
5
+ require 'em/pg'
6
+
7
+ require 'minitest/spec'
8
+ require 'minitest/autorun'
9
+ require 'minitest/reporters'
10
+ require 'minitest/em_sync'
11
+
12
+ logger = Logger.new nil
13
+
14
+ DB_CONFIG = {
15
+ host: "localhost",
16
+ port: 5432,
17
+ dbname: "test",
18
+ user: "postgres",
19
+ password: "postgres",
20
+ logger: logger
21
+ }
22
+
23
+ MiniTest::Reporters.use! MiniTest::Reporters::SpecReporter.new
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: em-pg
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Andrew Rudenko
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-01-28 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: eventmachine
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0.12'
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.12'
30
+ - !ruby/object:Gem::Dependency
31
+ name: pg
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0.14'
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.14'
46
+ description: Async PostgreSQL client API for Ruby/EventMachine
47
+ email: ceo@prepor.ru
48
+ executables: []
49
+ extensions: []
50
+ extra_rdoc_files: []
51
+ files:
52
+ - Gemfile
53
+ - Gemfile.lock
54
+ - README.md
55
+ - Rakefile
56
+ - em-pg.gemspec
57
+ - lib/em/pg.rb
58
+ - spec/em/pg_spec.rb
59
+ - spec/spec_helper.rb
60
+ homepage: http://github.com/prepor/em-postgres
61
+ licenses: []
62
+ post_install_message:
63
+ rdoc_options: []
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ! '>='
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ requirements: []
79
+ rubyforge_project:
80
+ rubygems_version: 1.8.24
81
+ signing_key:
82
+ specification_version: 3
83
+ summary: Async PostgreSQL client API for Ruby/EventMachine
84
+ test_files: []