data_objects 0.2.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|