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