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.
@@ -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