data_objects 0.2.0 → 0.9.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README +3 -3
- data/Rakefile +42 -22
- data/TODO +0 -5
- data/lib/data_objects.rb +35 -337
- data/lib/data_objects/command.rb +30 -0
- data/lib/data_objects/connection.rb +88 -0
- data/lib/data_objects/field.rb +19 -0
- data/lib/data_objects/logger.rb +233 -0
- data/lib/data_objects/quoting.rb +98 -0
- data/lib/data_objects/reader.rb +22 -0
- data/lib/data_objects/result.rb +13 -0
- data/lib/data_objects/support/pooling.rb +236 -0
- data/lib/data_objects/transaction.rb +42 -0
- data/spec/command_spec.rb +37 -0
- data/spec/connection_spec.rb +83 -0
- data/spec/dataobjects_spec.rb +1 -0
- data/spec/do_mock.rb +31 -0
- data/spec/reader_spec.rb +18 -0
- data/spec/result_spec.rb +23 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/support/pooling_spec.rb +374 -0
- data/spec/transaction_spec.rb +39 -0
- metadata +80 -36
@@ -0,0 +1,22 @@
|
|
1
|
+
module DataObjects
|
2
|
+
class Reader
|
3
|
+
|
4
|
+
def fields
|
5
|
+
raise NotImplementedError.new
|
6
|
+
end
|
7
|
+
|
8
|
+
def values
|
9
|
+
raise NotImplementedError.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def close
|
13
|
+
raise NotImplementedError.new
|
14
|
+
end
|
15
|
+
|
16
|
+
# Moves the cursor forward.
|
17
|
+
def next!
|
18
|
+
raise NotImplementedError.new
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module DataObjects
|
2
|
+
class Result
|
3
|
+
attr_accessor :insert_id, :affected_rows
|
4
|
+
|
5
|
+
def initialize(command, affected_rows, insert_id = nil)
|
6
|
+
@command, @affected_rows, @insert_id = command, affected_rows, insert_id
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_i
|
10
|
+
@affected_rows
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,236 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
class Object
|
4
|
+
# ==== Notes
|
5
|
+
# Provides pooling support to class it got included in.
|
6
|
+
#
|
7
|
+
# Pooling of objects is a faster way of aquiring instances
|
8
|
+
# of objects compared to regular allocation and initialization
|
9
|
+
# because instances are keeped in memory reused.
|
10
|
+
#
|
11
|
+
# Classes that include Pooling module have re-defined new
|
12
|
+
# method that returns instances aquired from pool.
|
13
|
+
#
|
14
|
+
# Term resource is used for any type of poolable objects
|
15
|
+
# and should NOT be thought as DataMapper Resource or
|
16
|
+
# ActiveResource resource and such.
|
17
|
+
#
|
18
|
+
# In Data Objects connections are pooled so that it is
|
19
|
+
# unnecessary to allocate and initialize connection object
|
20
|
+
# each time connection is needed, like per request in a
|
21
|
+
# web application.
|
22
|
+
#
|
23
|
+
# Pool obviously has to be thread safe because state of
|
24
|
+
# object is reset when it is released.
|
25
|
+
module Pooling
|
26
|
+
def self.included(base)
|
27
|
+
base.send(:extend, ClassMethods)
|
28
|
+
end
|
29
|
+
|
30
|
+
module ClassMethods
|
31
|
+
# ==== Notes
|
32
|
+
# Initializes the pool and returns it.
|
33
|
+
#
|
34
|
+
# ==== Parameters
|
35
|
+
# size_limit<Fixnum>:: maximum size of the pool.
|
36
|
+
#
|
37
|
+
# ==== Returns
|
38
|
+
# <ResourcePool>:: initialized pool
|
39
|
+
def initialize_pool(size_limit, options = {})
|
40
|
+
@__pool.flush! if @__pool
|
41
|
+
|
42
|
+
@__pool = ResourcePool.new(size_limit, self, options)
|
43
|
+
end
|
44
|
+
|
45
|
+
# ==== Notes
|
46
|
+
# Instances of poolable resource are aquired from
|
47
|
+
# pool. This quires a new instance from pool and
|
48
|
+
# returns it.
|
49
|
+
#
|
50
|
+
# ==== Returns
|
51
|
+
# Resource instance aquired from the pool.
|
52
|
+
#
|
53
|
+
# ==== Raises
|
54
|
+
# ArgumentError:: when pool is exhausted and no instance
|
55
|
+
# can be aquired.
|
56
|
+
def new
|
57
|
+
pool.aquire
|
58
|
+
end
|
59
|
+
|
60
|
+
# ==== Notes
|
61
|
+
# Returns pool for this resource class.
|
62
|
+
# Initialization is done when necessary.
|
63
|
+
# Default size limit of the pool is 10.
|
64
|
+
#
|
65
|
+
# ==== Returns
|
66
|
+
# <Object::Pooling::ResourcePool>:: pool for this resource class.
|
67
|
+
def pool
|
68
|
+
@__pool ||= ResourcePool.new(10, self)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# ==== Notes
|
73
|
+
# Pool
|
74
|
+
#
|
75
|
+
class ResourcePool
|
76
|
+
attr_reader :size_limit, :class_of_resources, :expiration_period
|
77
|
+
|
78
|
+
# ==== Notes
|
79
|
+
# Initializes resource pool.
|
80
|
+
#
|
81
|
+
# ==== Parameters
|
82
|
+
# size_limit<Fixnum>:: maximum number of resources in the pool.
|
83
|
+
# class_of_resources<Class>:: class of resource.
|
84
|
+
#
|
85
|
+
# ==== Raises
|
86
|
+
# ArgumentError:: when class of resource does not implement
|
87
|
+
# dispose instance method or is not a Class.
|
88
|
+
def initialize(size_limit, class_of_resources, options)
|
89
|
+
raise ArgumentError.new("Expected class of resources to be instance of Class, got: #{class_of_resources.class}") unless class_of_resources.is_a?(Class)
|
90
|
+
raise ArgumentError.new("Class #{class_of_resources} must implement dispose instance method to be poolable.") unless class_of_resources.instance_methods.include?("dispose")
|
91
|
+
|
92
|
+
@size_limit = size_limit
|
93
|
+
@class_of_resources = class_of_resources
|
94
|
+
|
95
|
+
@reserved = Set.new
|
96
|
+
@available = []
|
97
|
+
@lock = Mutex.new
|
98
|
+
|
99
|
+
initialization_args = options.delete(:initialization_args) || []
|
100
|
+
|
101
|
+
@expiration_period = options.delete(:expiration_period) || 60
|
102
|
+
@initialization_args = [*initialization_args]
|
103
|
+
|
104
|
+
@pool_expiration_thread = Thread.new do
|
105
|
+
while true
|
106
|
+
release_outdated
|
107
|
+
|
108
|
+
sleep (@expiration_period + 1)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# ==== Notes
|
114
|
+
# Current size of pool: number of already reserved
|
115
|
+
# resources.
|
116
|
+
def size
|
117
|
+
@reserved.size
|
118
|
+
end
|
119
|
+
|
120
|
+
# ==== Notes
|
121
|
+
# Indicates if pool has resources to aquire.
|
122
|
+
#
|
123
|
+
# ==== Returns
|
124
|
+
# <Boolean>:: true if pool has resources can be aquired,
|
125
|
+
# false otherwise.
|
126
|
+
def available?
|
127
|
+
@reserved.size < size_limit
|
128
|
+
end
|
129
|
+
|
130
|
+
# ==== Notes
|
131
|
+
# Aquires last used available resource and returns it.
|
132
|
+
# If no resources available, current implementation
|
133
|
+
# throws an exception.
|
134
|
+
def aquire
|
135
|
+
@lock.synchronize do
|
136
|
+
if available?
|
137
|
+
instance = prepair_available_resource
|
138
|
+
@reserved << instance
|
139
|
+
|
140
|
+
instance
|
141
|
+
else
|
142
|
+
raise RuntimeError
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# ==== Notes
|
148
|
+
# Releases previously aquired instance.
|
149
|
+
#
|
150
|
+
# ==== Parameters
|
151
|
+
# instance <Anything>:: previosly aquired instance.
|
152
|
+
#
|
153
|
+
# ==== Raises
|
154
|
+
# RuntimeError:: when given not pooled instance.
|
155
|
+
def release(instance)
|
156
|
+
@lock.synchronize do
|
157
|
+
if @reserved.include?(instance)
|
158
|
+
@reserved.delete(instance)
|
159
|
+
instance.dispose
|
160
|
+
@available << instance
|
161
|
+
else
|
162
|
+
raise RuntimeError
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# ==== Notes
|
168
|
+
# Releases all objects in the pool.
|
169
|
+
#
|
170
|
+
# ==== Returns
|
171
|
+
# nil
|
172
|
+
def flush!
|
173
|
+
@reserved.each do |instance|
|
174
|
+
self.release(instance)
|
175
|
+
end
|
176
|
+
|
177
|
+
nil
|
178
|
+
end
|
179
|
+
|
180
|
+
# ==== Notes
|
181
|
+
# Check if instance has been aquired from the pool.
|
182
|
+
#
|
183
|
+
# ==== Returns
|
184
|
+
# <Boolean>:: true if given resource instance has been aquired from pool,
|
185
|
+
# false otherwise.
|
186
|
+
def aquired?(instance)
|
187
|
+
@reserved.include?(instance)
|
188
|
+
end
|
189
|
+
|
190
|
+
# ==== Notes
|
191
|
+
# Releases instances that haven't been in use and
|
192
|
+
# hit the expiration period.
|
193
|
+
#
|
194
|
+
# ==== Returns
|
195
|
+
# nil
|
196
|
+
def release_outdated
|
197
|
+
@reserved.each do |instance|
|
198
|
+
release(instance) if time_to_release?(instance)
|
199
|
+
end
|
200
|
+
|
201
|
+
nil
|
202
|
+
end
|
203
|
+
|
204
|
+
# ==== Notes
|
205
|
+
# Checks if pooled resource instance is outdated and
|
206
|
+
# should be released.
|
207
|
+
#
|
208
|
+
# ==== Returns
|
209
|
+
# <Boolean>:: true if instance should be released, false otherwise.
|
210
|
+
def time_to_release?(instance)
|
211
|
+
(Time.now - instance.instance_variable_get("@__pool_aquire_timestamp")) > @expiration_period
|
212
|
+
end
|
213
|
+
|
214
|
+
protected
|
215
|
+
|
216
|
+
# ==== Notes
|
217
|
+
# Either allocates new resource,
|
218
|
+
# or takes last used available resource from
|
219
|
+
# the pool.
|
220
|
+
def prepair_available_resource
|
221
|
+
if @available.size > 0
|
222
|
+
res = @available.pop
|
223
|
+
res.instance_variable_set("@__pool_aquire_timestamp", Time.now)
|
224
|
+
|
225
|
+
res
|
226
|
+
else
|
227
|
+
res = @class_of_resources.allocate
|
228
|
+
res.send(:initialize, *@initialization_args)
|
229
|
+
res.instance_variable_set("@__pool_aquire_timestamp", Time.now)
|
230
|
+
|
231
|
+
res
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end # ResourcePool
|
235
|
+
end
|
236
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module DataObjects
|
2
|
+
|
3
|
+
class Transaction
|
4
|
+
|
5
|
+
HOST = "#{Socket::gethostbyname(Socket::gethostname)[0]}" rescue "localhost"
|
6
|
+
@@counter = 0
|
7
|
+
|
8
|
+
attr_reader :connection
|
9
|
+
attr_reader :id
|
10
|
+
|
11
|
+
def self.create_for_uri(uri)
|
12
|
+
uri = uri.is_a?(String) ? URI::parse(uri) : uri
|
13
|
+
DataObjects.const_get(uri.scheme.capitalize)::Transaction.new(uri)
|
14
|
+
end
|
15
|
+
|
16
|
+
#
|
17
|
+
# Creates a Transaction bound to the given connection
|
18
|
+
#
|
19
|
+
# ==== Parameters
|
20
|
+
# conn<DataObjects::Connection>:: The Connection to bind the new Transaction to
|
21
|
+
#
|
22
|
+
def initialize(uri)
|
23
|
+
@connection = DataObjects::Connection.new(uri)
|
24
|
+
@id = "#{HOST}:#{$$}:#{Time.now.to_f}:#{@@counter += 1}"
|
25
|
+
end
|
26
|
+
|
27
|
+
def close
|
28
|
+
@connection.close
|
29
|
+
end
|
30
|
+
|
31
|
+
[:begin, :commit, :rollback, :rollback_prepared, :prepare].each do |method_name|
|
32
|
+
|
33
|
+
eval <<EOF
|
34
|
+
def #{method_name}
|
35
|
+
raise NotImplementedError
|
36
|
+
end
|
37
|
+
EOF
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'spec_helper'))
|
2
|
+
|
3
|
+
describe DataObjects::Command do
|
4
|
+
before do
|
5
|
+
@connection = DataObjects::Connection.new('mock://localhost')
|
6
|
+
@command = DataObjects::Command.new(@connection, 'SQL STRING')
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should assign the connection object to @connection" do
|
10
|
+
@command.instance_variable_get("@connection").should == @connection
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should assign the sql text to @text" do
|
14
|
+
@command.instance_variable_get("@text").should == 'SQL STRING'
|
15
|
+
end
|
16
|
+
|
17
|
+
%w{connection execute_non_query execute_reader set_types to_s}.each do |meth|
|
18
|
+
it "should respond to ##{meth}" do
|
19
|
+
@command.should respond_to(meth.intern)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
%w{execute_non_query execute_reader set_types}.each do |meth|
|
24
|
+
it "should raise NotImplementedError on ##{meth}" do
|
25
|
+
lambda { @command.send(meth.intern, nil) }.should raise_error(NotImplementedError)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should make the connection object available in #connection" do
|
30
|
+
@command.connection.should == @command.instance_variable_get("@connection")
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should make the SQL text available in #to_s" do
|
34
|
+
@command.to_s.should == @command.instance_variable_get("@text")
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'spec_helper'))
|
2
|
+
|
3
|
+
describe DataObjects::Connection do
|
4
|
+
before do
|
5
|
+
@connection = DataObjects::Connection.new('mock://localhost')
|
6
|
+
end
|
7
|
+
|
8
|
+
%w{dispose create_command}.each do |meth|
|
9
|
+
it "should respond to ##{meth}" do
|
10
|
+
@connection.should respond_to(meth.intern)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should have #to_s that returns the connection uri string" do
|
15
|
+
@connection.to_s.should == 'mock://localhost'
|
16
|
+
end
|
17
|
+
|
18
|
+
describe "getting inherited" do
|
19
|
+
# HACK: Connections needs to exist under the DataObjects namespace?
|
20
|
+
module DataObjects
|
21
|
+
class MyConnection < DataObjects::Connection; end
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should set the @connection_lock ivar to a Mutex" do
|
25
|
+
DataObjects::MyConnection.instance_variable_get("@connection_lock").should_not be_nil
|
26
|
+
DataObjects::MyConnection.instance_variable_get("@connection_lock").should be_kind_of(Mutex)
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should set the @available_connections ivar to a Hash" do
|
30
|
+
DataObjects::MyConnection.instance_variable_get("@available_connections").should_not be_nil
|
31
|
+
DataObjects::MyConnection.instance_variable_get("@available_connections").should be_kind_of(Hash)
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should set the @reserved_connections ivar to a Set" do
|
35
|
+
DataObjects::MyConnection.instance_variable_get("@reserved_connections").should_not be_nil
|
36
|
+
DataObjects::MyConnection.instance_variable_get("@reserved_connections").should be_kind_of(Set)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "initialization" do
|
41
|
+
it "should accept a regular connection uri as a String" do
|
42
|
+
c = DataObjects::Connection.new('mock://localhost/database')
|
43
|
+
# relying on the fact that mock connection sets @uri
|
44
|
+
uri = c.instance_variable_get("@uri")
|
45
|
+
|
46
|
+
uri.should be_kind_of(Addressable::URI)
|
47
|
+
uri.scheme.should == 'mock'
|
48
|
+
uri.host.should == 'localhost'
|
49
|
+
uri.path.should == '/database'
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should accept a conneciton uri as a Addressable::URI" do
|
53
|
+
c = DataObjects::Connection.new(Addressable::URI::parse('mock://localhost/database'))
|
54
|
+
# relying on the fact that mock connection sets @uri
|
55
|
+
uri = c.instance_variable_get("@uri")
|
56
|
+
|
57
|
+
uri.should be_kind_of(Addressable::URI)
|
58
|
+
uri.to_s.should == 'mock://localhost/database'
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should determine which DataObject adapter from the uri scheme" do
|
62
|
+
DataObjects::Mock::Connection.should_receive(:__new)
|
63
|
+
DataObjects::Connection.new('mock://localhost/database')
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should determine which DataObject adapter from a JDBC URL scheme" do
|
67
|
+
DataObjects::Mock::Connection.should_receive(:__new)
|
68
|
+
DataObjects::Connection.new('jdbc:mock://localhost/database')
|
69
|
+
end
|
70
|
+
|
71
|
+
it "should aquire a connection" do
|
72
|
+
uri = Addressable::URI.parse('mock://localhost/database')
|
73
|
+
DataObjects::Mock::Connection.should_receive(:__new).with(uri)
|
74
|
+
|
75
|
+
DataObjects::Connection.new(uri)
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should return the Connection specified by the scheme" do
|
79
|
+
c = DataObjects::Connection.new(Addressable::URI.parse('mock://localhost/database'))
|
80
|
+
c.should be_kind_of(DataObjects::Mock::Connection)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'spec_helper'))
|
data/spec/do_mock.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
module DataObjects
|
2
|
+
|
3
|
+
module Mock
|
4
|
+
class Connection < DataObjects::Connection
|
5
|
+
def initialize(uri)
|
6
|
+
@uri = uri
|
7
|
+
end
|
8
|
+
|
9
|
+
def dispose
|
10
|
+
nil
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class Command < DataObjects::Command
|
15
|
+
def execute_non_query(*args)
|
16
|
+
Result.new(self, 0, nil)
|
17
|
+
end
|
18
|
+
|
19
|
+
def execute_reader(*args)
|
20
|
+
Reader.new
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class Result < DataObjects::Result
|
25
|
+
end
|
26
|
+
|
27
|
+
class Reader < DataObjects::Reader
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|