├── History.txt ├── README.md ├── Rakefile ├── VERSION ├── examples ├── Gemfile ├── app.rb ├── config │ └── database.yml ├── rack │ └── fiber_pool.rb ├── rails.rb └── standalone.rb ├── lib ├── active_record │ ├── connection_adapters │ │ └── em_postgresql_adapter.rb │ └── patches.rb ├── fiber_pool.rb └── postgres_connection.rb └── test ├── database.yml └── test_database.rb /History.txt: -------------------------------------------------------------------------------- 1 | v0.4.0 2 | 3 | * Support using driver in blocking mode, in order to support environments like 4 | rake where there is no EM reactor running. 5 | 6 | v0.3.0 7 | 8 | * ActiveRecord patches for fiber-safe connection pooling. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | em_postgresql 2 | --------------- 3 | 4 | An EventMachine-aware driver for using Postgresql with ActiveRecord. 5 | 6 | Requirements 7 | ============== 8 | 9 | * Ruby 1.9 10 | * EventMachine 0.12.10 11 | * postgres-pr 0.6.3 12 | * Rails 2.3.5 13 | 14 | Tested with these version, other versions might work. YMMV. 15 | 16 | You CANNOT have the **pg** gem installed. ActiveRecord prefers the **pg** gem but this code requires 17 | the **postgres-pr** gem to be loaded. I'm not sure if there is a way to make them live together in harmony. 18 | 19 | You'll need to ensure your code is running within an active Fiber using the FiberPool defined in fiber_pool.rb. If you are running Rails in Thin, the following code is a good place to start to figure out how to do this: 20 | 21 | 22 | 23 | Usage 24 | ======= 25 | 26 | List this gem in your `config/environment.rb`: 27 | 28 | config.gem 'postgres-pr', :lib => false 29 | config.gem 'em_postgresql', :lib => false 30 | 31 | and update your `config/database.yml` to contain the proper adapter attribute: 32 | 33 | adapter: em_postgresql 34 | 35 | 36 | Author 37 | ========= 38 | 39 | Mike Perham, mperham AT gmail.com, 40 | [Github](http://github.com/mperham), 41 | [Twitter](http://twitter.com/mperham), 42 | [Blog](http://mikeperham.com) 43 | 44 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # vim: syntax=Ruby 2 | require 'rubygems' 3 | require 'rake/testtask' 4 | 5 | begin 6 | require 'jeweler' 7 | Jeweler::Tasks.new do |s| 8 | s.name = "em_postgresql" 9 | s.summary = s.description = "An ActiveRecord driver for using Postgresql with EventMachine" 10 | s.email = "mperham@gmail.com" 11 | s.homepage = "http://github.com/mperham/em_postgresql" 12 | s.authors = ['Mike Perham'] 13 | s.files = FileList["[A-Z]*", "{lib,test}/**/*"] 14 | s.test_files = FileList["test/test_*.rb"] 15 | s.add_dependency 'postgres-pr', '>=0.6.1' 16 | s.add_dependency 'eventmachine', '>=0.12.10' 17 | end 18 | 19 | rescue LoadError 20 | puts "Jeweler not available. Install it for jeweler-related tasks with: sudo gem install jeweler" 21 | end 22 | 23 | 24 | task :gin => [:gemspec, :build] do 25 | puts `gem install pkg/em_postgresql-#{File.read('VERSION').strip}.gem` 26 | end 27 | 28 | 29 | Rake::TestTask.new 30 | 31 | task :default => :test 32 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.4.0 2 | -------------------------------------------------------------------------------- /examples/Gemfile: -------------------------------------------------------------------------------- 1 | gem 'em-http-request', '0.2.7' 2 | gem 'activerecord', '2.3.5' 3 | gem 'postgres-pr', '0.6.1' 4 | gem 'eventmachine', '0.12.10' 5 | gem 'em_postgresql', '0.2.1' 6 | gem 'rake', '0.8.7' 7 | gem 'thin', '1.2.7' 8 | gem 'sinatra', '1.0' 9 | gem 'rack', '1.1.0' 10 | 11 | # activesupport dependencies 12 | gem 'mime-types', '1.16' 13 | gem 'tmail', '1.2.7.1' 14 | gem "builder", '2.1.2' 15 | gem "memcache-client", '1.8.0' 16 | gem "tzinfo", '0.3.16' 17 | gem "i18n", '0.3.3' 18 | gem 'yajl-ruby', '0.7.3' 19 | 20 | group :test do 21 | gem "mocha", '0.9.8' 22 | end 23 | -------------------------------------------------------------------------------- /examples/app.rb: -------------------------------------------------------------------------------- 1 | raise LoadError, "Ruby 1.9.1 only" if RUBY_VERSION < '1.9.1' 2 | 3 | #require '.bundle/environment' 4 | require 'rubygems' 5 | require 'sinatra/base' 6 | require 'fiber' 7 | 8 | $LOAD_PATH << File.dirname(__FILE__) + '/../lib' 9 | 10 | require 'rails' 11 | require 'rack/fiber_pool' 12 | require 'active_record' 13 | require 'active_record/connection_adapters/abstract_adapter' 14 | 15 | class Site < ActiveRecord::Base 16 | end 17 | 18 | # rackup -s thin app.rb 19 | # http://localhost:9292/test 20 | class App < Sinatra::Base 21 | 22 | use Rack::FiberPool do |fp| 23 | ActiveRecord::ConnectionAdapters.register_fiber_pool(fp) 24 | end 25 | # ConnectionManagement must come AFTER FiberPool 26 | use ActiveRecord::ConnectionAdapters::ConnectionManagement 27 | 28 | set :root, File.dirname(__FILE__) 29 | set :logging, true 30 | set :show_exceptions, Proc.new { development? || test? } 31 | set :raise_errors, Proc.new { production? || staging? } 32 | 33 | get '/test' do 34 | sites = Site.all 35 | content_type "text/plain" 36 | body sites.inspect 37 | end 38 | 39 | helpers do 40 | def self.staging? 41 | environment == :staging 42 | end 43 | end 44 | 45 | configure do 46 | Rails.bootstrap 47 | end 48 | 49 | end -------------------------------------------------------------------------------- /examples/config/database.yml: -------------------------------------------------------------------------------- 1 | base: &base 2 | adapter: em_postgresql 3 | host: localhost 4 | port: 5432 5 | database: onespot_test 6 | username: mike 7 | password: password 8 | 9 | development: 10 | <<: *base 11 | 12 | production: 13 | <<: *base 14 | 15 | staging: 16 | <<: *base 17 | 18 | test: 19 | <<: *base 20 | -------------------------------------------------------------------------------- /examples/rack/fiber_pool.rb: -------------------------------------------------------------------------------- 1 | require 'fiber_pool' 2 | 3 | module Rack 4 | # Run each request in a Fiber. This FiberPool is 5 | # provided by em_postgresql. Should probably split 6 | # this dependency out. 7 | class FiberPool 8 | def initialize(app) 9 | @app = app 10 | @fiber_pool = ::FiberPool.new 11 | yield @fiber_pool if block_given? 12 | end 13 | 14 | def call(env) 15 | call_app = lambda do 16 | result = @app.call(env) 17 | env['async.callback'].call result 18 | end 19 | 20 | @fiber_pool.spawn(&call_app) 21 | throw :async 22 | end 23 | end 24 | end -------------------------------------------------------------------------------- /examples/rails.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' 2 | require 'erb' 3 | 4 | Time.zone = 'UTC' 5 | 6 | module ::Rails 7 | def self.bootstrap 8 | Rails.logger.info "Bootstrapping Rails [#{Rails.root}, #{Rails.env}]" 9 | 10 | Object.const_set(:RAILS_ENV, Rails.env.to_s) 11 | Object.const_set(:RAILS_ROOT, Rails.root) 12 | 13 | filename = File.join(Rails.root, 'config', 'database.yml') 14 | ActiveRecord::Base.configurations = YAML::load(ERB.new(File.read(filename)).result) 15 | ActiveRecord::Base.default_timezone = :utc 16 | ActiveRecord::Base.logger = Rails.logger 17 | ActiveRecord::Base.time_zone_aware_attributes = true 18 | ActiveRecord::Base.establish_connection 19 | end 20 | def self.root 21 | ::App.root 22 | end 23 | def self.env 24 | ::App.environment 25 | end 26 | def self.logger 27 | @logger ||= Logger.new(STDOUT) 28 | end 29 | end 30 | 31 | 32 | -------------------------------------------------------------------------------- /examples/standalone.rb: -------------------------------------------------------------------------------- 1 | # This program demonstrates that the connection pool actual works, as does the wait_timeout option. 2 | # You need to provide your own configuration to #establish_connection. 3 | 4 | $LOAD_PATH << File.dirname(__FILE__) + '/../lib' 5 | 6 | require "eventmachine" 7 | require "fiber" 8 | require "active_record" 9 | require "benchmark" 10 | 11 | ActiveRecord::Base.logger = Logger.new(STDOUT) 12 | ActiveRecord::Base.establish_connection :adapter => "em_postgresql", 13 | :port => 5432, 14 | :pool => 2, 15 | :username => "cjbottaro", 16 | :host => "localhost", 17 | :database => "editor-ui_development", 18 | :wait_timeout => 2 19 | 20 | EM.run do 21 | Fiber.new do 22 | fibers = [] 23 | time = Benchmark.realtime do 24 | 25 | fibers = 5.times.collect do 26 | Fiber.new do 27 | begin 28 | ActiveRecord::Base.connection.execute "select pg_sleep(1)" 29 | ActiveRecord::Base.clear_active_connections! 30 | rescue => e 31 | puts e.inspect 32 | end 33 | end.tap{ |fiber| fiber.resume } 34 | end 35 | 36 | fibers.each do |fiber| 37 | while fiber.alive? 38 | current_fiber = Fiber.current 39 | EM.next_tick{ current_fiber.resume } 40 | Fiber.yield 41 | end 42 | end 43 | 44 | puts "first batch done" 45 | 46 | # This is a copy/paste job. 47 | fibers = 5.times.collect do 48 | Fiber.new do 49 | begin 50 | ActiveRecord::Base.connection.execute "select pg_sleep(1)" 51 | ActiveRecord::Base.clear_active_connections! 52 | rescue => e 53 | puts e.inspect 54 | end 55 | end.tap{ |fiber| fiber.resume } 56 | end 57 | 58 | fibers.each do |fiber| 59 | while fiber.alive? 60 | current_fiber = Fiber.current 61 | EM.next_tick{ current_fiber.resume } 62 | Fiber.yield 63 | end 64 | end 65 | 66 | end 67 | puts time 68 | EM.stop 69 | end.resume 70 | end 71 | -------------------------------------------------------------------------------- /lib/active_record/connection_adapters/em_postgresql_adapter.rb: -------------------------------------------------------------------------------- 1 | require 'postgres_connection' 2 | 3 | require 'active_record' 4 | require 'active_record/connection_adapters/postgresql_adapter' 5 | require 'active_record/patches' 6 | 7 | if !PGconn.respond_to?(:quote_ident) 8 | def PGconn.quote_ident(name) 9 | %("#{name}") 10 | end 11 | end 12 | 13 | module ActiveRecord 14 | module ConnectionAdapters 15 | 16 | class EmPostgreSQLAdapter < PostgreSQLAdapter 17 | # checkin :logi 18 | # checkout :logo 19 | # 20 | # def logo 21 | # puts "#{Fiber.current.object_id} #{self.object_id} checkout" 22 | # end 23 | # def logi 24 | # puts "#{Fiber.current.object_id} #{self.object_id} checkin" 25 | # end 26 | 27 | def initialize(connection, logger, connection_parameters, config) 28 | @hostname = connection_parameters[0] 29 | @port = connection_parameters[1] 30 | @connect_parameters = connection_parameters[4..-1] 31 | super(connection, logger, connection_parameters, config) 32 | end 33 | 34 | def connect 35 | if EM.reactor_running? 36 | @logger.info "Connecting to #{@hostname}:#{@port}" 37 | @connection = ::EM.connect(@hostname, @port, ::EM::P::PostgresConnection) 38 | 39 | fiber = Fiber.current 40 | yielding = true 41 | result = false 42 | message = nil 43 | task = @connection.connect(*@connect_parameters) 44 | task.callback do |rc, msg| 45 | result = rc 46 | message = msg 47 | fiber.resume 48 | end 49 | task.errback do |msg| 50 | result = false 51 | message = msg 52 | yielding = false 53 | end 54 | Fiber.yield if yielding 55 | 56 | raise RuntimeError, "Connection failed: #{message}" if !result 57 | 58 | # Use escape string syntax if available. We cannot do this lazily when encountering 59 | # the first string, because that could then break any transactions in progress. 60 | # See: http://www.postgresql.org/docs/current/static/runtime-config-compatible.html 61 | # If PostgreSQL doesn't know the standard_conforming_strings parameter then it doesn't 62 | # support escape string syntax. Don't override the inherited quoted_string_prefix. 63 | if respond_to?(:supports_standard_conforming_strings?) and supports_standard_conforming_strings? 64 | self.class.instance_eval do 65 | define_method(:quoted_string_prefix) { 'E' } 66 | end 67 | end 68 | 69 | # Money type has a fixed precision of 10 in PostgreSQL 8.2 and below, and as of 70 | # PostgreSQL 8.3 it has a fixed precision of 19. PostgreSQLColumn.extract_precision 71 | # should know about this but can't detect it there, so deal with it here. 72 | money_precision = (postgresql_version >= 80300) ? 19 : 10 73 | PostgreSQLColumn.module_eval(<<-end_eval) 74 | def extract_precision(sql_type) # def extract_precision(sql_type) 75 | if sql_type =~ /^money$/ # if sql_type =~ /^money$/ 76 | #{money_precision} # 19 77 | else # else 78 | super # super 79 | end # end 80 | end # end 81 | end_eval 82 | 83 | configure_connection 84 | @connection 85 | else 86 | @logger.debug "[em_postgresql] EM not running, falling back to blocking behavior" 87 | super 88 | end 89 | end 90 | 91 | def active? 92 | if EM.reactor_running? 93 | begin 94 | !@connection.closed? && @connection.exec('SELECT 1') 95 | rescue RuntimeError => re 96 | false 97 | end 98 | else 99 | super 100 | end 101 | end 102 | 103 | end 104 | end 105 | 106 | class Base 107 | # Establishes a connection to the database that's used by all Active Record objects 108 | def self.em_postgresql_connection(config) # :nodoc: 109 | config = config.symbolize_keys 110 | host = config[:host] 111 | port = config[:port] || 5432 112 | username = config[:username].to_s if config[:username] 113 | password = config[:password].to_s if config[:password] 114 | 115 | if config.has_key?(:database) 116 | database = config[:database] 117 | else 118 | raise ArgumentError, "No database specified. Missing argument: database." 119 | end 120 | 121 | # The postgres drivers don't allow the creation of an unconnected PGconn object, 122 | # so just pass a nil connection object for the time being. 123 | ConnectionAdapters::EmPostgreSQLAdapter.new(nil, logger, [host, port, nil, nil, database, username, password], config) 124 | end 125 | end 126 | 127 | end 128 | -------------------------------------------------------------------------------- /lib/active_record/patches.rb: -------------------------------------------------------------------------------- 1 | module ActiveRecord 2 | module ConnectionAdapters 3 | 4 | def self.fiber_pools 5 | @fiber_pools ||= [] 6 | end 7 | def self.register_fiber_pool(fp) 8 | fiber_pools << fp 9 | end 10 | 11 | class FiberedMonitor 12 | class Queue 13 | def initialize 14 | @queue = [] 15 | end 16 | 17 | def wait(timeout) 18 | t = timeout || 5 19 | fiber = Fiber.current 20 | x = EM::Timer.new(t) do 21 | @queue.delete(fiber) 22 | fiber.resume(false) 23 | end 24 | @queue << fiber 25 | Fiber.yield.tap{ x.cancel } 26 | end 27 | 28 | def signal 29 | fiber = @queue.pop 30 | fiber.resume(true) if fiber 31 | end 32 | end 33 | 34 | def synchronize 35 | yield 36 | end 37 | 38 | def new_cond 39 | Queue.new 40 | end 41 | end 42 | 43 | # ActiveRecord's connection pool is based on threads. Since we are working 44 | # with EM and a single thread, multiple fiber design, we need to provide 45 | # our own connection pool that keys off of Fiber.current so that different 46 | # fibers running in the same thread don't try to use the same connection. 47 | class ConnectionPool 48 | def initialize(spec) 49 | @spec = spec 50 | 51 | # The cache of reserved connections mapped to threads 52 | @reserved_connections = {} 53 | 54 | # The mutex used to synchronize pool access 55 | @connection_mutex = FiberedMonitor.new 56 | @queue = @connection_mutex.new_cond 57 | 58 | # default 5 second timeout unless on ruby 1.9 59 | @timeout = spec.config[:wait_timeout] || 5 60 | 61 | # default max pool size to 5 62 | @size = (spec.config[:pool] && spec.config[:pool].to_i) || 5 63 | 64 | @connections = [] 65 | @checked_out = [] 66 | end 67 | 68 | private 69 | 70 | def current_connection_id #:nodoc: 71 | Fiber.current.object_id 72 | end 73 | 74 | # Remove stale fibers from the cache. 75 | def remove_stale_cached_threads!(cache, &block) 76 | return if ActiveRecord::ConnectionAdapters.fiber_pools.empty? 77 | 78 | keys = Set.new(cache.keys) 79 | 80 | ActiveRecord::ConnectionAdapters.fiber_pools.each do |pool| 81 | pool.busy_fibers.each_pair do |object_id, fiber| 82 | keys.delete(object_id) 83 | end 84 | end 85 | 86 | # puts "Pruning stale connections: #{f.busy_fibers.size} #{f.fibers.size} #{keys.inspect}" 87 | keys.each do |key| 88 | next unless cache.has_key?(key) 89 | block.call(key, cache[key]) 90 | cache.delete(key) 91 | end 92 | end 93 | 94 | # The next three methods (#checkout_new_connection, #checkout_existing_connection and #checkout_and_verify) require modification. 95 | # The reason is because @connection_mutex.synchronize was modified to do nothing, which means #checkout is unguarded. It was 96 | # assumed that was ok because the current fiber wouldn't yield during execution of #checkout, but that is untrue. Both #new_connection 97 | # and #checkout_and_verify will yield the current fiber, thus allowing the body of #checkout to be accessed by multiple fibers at once. 98 | # So if we want this to work without a lock, we need to make sure that the variables used to test the conditions in #checkout are 99 | # modified *before* the current fiber is yielded and the next fiber enters #checkout. 100 | 101 | def checkout_new_connection 102 | 103 | # #new_connection will yield the current fiber, thus we need to fill @connections and @checked_out with placeholders so 104 | # that the next fiber to enter #checkout will take the appropriate action. Once we actually have our connection, we 105 | # replace the placeholders with it. 106 | 107 | @connections << current_connection_id 108 | @checked_out << current_connection_id 109 | 110 | c = new_connection 111 | 112 | @connections[@connections.index(current_connection_id)] = c 113 | @checked_out[@checked_out.index(current_connection_id)] = c 114 | 115 | checkout_and_verify(c) 116 | end 117 | 118 | def checkout_existing_connection 119 | c = (@connections - @checked_out).first 120 | @checked_out << c 121 | checkout_and_verify(c) 122 | end 123 | 124 | def checkout_and_verify(c) 125 | c.run_callbacks :checkout 126 | c.verify! 127 | c 128 | end 129 | end 130 | 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /lib/fiber_pool.rb: -------------------------------------------------------------------------------- 1 | # Author:: Mohammad A. Ali (mailto:oldmoe@gmail.com) 2 | # Copyright:: Copyright (c) 2008 eSpace, Inc. 3 | # License:: Distributes under the same terms as Ruby 4 | 5 | require 'fiber' 6 | 7 | class Fiber 8 | 9 | #Attribute Reference--Returns the value of a fiber-local variable, using 10 | #either a symbol or a string name. If the specified variable does not exist, 11 | #returns nil. 12 | def [](key) 13 | local_fiber_variables[key] 14 | end 15 | 16 | #Attribute Assignment--Sets or creates the value of a fiber-local variable, 17 | #using either a symbol or a string. See also Fiber#[]. 18 | def []=(key,value) 19 | local_fiber_variables[key] = value 20 | end 21 | 22 | private 23 | 24 | def local_fiber_variables 25 | @local_fiber_variables ||= {} 26 | end 27 | end 28 | 29 | class FiberPool 30 | 31 | # gives access to the currently free fibers 32 | attr_reader :fibers 33 | attr_reader :busy_fibers 34 | 35 | # Code can register a proc with this FiberPool to be called 36 | # every time a Fiber is finished. Good for releasing resources 37 | # like ActiveRecord database connections. 38 | attr_accessor :generic_callbacks 39 | 40 | # Prepare a list of fibers that are able to run different blocks of code 41 | # every time. Once a fiber is done with its block, it attempts to fetch 42 | # another one from the queue 43 | def initialize(count = 100) 44 | @fibers,@busy_fibers,@queue,@generic_callbacks = [],{},[],[] 45 | count.times do |i| 46 | fiber = Fiber.new do |block| 47 | loop do 48 | block.call 49 | # callbacks are called in a reverse order, much like c++ destructor 50 | Fiber.current[:callbacks].pop.call while Fiber.current[:callbacks].length > 0 51 | generic_callbacks.each do |cb| 52 | cb.call 53 | end 54 | unless @queue.empty? 55 | block = @queue.shift 56 | else 57 | @busy_fibers.delete(Fiber.current.object_id) 58 | @fibers << Fiber.current 59 | block = Fiber.yield 60 | end 61 | end 62 | end 63 | fiber[:callbacks] = [] 64 | fiber[:em_keys] = [] 65 | @fibers << fiber 66 | end 67 | end 68 | 69 | # If there is an available fiber use it, otherwise, leave it to linger 70 | # in a queue 71 | def spawn(&block) 72 | if fiber = @fibers.shift 73 | fiber[:callbacks] = [] 74 | @busy_fibers[fiber.object_id] = fiber 75 | fiber.resume(block) 76 | else 77 | @queue << block 78 | end 79 | self # we are keen on hiding our queue 80 | end 81 | 82 | end 83 | -------------------------------------------------------------------------------- /lib/postgres_connection.rb: -------------------------------------------------------------------------------- 1 | require 'eventmachine' 2 | require 'postgres-pr/message' 3 | require 'postgres-pr/connection' 4 | require 'stringio' 5 | require 'fiber' 6 | 7 | class StringIO # :nodoc: 8 | # Reads exactly +n+ bytes. 9 | # 10 | # If the data read is nil an EOFError is raised. 11 | # 12 | # If the data read is too short a TruncatedDataError is raised and the read 13 | # data is obtainable via its #data method. 14 | def readbytes(n) 15 | str = read(n) 16 | if str == nil 17 | raise EOFError, "End of file reached" 18 | end 19 | if str.size < n 20 | raise TruncatedDataError.new("data truncated", str) 21 | end 22 | str 23 | end 24 | alias read_exactly_n_bytes readbytes 25 | end 26 | 27 | 28 | module EventMachine 29 | module Protocols 30 | class PostgresConnection < EventMachine::Connection 31 | include PostgresPR 32 | 33 | def initialize 34 | @data = "" 35 | @params = {} 36 | @connected = false 37 | end 38 | 39 | # Fibered impl for synchronous execution of SQL within EM 40 | def exec(sql) 41 | fiber = Fiber.current 42 | # p [fiber.object_id, self.object_id, sql] 43 | yielding = true 44 | (status, result, errors) = nil 45 | d = query(sql) 46 | d.callback do |s, r, e| 47 | (status, result, errors) = s, r, e 48 | fiber.resume 49 | end 50 | d.errback do |msg| 51 | errors = msg 52 | status = false 53 | # errback is called from the same fiber 54 | yielding = false 55 | end 56 | 57 | Fiber.yield if yielding 58 | # p [fiber.object_id, self.object_id, result] 59 | return PGresult.new(result) if status 60 | raise RuntimeError, (errors || result).inspect 61 | end 62 | 63 | def close 64 | close_connection 65 | end 66 | 67 | def closed? 68 | !@connected 69 | end 70 | 71 | def post_init 72 | @connected = true 73 | end 74 | 75 | def unbind 76 | @connected = false 77 | if o = (@pending_query || @pending_conn) 78 | o.succeed false, "lost connection" 79 | end 80 | end 81 | 82 | def connect(db, user, psw=nil) 83 | d = EM::DefaultDeferrable.new 84 | d.timeout 15 85 | 86 | if @pending_query || @pending_conn 87 | d.fail "Operation already in progress" 88 | else 89 | @pending_conn = d 90 | prms = {"user"=>user, "database"=>db} 91 | @user = user 92 | if psw 93 | @password = psw 94 | #prms["password"] = psw 95 | end 96 | send_data PostgresPR::StartupMessage.new( 3 << 16, prms ).dump 97 | end 98 | 99 | d 100 | end 101 | 102 | def query(sql) 103 | d = EM::DefaultDeferrable.new 104 | d.timeout 15 105 | 106 | if !@connected 107 | d.fail "Not connected" 108 | elsif @pending_query || @pending_conn 109 | d.fail "Operation already in progress" 110 | else 111 | @r = PostgresPR::Connection::Result.new 112 | @e = [] 113 | @pending_query = d 114 | send_data PostgresPR::Query.dump(sql) 115 | end 116 | 117 | d 118 | end 119 | 120 | def receive_data(data) 121 | @data << data 122 | while @data.length >= 5 123 | pktlen = @data[1...5].unpack("N").first 124 | if @data.length >= (1 + pktlen) 125 | pkt = @data.slice!(0...(1+pktlen)) 126 | m = StringIO.open( pkt, "r" ) {|io| PostgresPR::Message.read( io ) } 127 | if @pending_conn 128 | dispatch_conn_message m 129 | elsif @pending_query 130 | dispatch_query_message m 131 | else 132 | raise "Unexpected message from database" 133 | end 134 | else 135 | break # very important, break out of the while 136 | end 137 | end 138 | end 139 | 140 | # Cloned and modified from the postgres-pr. 141 | def dispatch_conn_message(msg) 142 | case msg 143 | when AuthentificationClearTextPassword 144 | raise ArgumentError, "no password specified" if @password.nil? 145 | send_data PasswordMessage.new(@password).dump 146 | 147 | when AuthentificationCryptPassword 148 | raise ArgumentError, "no password specified" if @password.nil? 149 | send_data PasswordMessage.new(@password.crypt(msg.salt)).dump 150 | 151 | when AuthentificationMD5Password 152 | raise ArgumentError, "no password specified" if @password.nil? 153 | require 'digest/md5' 154 | 155 | m = Digest::MD5.hexdigest(@password + @user) 156 | m = Digest::MD5.hexdigest(m + msg.salt) 157 | m = 'md5' + m 158 | send_data PasswordMessage.new(m).dump 159 | 160 | when AuthentificationKerberosV4, AuthentificationKerberosV5, AuthentificationSCMCredential 161 | raise "unsupported authentification" 162 | 163 | when AuthentificationOk 164 | when ErrorResponse 165 | raise msg.field_values.join("\t") 166 | when NoticeResponse 167 | @notice_processor.call(msg) if @notice_processor 168 | when ParameterStatus 169 | @params[msg.key] = msg.value 170 | when BackendKeyData 171 | # TODO 172 | #p msg 173 | when ReadyForQuery 174 | # TODO: use transaction status 175 | pc,@pending_conn = @pending_conn,nil 176 | pc.succeed true 177 | else 178 | raise "unhandled message type" 179 | end 180 | end 181 | 182 | # Cloned and modified from the postgres-pr. 183 | def dispatch_query_message(msg) 184 | case msg 185 | when DataRow 186 | @r.rows << msg.columns 187 | when CommandComplete 188 | @r.cmd_tag = msg.cmd_tag 189 | when ReadyForQuery 190 | pq,@pending_query = @pending_query,nil 191 | pq.succeed @e.size == 0, @r, @e 192 | when RowDescription 193 | @r.fields = msg.fields 194 | when CopyInResponse 195 | when CopyOutResponse 196 | when EmptyQueryResponse 197 | when ErrorResponse 198 | @e << msg.field_values[2] 199 | when NoticeResponse 200 | @notice_processor.call(msg) if @notice_processor 201 | when ParameterStatus 202 | else 203 | # TODO 204 | puts "Unknown Postgres message: #{msg}" 205 | end 206 | end 207 | end 208 | end 209 | end 210 | -------------------------------------------------------------------------------- /test/database.yml: -------------------------------------------------------------------------------- 1 | test: 2 | adapter: em_postgresql 3 | host: localhost 4 | username: mike 5 | password: password 6 | database: onespot_test 7 | schema_search_path: bdu,public 8 | statement_timeout: 60 9 | encoding: UTF8 10 | port: 5432 11 | -------------------------------------------------------------------------------- /test/test_database.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'logger' 3 | require 'yaml' 4 | require 'erb' 5 | 6 | gem 'activerecord', '2.3.5' 7 | require 'active_record' 8 | 9 | RAILS_ENV='test' 10 | 11 | ActiveRecord::Base.configurations = YAML::load(ERB.new(File.read(File.join(File.dirname(__FILE__), 'database.yml'))).result) 12 | ActiveRecord::Base.default_timezone = :utc 13 | ActiveRecord::Base.logger = Logger.new(STDOUT) 14 | ActiveRecord::Base.logger.level = Logger::INFO 15 | ActiveRecord::Base.pluralize_table_names = false 16 | ActiveRecord::Base.time_zone_aware_attributes = true 17 | Time.zone = 'UTC' 18 | 19 | require 'eventmachine' 20 | require 'test/unit' 21 | 22 | class Site < ActiveRecord::Base 23 | set_table_name 'site' 24 | end 25 | 26 | class TestDatabase < Test::Unit::TestCase 27 | def test_live_server 28 | EM.run do 29 | Fiber.new do 30 | ActiveRecord::Base.establish_connection 31 | 32 | result = ActiveRecord::Base.connection.query('select id, domain_name from site') 33 | assert result 34 | assert_equal 3, result.size 35 | 36 | result = Site.all 37 | assert result 38 | assert_equal 3, result.size 39 | 40 | result = Site.find(1) 41 | assert_equal 1, result.id 42 | assert_equal 'somedomain.com', result.domain_name 43 | end.resume 44 | 45 | EM.add_timer(1) do 46 | EM.stop 47 | end 48 | 49 | end 50 | end 51 | 52 | def test_without_em 53 | ActiveRecord::Base.establish_connection 54 | 55 | result = ActiveRecord::Base.connection.query('select id, domain_name from site') 56 | assert result 57 | assert_equal 3, result.size 58 | 59 | result = Site.all 60 | assert result 61 | assert_equal 3, result.size 62 | 63 | result = Site.find(1) 64 | assert_equal 1, result.id 65 | assert_equal 'somedomain.com', result.domain_name 66 | end 67 | end --------------------------------------------------------------------------------