make Database return PoolStatement

create StatementMethods for common interface between Statement and PoolStatment.
This commit is contained in:
Brian J. Cardiff 2016-08-29 16:56:34 -03:00
parent 6a0a450622
commit 75aa821f5f
6 changed files with 148 additions and 31 deletions

View file

@ -39,4 +39,12 @@ describe DB::Database do
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
end end

View file

@ -125,6 +125,7 @@ require "./db/database"
require "./db/driver" require "./db/driver"
require "./db/connection" require "./db/connection"
require "./db/statement" require "./db/statement"
require "./db/pool_statement"
require "./db/result_set" require "./db/result_set"
require "./db/error" require "./db/error"
require "./db/mapping" require "./db/mapping"

View file

@ -54,18 +54,14 @@ module DB
# :nodoc: # :nodoc:
def prepare(query) def prepare(query)
conn = get_from_pool # TODO query based cache for pool statement
begin # TODO clear PoolStatements when closing the DB
conn.prepare(query) PoolStatement.new self, query
rescue ex
return_to_pool(conn)
raise ex
end
end end
# :nodoc: # :nodoc:
def get_from_pool def checkout_some(candidates : Enumerable(Connection)) : {Connection, Bool}
@pool.checkout @pool.checkout_some candidates
end end
# :nodoc: # :nodoc:
@ -77,7 +73,7 @@ module DB
# the connection is returned to the pool after # the connection is returned to the pool after
# when the block ends # when the block ends
def using_connection def using_connection
connection = get_from_pool connection = @pool.checkout
begin begin
yield connection yield connection
ensure ensure

View file

@ -36,6 +36,25 @@ module DB
resource resource
end end
# ```
# selected, is_candidate = pool.checkout_some(candidates)
# ```
# `selected` be a resource from the `candidates` list and `is_candidate` == `true`
# or `selected` will be a new resource adn `is_candidate` == `false`
def checkout_some(candidates : Enumerable(T)) : {T, Bool}
# TODO honor candidates while waiting for availables
# this will allow us to remove `candidates.includes?(resource)`
candidates.each do |resource|
if is_available?(resource)
@available.delete resource
return {resource, true}
end
end
resource = checkout
{resource, candidates.includes?(resource)}
end
def release(resource : T) : Nil def release(resource : T) : Nil
if can_increase_idle_pool if can_increase_idle_pool
@available << resource @available << resource

73
src/db/pool_statement.cr Normal file
View file

@ -0,0 +1,73 @@
module DB
# When a statement is to be executed in a DB that has a connection pool
# a statement from the DB needs to be able to represent a statement in any
# of the connections of the pool. Otherwise the user will need to deal with
# actual connections in some point.
class PoolStatement
include StatementMethods
@statements = {} of Connection => Statement
def initialize(@db : Database, @query : String)
# Prepares a statement on some connection
# otherwise the preparation is delayed until the first execution.
# After the first initialization the connection must be released
# it will be checked out when executing it.
get_statement.release_connection
# TODO use a round-robin selection in the pool so multiple sequentially
# initialized statements are assigned to different connections.
end
protected def do_close
# TODO close all statements on all connections.
# currently statements are closed when the connection is closed.
# WHAT-IF the connection is busy? Should each statement be able to
# deallocate itself when the connection is free.
@statements.clear
end
# See `QueryMethods#exec`
def exec : ExecResult
get_statement.exec
end
# See `QueryMethods#exec`
def exec(*args) : ExecResult
get_statement.exec(*args)
end
# See `QueryMethods#exec`
def exec(args : Array) : ExecResult
get_statement.exec(args)
end
# See `QueryMethods#query`
def query : ResultSet
get_statement.query
end
# See `QueryMethods#query`
def query(*args) : ResultSet
get_statement.query(*args)
end
# See `QueryMethods#query`
def query(args : Array) : ResultSet
get_statement.query(args)
end
# builds a statement over a real connection
# the conneciton and the stament is registered in `@statements`
private def get_statement : Statement
conn, existing = @db.checkout_some(@statements.keys)
if existing
@statements[conn]
else
stmt = conn.prepare @query
@statements[conn] = stmt
stmt
end
end
end
end

View file

@ -1,4 +1,44 @@
module DB module DB
# Common interface for connection based statements
# and for connection pool statements.
module StatementMethods
include Disposable
protected def do_close
end
# See `QueryMethods#scalar`
def scalar(*args)
query(*args) do |rs|
rs.each do
return rs.read
end
end
raise "no results"
end
# See `QueryMethods#query`
def query(*args)
rs = query(*args)
yield rs ensure rs.close
end
# See `QueryMethods#exec`
abstract def exec : ExecResult
# See `QueryMethods#exec`
abstract def exec(*args) : ExecResult
# See `QueryMethods#exec`
abstract def exec(args : Array) : ExecResult
# See `QueryMethods#query`
abstract def query : ResultSet
# See `QueryMethods#query`
abstract def query(*args) : ResultSet
# See `QueryMethods#query`
abstract def query(args : Array) : ResultSet
end
# Represents a prepared query in a `Connection`. # Represents a prepared query in a `Connection`.
# It should be created by `QueryMethods`. # It should be created by `QueryMethods`.
# #
@ -10,7 +50,7 @@ module DB
# 4. `#perform_exec` executes a query that is expected to return an `ExecResult` # 4. `#perform_exec` executes a query that is expected to return an `ExecResult`
# 6. `#do_close` is called to release the statement resources. # 6. `#do_close` is called to release the statement resources.
abstract class Statement abstract class Statement
include Disposable include StatementMethods
# :nodoc: # :nodoc:
getter connection getter connection
@ -18,9 +58,6 @@ module DB
def initialize(@connection : Connection) def initialize(@connection : Connection)
end end
protected def do_close
end
def release_connection def release_connection
@connection.database.return_to_pool(@connection) @connection.database.return_to_pool(@connection)
end end
@ -41,17 +78,6 @@ module DB
perform_exec_and_release(args) perform_exec_and_release(args)
end end
# See `QueryMethods#scalar`
def scalar(*args)
query(*args) do |rs|
rs.each do
return rs.read
end
end
raise "no results"
end
# See `QueryMethods#query` # See `QueryMethods#query`
def query def query
perform_query Tuple.new perform_query Tuple.new
@ -67,12 +93,6 @@ module DB
perform_query args perform_query args
end end
# See `QueryMethods#query`
def query(*args)
rs = query(*args)
yield rs ensure rs.close
end
private def perform_exec_and_release(args : Enumerable) : ExecResult private def perform_exec_and_release(args : Enumerable) : ExecResult
return perform_exec(args) return perform_exec(args)
ensure ensure