Merge branch 'feature/pool'

Conflicts:
	src/db/error.cr
This commit is contained in:
Brian J. Cardiff 2016-09-13 01:42:24 -03:00
commit 751be7aa6a
14 changed files with 767 additions and 89 deletions

90
spec/database_spec.cr Normal file
View file

@ -0,0 +1,90 @@
require "./spec_helper"
describe DB::Database do
it "allows connection initialization" do
cnn_setup = 0
DB.open "dummy://localhost:1027?initial_pool_size=2&max_pool_size=4&max_idle_pool_size=1" do |db|
cnn_setup.should eq(0)
db.setup_connection do |cnn|
cnn_setup += 1
end
cnn_setup.should eq(2)
db.using_connection do
cnn_setup.should eq(2)
db.using_connection do
cnn_setup.should eq(2)
db.using_connection do
cnn_setup.should eq(3)
db.using_connection do
cnn_setup.should eq(4)
end
# the pool didn't shrink no new initialization should be done next
db.using_connection do
cnn_setup.should eq(4)
end
end
# the pool shrink 1. max_idle_pool_size=1
# after the previous end there where 2.
db.using_connection do
cnn_setup.should eq(4)
# so now there will be a new connection created
db.using_connection do
cnn_setup.should eq(5)
end
end
end
end
end
end
it "should allow creation of more statements than pool connections" do
DB.open "dummy://localhost:1027?initial_pool_size=1&max_pool_size=2" do |db|
db.prepare("query1").should be_a(DB::PoolStatement)
db.prepare("query2").should be_a(DB::PoolStatement)
db.prepare("query3").should be_a(DB::PoolStatement)
end
end
it "should return same statement in pool per query" do
with_dummy do |db|
stmt = db.prepare("query1")
db.prepare("query2").should_not eq(stmt)
db.prepare("query1").should eq(stmt)
end
end
it "should close pool statements when closing db" do
stmt = uninitialized DB::PoolStatement
with_dummy do |db|
stmt = db.prepare("query1")
end
stmt.closed?.should be_true
end
it "should not reconnect if connection is lost and retry_attempts=0" do
DummyDriver::DummyConnection.clear_connections
DB.open "dummy://localhost:1027?initial_pool_size=1&max_pool_size=1&retry_attempts=0" do |db|
db.exec("stmt1")
DummyDriver::DummyConnection.connections.size.should eq(1)
DummyDriver::DummyConnection.connections.first.disconnect!
expect_raises DB::PoolRetryAttemptsExceeded do
db.exec("stmt1")
end
DummyDriver::DummyConnection.connections.size.should eq(1)
end
end
it "should reconnect if connection is lost and executing same statement" do
DummyDriver::DummyConnection.clear_connections
DB.open "dummy://localhost:1027?initial_pool_size=1&max_pool_size=1&retry_attempts=1" do |db|
db.exec("stmt1")
DummyDriver::DummyConnection.connections.size.should eq(1)
DummyDriver::DummyConnection.connections.first.disconnect!
db.exec("stmt1")
DummyDriver::DummyConnection.connections.size.should eq(2)
end
end
end

View file

@ -61,7 +61,7 @@ describe DB do
it "should raise if the sole connection is been used" do
with_dummy do |db|
db.query "1" do |rs|
expect_raises Exception, /DB Pool Exhausted/ do
expect_raises DB::PoolTimeout do
db.scalar "2"
end
end

View file

@ -9,6 +9,7 @@ class DummyDriver < DB::Driver
class DummyConnection < DB::Connection
def initialize(db)
super(db)
@connected = true
@@connections ||= [] of DummyConnection
@@connections.not_nil! << self
end
@ -29,6 +30,14 @@ class DummyDriver < DB::Driver
0
end
def check
raise DB::ConnectionLost.new(self) unless @connected
end
def disconnect!
@connected = false
end
protected def do_close
super
end
@ -43,11 +52,13 @@ class DummyDriver < DB::Driver
end
protected def perform_query(args : Enumerable)
@connection.as(DummyConnection).check
set_params args
DummyResultSet.new self, @query
end
protected def perform_exec(args : Enumerable)
@connection.as(DummyConnection).check
set_params args
raise "forced exception due to query" if @query == "raise"
DB::ExecResult.new 0i64, 0_i64
@ -188,7 +199,15 @@ end
def with_dummy
DummyDriver::DummyConnection.clear_connections
DB.open "dummy://host" do |db|
DB.open "dummy://host?checkout_timeout=0.5" do |db|
yield db
end
end
def with_dummy_connection
with_dummy do |db|
db.using_connection do |cnn|
yield cnn.as(DummyDriver::DummyConnection)
end
end
end

192
spec/pool_spec.cr Normal file
View file

@ -0,0 +1,192 @@
require "./spec_helper"
class ShouldSleepingOp
@is_sleeping = false
getter is_sleeping
getter sleep_happened
def initialize
@sleep_happened = Channel(Nil).new
end
def should_sleep
s = self
@is_sleeping = true
spawn do
sleep 0.1
s.is_sleeping.should be_true
s.sleep_happened.send(nil)
end
yield
@is_sleeping = false
end
def wait_for_sleep
@sleep_happened.receive
end
end
class WaitFor
def initialize
@channel = Channel(Nil).new
end
def wait
@channel.receive
end
def check
@channel.send(nil)
end
end
class Closable
include DB::Disposable
protected def do_close
end
end
describe DB::Pool do
it "should use proc to create objects" do
block_called = 0
pool = DB::Pool.new(initial_pool_size: 3) { block_called += 1; Closable.new }
block_called.should eq(3)
end
it "should get resource" do
pool = DB::Pool.new { Closable.new }
pool.checkout.should be_a Closable
end
it "should be available if not checkedout" do
resource = uninitialized Closable
pool = DB::Pool.new(initial_pool_size: 1) { resource = Closable.new }
pool.is_available?(resource).should be_true
end
it "should not be available if checkedout" do
pool = DB::Pool.new { Closable.new }
resource = pool.checkout
pool.is_available?(resource).should be_false
end
it "should be available if returned" do
pool = DB::Pool.new { Closable.new }
resource = pool.checkout
pool.release resource
pool.is_available?(resource).should be_true
end
it "should wait for available resource" do
pool = DB::Pool.new(max_pool_size: 1, initial_pool_size: 1) { Closable.new }
b_cnn_request = ShouldSleepingOp.new
wait_a = WaitFor.new
wait_b = WaitFor.new
spawn do
a_cnn = pool.checkout
b_cnn_request.wait_for_sleep
pool.release a_cnn
wait_a.check
end
spawn do
b_cnn_request.should_sleep do
pool.checkout
end
wait_b.check
end
wait_a.wait
wait_b.wait
end
it "should create new if max was not reached" do
block_called = 0
pool = DB::Pool.new(max_pool_size: 2, initial_pool_size: 1) { block_called += 1; Closable.new }
block_called.should eq 1
pool.checkout
block_called.should eq 1
pool.checkout
block_called.should eq 2
end
it "should reuse returned resources" do
all = [] of Closable
pool = DB::Pool.new(max_pool_size: 2, initial_pool_size: 1) { Closable.new.tap { |c| all << c } }
pool.checkout
b1 = pool.checkout
pool.release b1
b2 = pool.checkout
b1.should eq b2
all.size.should eq 2
end
it "should close available and total" do
all = [] of Closable
pool = DB::Pool.new(max_pool_size: 2, initial_pool_size: 1) { Closable.new.tap { |c| all << c } }
a = pool.checkout
b = pool.checkout
pool.release b
all.size.should eq 2
all[0].closed?.should be_false
all[1].closed?.should be_false
pool.close
all[0].closed?.should be_true
all[1].closed?.should be_true
end
it "should timeout" do
pool = DB::Pool.new(max_pool_size: 1, checkout_timeout: 0.1) { Closable.new }
pool.checkout
expect_raises DB::PoolTimeout do
pool.checkout
end
end
it "should be able to release after a timeout" do
pool = DB::Pool.new(max_pool_size: 1, checkout_timeout: 0.1) { Closable.new }
a = pool.checkout
pool.checkout rescue nil
pool.release a
end
it "should close if max idle amount is reached" do
all = [] of Closable
pool = DB::Pool.new(max_pool_size: 3, max_idle_pool_size: 1) { Closable.new.tap { |c| all << c } }
pool.checkout
pool.checkout
pool.checkout
all.size.should eq 3
all.any?(&.closed?).should be_false
pool.release all[0]
all.any?(&.closed?).should be_false
pool.release all[1]
all[0].closed?.should be_false
all[1].closed?.should be_true
all[2].closed?.should be_false
end
it "should create resource after max_pool was reached if idle forced some close up" do
all = [] of Closable
pool = DB::Pool.new(max_pool_size: 3, max_idle_pool_size: 1) { Closable.new.tap { |c| all << c } }
pool.checkout
pool.checkout
pool.checkout
pool.release all[0]
pool.release all[1]
pool.checkout
pool.checkout
all.size.should eq 4
end
end

View file

@ -2,14 +2,14 @@ require "./spec_helper"
describe DB::Statement do
it "should prepare statements" do
with_dummy do |db|
db.prepare("the query").should be_a(DB::Statement)
with_dummy_connection do |cnn|
cnn.prepare("the query").should be_a(DB::Statement)
end
end
it "should initialize positional params in query" do
with_dummy do |db|
stmt = db.prepare("the query").as(DummyDriver::DummyStatement)
with_dummy_connection do |cnn|
stmt = cnn.prepare("the query").as(DummyDriver::DummyStatement)
stmt.query "a", 1, nil
stmt.params[0].should eq("a")
stmt.params[1].should eq(1)
@ -18,8 +18,8 @@ describe DB::Statement do
end
it "should initialize positional params in query with array" do
with_dummy do |db|
stmt = db.prepare("the query").as(DummyDriver::DummyStatement)
with_dummy_connection do |cnn|
stmt = cnn.prepare("the query").as(DummyDriver::DummyStatement)
stmt.query ["a", 1, nil]
stmt.params[0].should eq("a")
stmt.params[1].should eq(1)
@ -28,8 +28,8 @@ describe DB::Statement do
end
it "should initialize positional params in exec" do
with_dummy do |db|
stmt = db.prepare("the query").as(DummyDriver::DummyStatement)
with_dummy_connection do |cnn|
stmt = cnn.prepare("the query").as(DummyDriver::DummyStatement)
stmt.exec "a", 1, nil
stmt.params[0].should eq("a")
stmt.params[1].should eq(1)
@ -38,8 +38,8 @@ describe DB::Statement do
end
it "should initialize positional params in exec with array" do
with_dummy do |db|
stmt = db.prepare("the query").as(DummyDriver::DummyStatement)
with_dummy_connection do |cnn|
stmt = cnn.prepare("the query").as(DummyDriver::DummyStatement)
stmt.exec ["a", 1, nil]
stmt.params[0].should eq("a")
stmt.params[1].should eq(1)
@ -48,8 +48,8 @@ describe DB::Statement do
end
it "should initialize positional params in scalar" do
with_dummy do |db|
stmt = db.prepare("the query").as(DummyDriver::DummyStatement)
with_dummy_connection do |cnn|
stmt = cnn.prepare("the query").as(DummyDriver::DummyStatement)
stmt.scalar "a", 1, nil
stmt.params[0].should eq("a")
stmt.params[1].should eq(1)
@ -58,8 +58,8 @@ describe DB::Statement do
end
it "query with block should not close statement" do
with_dummy do |db|
stmt = db.prepare "3,4 1,2"
with_dummy_connection do |cnn|
stmt = cnn.prepare "3,4 1,2"
stmt.query
stmt.closed?.should be_false
end
@ -67,16 +67,16 @@ describe DB::Statement do
it "closing connection should close statement" do
stmt = uninitialized DB::Statement
with_dummy do |db|
stmt = db.prepare "3,4 1,2"
with_dummy_connection do |cnn|
stmt = cnn.prepare "3,4 1,2"
stmt.query
end
stmt.closed?.should be_true
end
it "query with block should not close statement" do
with_dummy do |db|
stmt = db.prepare "3,4 1,2"
with_dummy_connection do |cnn|
stmt = cnn.prepare "3,4 1,2"
stmt.query do |rs|
end
stmt.closed?.should be_false
@ -84,8 +84,8 @@ describe DB::Statement do
end
it "query should not close statement" do
with_dummy do |db|
stmt = db.prepare "3,4 1,2"
with_dummy_connection do |cnn|
stmt = cnn.prepare "3,4 1,2"
stmt.query do |rs|
end
stmt.closed?.should be_false
@ -93,28 +93,28 @@ describe DB::Statement do
end
it "scalar should not close statement" do
with_dummy do |db|
stmt = db.prepare "3,4 1,2"
with_dummy_connection do |cnn|
stmt = cnn.prepare "3,4 1,2"
stmt.scalar
stmt.closed?.should be_false
end
end
it "exec should not close statement" do
with_dummy do |db|
stmt = db.prepare "3,4 1,2"
with_dummy_connection do |cnn|
stmt = cnn.prepare "3,4 1,2"
stmt.exec
stmt.closed?.should be_false
end
end
it "connection should cache statements by query" do
with_dummy do |db|
rs = db.query "1, ?", 2
with_dummy_connection do |cnn|
rs = cnn.query "1, ?", 2
stmt = rs.statement
rs.close
rs = db.query "1, ?", 4
rs = cnn.query "1, ?", 4
rs.statement.should be(stmt)
end
end
@ -124,7 +124,8 @@ describe DB::Statement do
expect_raises do
db.exec "raise"
end
db.@in_pool.should be_true
DummyDriver::DummyConnection.connections.size.should eq(1)
db.pool.is_available?(DummyDriver::DummyConnection.connections.first)
end
end
end