em-pg 0.1.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.
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: []