Skip to content

Commit f13846b

Browse files
Refactor connection factory (#181)
* Start moving out URI from ConnectionContext Create connections with an initial context. Database will set itself as context after connection has been created * Migrate to simpler/decoupled factory in driver This allows more freedom on how the connection is created. It will no longer need to have an explicit reference to the connection URI * Introduce DB::Connection::Options Move prepared_statements out from ConnectionContext * Delegate options parsing to driver DRY parsing connection options for database * Introduce DB::Pool::Options * Rename Driver#connection_pool_options to pool_options * Drop driver getter from database * Drop uri getter from database * Add public Database#initialize method * Drop :nodoc: Database#initialize * Pass spec helper explicitly (to access methods within each spec) * Update docs * Update src/db/pool.cr Co-authored-by: Beta Ziliani <[email protected]> * Use ConnectionBuilder instead of procs * Fix inferred type when there is a single concrete connection type * Update src/db/driver.cr Co-authored-by: Beta Ziliani <[email protected]> --------- Co-authored-by: Beta Ziliani <[email protected]>
1 parent 65b926c commit f13846b

13 files changed

+178
-86
lines changed

spec/custom_drivers_types_spec.cr

+26-6
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@ class FooValue
3636
end
3737

3838
class FooDriver < DB::Driver
39+
class FooConnectionBuilder < DB::ConnectionBuilder
40+
def initialize(@options : DB::Connection::Options)
41+
end
42+
43+
def build : DB::Connection
44+
FooConnection.new(@options)
45+
end
46+
end
47+
3948
alias Any = DB::Any | FooValue
4049
@@row = [] of Any
4150

@@ -47,8 +56,9 @@ class FooDriver < DB::Driver
4756
@@row
4857
end
4958

50-
def build_connection(context : DB::ConnectionContext) : DB::Connection
51-
FooConnection.new(context)
59+
def connection_builder(uri : URI) : DB::ConnectionBuilder
60+
params = HTTP::Params.parse(uri.query || "")
61+
FooConnectionBuilder.new(connection_options(params))
5262
end
5363

5464
class FooConnection < DB::Connection
@@ -99,6 +109,15 @@ class BarValue
99109
end
100110

101111
class BarDriver < DB::Driver
112+
class BarConnectionBuilder < DB::ConnectionBuilder
113+
def initialize(@options : DB::Connection::Options)
114+
end
115+
116+
def build : DB::Connection
117+
BarConnection.new(@options)
118+
end
119+
end
120+
102121
alias Any = DB::Any | BarValue
103122
@@row = [] of Any
104123

@@ -110,8 +129,9 @@ class BarDriver < DB::Driver
110129
@@row
111130
end
112131

113-
def build_connection(context : DB::ConnectionContext) : DB::Connection
114-
BarConnection.new(context)
132+
def connection_builder(uri : URI) : DB::ConnectionBuilder
133+
params = HTTP::Params.parse(uri.query || "")
134+
BarConnectionBuilder.new(connection_options(params))
115135
end
116136

117137
class BarConnection < DB::Connection
@@ -156,8 +176,8 @@ DB.register_driver "bar", BarDriver
156176

157177
describe DB do
158178
it "should be able to register multiple drivers" do
159-
DB.open("foo://host").driver.should be_a(FooDriver)
160-
DB.open("bar://host").driver.should be_a(BarDriver)
179+
DB.open("foo://host").checkout.should be_a(FooDriver::FooConnection)
180+
DB.open("bar://host").checkout.should be_a(BarDriver::BarConnection)
161181
end
162182

163183
it "Foo and Bar drivers should return fake_row" do

spec/db_spec.cr

+2-5
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,9 @@ describe DB do
99
DB.driver_class("dummy").should eq(DummyDriver)
1010
end
1111

12-
it "should instantiate driver with connection uri" do
12+
it "should create dummy connection" do
1313
db = DB.open "dummy://localhost:1027"
14-
db.driver.should be_a(DummyDriver)
15-
db.uri.scheme.should eq("dummy")
16-
db.uri.host.should eq("localhost")
17-
db.uri.port.should eq(1027)
14+
db.checkout.should be_a(DummyDriver::DummyConnection)
1815
end
1916

2017
it "should create a connection and close it" do

spec/dummy_driver.cr

+14-4
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,23 @@ require "spec"
22
require "../src/db"
33

44
class DummyDriver < DB::Driver
5-
def build_connection(context : DB::ConnectionContext) : DB::Connection
6-
DummyConnection.new(context)
5+
class DummyConnectionBuilder < DB::ConnectionBuilder
6+
def initialize(@options : DB::Connection::Options)
7+
end
8+
9+
def build : DB::Connection
10+
DummyConnection.new(@options)
11+
end
12+
end
13+
14+
def connection_builder(uri : URI) : DB::ConnectionBuilder
15+
params = HTTP::Params.parse(uri.query || "")
16+
DummyConnectionBuilder.new(connection_options(params))
717
end
818

919
class DummyConnection < DB::Connection
10-
def initialize(context)
11-
super(context)
20+
def initialize(options : DB::Connection::Options)
21+
super(options)
1222
@connected = true
1323
@@connections ||= [] of DummyConnection
1424
@@connections.not_nil! << self

spec/http_client_pool_spec.cr

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ describe DB::Pool do
2222
expected_per_connection = 5
2323
requests = fixed_pool_size * expected_per_connection
2424

25-
pool = DB::Pool.new(
25+
pool = DB::Pool.new(DB::Pool::Options.new(
2626
initial_pool_size: fixed_pool_size,
2727
max_pool_size: fixed_pool_size,
28-
max_idle_pool_size: fixed_pool_size) {
28+
max_idle_pool_size: fixed_pool_size)) {
2929
HTTP::Client.new(URI.parse("http://127.0.0.1:#{address.port}/"))
3030
}
3131

spec/pool_spec.cr

+18-14
Original file line numberDiff line numberDiff line change
@@ -57,34 +57,38 @@ class Closable
5757
end
5858
end
5959

60+
private def create_pool(**options, &factory : -> T) forall T
61+
DB::Pool.new(DB::Pool::Options.new(**options), &factory)
62+
end
63+
6064
describe DB::Pool do
6165
it "should use proc to create objects" do
6266
block_called = 0
63-
pool = DB::Pool.new(initial_pool_size: 3) { block_called += 1; Closable.new }
67+
pool = create_pool(initial_pool_size: 3) { block_called += 1; Closable.new }
6468
block_called.should eq(3)
6569
end
6670

6771
it "should get resource" do
68-
pool = DB::Pool.new { Closable.new }
72+
pool = create_pool { Closable.new }
6973
resource = pool.checkout
7074
resource.should be_a Closable
7175
resource.before_checkout_called.should be_true
7276
end
7377

7478
it "should be available if not checkedout" do
7579
resource = uninitialized Closable
76-
pool = DB::Pool.new(initial_pool_size: 1) { resource = Closable.new }
80+
pool = create_pool(initial_pool_size: 1) { resource = Closable.new }
7781
pool.is_available?(resource).should be_true
7882
end
7983

8084
it "should not be available if checkedout" do
81-
pool = DB::Pool.new { Closable.new }
85+
pool = create_pool { Closable.new }
8286
resource = pool.checkout
8387
pool.is_available?(resource).should be_false
8488
end
8589

8690
it "should be available if returned" do
87-
pool = DB::Pool.new { Closable.new }
91+
pool = create_pool { Closable.new }
8892
resource = pool.checkout
8993
resource.after_release_called.should be_false
9094
pool.release resource
@@ -93,7 +97,7 @@ describe DB::Pool do
9397
end
9498

9599
it "should wait for available resource" do
96-
pool = DB::Pool.new(max_pool_size: 1, initial_pool_size: 1) { Closable.new }
100+
pool = create_pool(max_pool_size: 1, initial_pool_size: 1) { Closable.new }
97101

98102
b_cnn_request = ShouldSleepingOp.new
99103
wait_a = WaitFor.new
@@ -121,7 +125,7 @@ describe DB::Pool do
121125

122126
it "should create new if max was not reached" do
123127
block_called = 0
124-
pool = DB::Pool.new(max_pool_size: 2, initial_pool_size: 1) { block_called += 1; Closable.new }
128+
pool = create_pool(max_pool_size: 2, initial_pool_size: 1) { block_called += 1; Closable.new }
125129
block_called.should eq 1
126130
pool.checkout
127131
block_called.should eq 1
@@ -131,7 +135,7 @@ describe DB::Pool do
131135

132136
it "should reuse returned resources" do
133137
all = [] of Closable
134-
pool = DB::Pool.new(max_pool_size: 2, initial_pool_size: 1) { Closable.new.tap { |c| all << c } }
138+
pool = create_pool(max_pool_size: 2, initial_pool_size: 1) { Closable.new.tap { |c| all << c } }
135139
pool.checkout
136140
b1 = pool.checkout
137141
pool.release b1
@@ -143,7 +147,7 @@ describe DB::Pool do
143147

144148
it "should close available and total" do
145149
all = [] of Closable
146-
pool = DB::Pool.new(max_pool_size: 2, initial_pool_size: 1) { Closable.new.tap { |c| all << c } }
150+
pool = create_pool(max_pool_size: 2, initial_pool_size: 1) { Closable.new.tap { |c| all << c } }
147151
a = pool.checkout
148152
b = pool.checkout
149153
pool.release b
@@ -157,23 +161,23 @@ describe DB::Pool do
157161
end
158162

159163
it "should timeout" do
160-
pool = DB::Pool.new(max_pool_size: 1, checkout_timeout: 0.1) { Closable.new }
164+
pool = create_pool(max_pool_size: 1, checkout_timeout: 0.1) { Closable.new }
161165
pool.checkout
162166
expect_raises DB::PoolTimeout do
163167
pool.checkout
164168
end
165169
end
166170

167171
it "should be able to release after a timeout" do
168-
pool = DB::Pool.new(max_pool_size: 1, checkout_timeout: 0.1) { Closable.new }
172+
pool = create_pool(max_pool_size: 1, checkout_timeout: 0.1) { Closable.new }
169173
a = pool.checkout
170174
pool.checkout rescue nil
171175
pool.release a
172176
end
173177

174178
it "should close if max idle amount is reached" do
175179
all = [] of Closable
176-
pool = DB::Pool.new(max_pool_size: 3, max_idle_pool_size: 1) { Closable.new.tap { |c| all << c } }
180+
pool = create_pool(max_pool_size: 3, max_idle_pool_size: 1) { Closable.new.tap { |c| all << c } }
177181
pool.checkout
178182
pool.checkout
179183
pool.checkout
@@ -191,7 +195,7 @@ describe DB::Pool do
191195
end
192196

193197
it "should not return closed resources to the pool" do
194-
pool = DB::Pool.new(max_pool_size: 1, max_idle_pool_size: 1) { Closable.new }
198+
pool = create_pool(max_pool_size: 1, max_idle_pool_size: 1) { Closable.new }
195199

196200
# pool size 1 should be reusing the one resource
197201
resource1 = pool.checkout
@@ -209,7 +213,7 @@ describe DB::Pool do
209213

210214
it "should create resource after max_pool was reached if idle forced some close up" do
211215
all = [] of Closable
212-
pool = DB::Pool.new(max_pool_size: 3, max_idle_pool_size: 1) { Closable.new.tap { |c| all << c } }
216+
pool = create_pool(max_pool_size: 3, max_idle_pool_size: 1) { Closable.new.tap { |c| all << c } }
213217
pool.checkout
214218
pool.checkout
215219
pool.checkout

src/db.cr

+9-2
Original file line numberDiff line numberDiff line change
@@ -152,15 +152,21 @@ module DB
152152
end
153153

154154
private def self.build_database(uri : URI)
155-
Database.new(build_driver(uri), uri)
155+
driver = build_driver(uri)
156+
params = HTTP::Params.parse(uri.query || "")
157+
connection_options = driver.connection_options(params)
158+
pool_options = driver.pool_options(params)
159+
builder = driver.connection_builder(uri)
160+
factory = ->{ builder.build }
161+
Database.new(connection_options, pool_options, &factory)
156162
end
157163

158164
private def self.build_connection(connection_string : String)
159165
build_connection(URI.parse(connection_string))
160166
end
161167

162168
private def self.build_connection(uri : URI)
163-
build_driver(uri).build_connection(SingleConnectionContext.new(uri)).as(Connection)
169+
build_driver(uri).connection_builder(uri).build
164170
end
165171

166172
private def self.build_driver(uri : URI)
@@ -188,6 +194,7 @@ require "./db/enumerable_concat"
188194
require "./db/query_methods"
189195
require "./db/session_methods"
190196
require "./db/disposable"
197+
require "./db/connection_builder"
191198
require "./db/driver"
192199
require "./db/statement"
193200
require "./db/begin_transaction"

src/db/connection.cr

+18-6
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,28 @@ module DB
2323
include SessionMethods(Connection, Statement)
2424
include BeginTransaction
2525

26+
record Options,
27+
# Return whether the statements should be prepared by default
28+
prepared_statements : Bool = true do
29+
def self.from_http_params(params : HTTP::Params, default = Options.new)
30+
Options.new(
31+
prepared_statements: DB.fetch_bool(params, "prepared_statements", default.prepared_statements)
32+
)
33+
end
34+
end
35+
2636
# :nodoc:
27-
getter context
37+
property context : ConnectionContext = SingleConnectionContext.default
2838
@statements_cache = StringKeyCache(Statement).new
2939
@transaction = false
30-
getter? prepared_statements : Bool
3140
# :nodoc:
3241
property auto_release : Bool = true
3342

34-
def initialize(@context : ConnectionContext)
35-
@prepared_statements = @context.prepared_statements?
43+
def initialize(@options : Options)
44+
end
45+
46+
def prepared_statements? : Bool
47+
@options.prepared_statements
3648
end
3749

3850
# :nodoc:
@@ -59,7 +71,7 @@ module DB
5971
protected def do_close
6072
@statements_cache.each_value &.close
6173
@statements_cache.clear
62-
@context.discard self
74+
context.discard self
6375
end
6476

6577
# :nodoc:
@@ -75,7 +87,7 @@ module DB
7587
# managed by the database. Should be used
7688
# only if the connection was obtained by `Database#checkout`.
7789
def release
78-
@context.release(self)
90+
context.release(self)
7991
end
8092

8193
# :nodoc:

src/db/connection_builder.cr

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module DB
2+
# A connection factory with a specific configuration.
3+
#
4+
# See `Driver#connection_builder`.
5+
abstract class ConnectionBuilder
6+
abstract def build : Connection
7+
end
8+
end

src/db/connection_context.cr

+1-13
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
11
module DB
22
module ConnectionContext
3-
# Returns the uri with the connection settings to the database
4-
abstract def uri : URI
5-
6-
# Return whether the statements should be prepared by default
7-
abstract def prepared_statements? : Bool
8-
93
# Indicates that the *connection* was permanently closed
104
# and should not be used in the future.
115
abstract def discard(connection : Connection)
@@ -19,13 +13,7 @@ module DB
1913
class SingleConnectionContext
2014
include ConnectionContext
2115

22-
getter uri : URI
23-
getter? prepared_statements : Bool
24-
25-
def initialize(@uri : URI)
26-
params = HTTP::Params.parse(uri.query || "")
27-
@prepared_statements = DB.fetch_bool(params, "prepared_statements", true)
28-
end
16+
class_getter default : SingleConnectionContext = SingleConnectionContext.new
2917

3018
def discard(connection : Connection)
3119
end

0 commit comments

Comments
 (0)