pod4 1.0.1 → 1.0.3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 222b047955f5c90f80f9766f7d14b51322761dd9dce60d289c104b0f8becce35
4
- data.tar.gz: 34d692c2d9d1831e4d3b6f5525ab2bda44bb7351dd6f8041b1ebe66d41db6463
3
+ metadata.gz: 7f7db5348478c3d4dba39f248760d0f148f14f21866246e95ec3ea8c69505979
4
+ data.tar.gz: 7cbace08f7fb4151bfb7b9713bed4d5262d4d4bc7b4a0a87e79723e6c990fabf
5
5
  SHA512:
6
- metadata.gz: 92f70348d8ac572a9ca5a3a4dfb3fe7b1abf6f1fa0e6846a7bab3aa94f18cc35413706e663db56517fcf5b525a7e481ca464b66c990f892f0531c0b5aa39375f
7
- data.tar.gz: 6850576058884778e31e38c63bb02eec915baa63423168d523b7d0593d5fb9dfd97151ad32584c1fc3f6bcacd34119871033bf3f45b89cc555266cf75688de4c
6
+ metadata.gz: 9b0d0707ae21d1060cbc5df5fa0bee569c85a5dc6f9af3db288c4ba4025c047a1cbb3f3390535df81589b1143053bc29337a407b3f345a4bbcfa594ae7051e83
7
+ data.tar.gz: 983c421cdfca75e40b211d19f9a80ed63bbd14068a539b3861be446803231b361d323e34f91de1675ad1bbc528cd62e9f6dd9601d0b7e252d3985287e8ccbcc8
data/.hgtags CHANGED
@@ -33,3 +33,4 @@ e98232cdc6cbe61e2bf0513fcf578001a2931018 0.10.4
33
33
  daae29a48272b5c3029b6509354bd3c3e6bf339a 0.10.5
34
34
  48d204fe041672e6df5845ad552497df8db3029a 0.10.6
35
35
  82ec1031be4084a673f2f979f9bbf9950e21d71a 1.0.0
36
+ a6b22bc7314b21fb0959f77ec324911b631d786b 1.0.2
data/Gemfile CHANGED
@@ -11,6 +11,8 @@ group :development, :test do
11
11
  gem "rspec", "~> 3.10"
12
12
  gem 'pry'
13
13
  gem "pry-doc"
14
+ gem "base64"
15
+ gem "logger"
14
16
 
15
17
  # For testing
16
18
  gem "sequel", "~> 5.4"
data/README.md CHANGED
@@ -466,18 +466,26 @@ where I was going.
466
466
  In practice this means you need to get your DB connection details from somewhere, maybe create your
467
467
  Sequel DB object; and only then can you require your models.
468
468
 
469
- Leading on from this, wrinkle two: except when using Sequel (which behaves differently) each
470
- interface has its own connection to the database. This means that your application has (simplifying
471
- a bit here) one database connection for each model class. So if you have a Customer model and a
472
- Orders model, your application will hold two connections to the database. All customer enquiries
473
- share a single connection, and all order enquiries share a single connection.
469
+ As it stood, this meant that, except when using Sequel (which behaves differently) each
470
+ interface had its own connection to the database. This means that your application had (simplifying
471
+ a bit here) one database connection for each model class.
472
+
473
+ This scaled surprisingly well, but (again, excepting Sequel) was not thread safe; multiple threads,
474
+ if you had them, would share the same connection. Note that we are talking in the past tense for
475
+ this second wrinkle, because of:
474
476
 
475
- I'm finding that, generally, this scales about right. But if you have a lot of different models and a
476
- database that runs out of connections easily, it might be problematic.
477
477
 
478
478
  ### The Connection Object ###
479
479
 
480
- The solution to both of these wrinkles, if you need one, is to use a Pod4::Connection object:
480
+ The solution to both of these wrinkles is the Pod4::Connection object.
481
+
482
+ By default any interface other than SequelInterface will create a pool of connections and connect
483
+ to the database with one your thread is currently using. (Sequel uses it's own connection pool, so
484
+ will create one internally. It uses its own thread pool, of course, and we only use the one Sequel
485
+ DB object for the whole of Pod4, so it doesn't need any of that. That's why it uses
486
+ Pod4::Connection, instead.)
487
+
488
+ You can set this up manually, if you wish:
481
489
 
482
490
  #
483
491
  # init.rb -- bootup for my project
@@ -519,14 +527,6 @@ With TdsInterface and PgInterface you can pass the same connection to multiple m
519
527
  share it. These interfaces take a Pod4::ConnectionPool instead, but otherwise the code looks
520
528
  exactly the same as the above example.
521
529
 
522
- (Technical note: the ConnectionPool object will actually provide multiple connections, one per
523
- thread that uses it. This satisfies the requirement of the underlying libraries that connections
524
- are not shared between threads, and therefore ensures that Pod4 is more or less thread safe. You
525
- get this functionality automatically -- if you don't define a ConnectionPool, then the interface
526
- will create one internally. Sequel uses its own thread pool, of course, and we only use the one
527
- Sequel DB object for the whole of Pod4, so it doesn't need any of that. That's why it uses
528
- Pod4::Connection, instead.)
529
-
530
530
 
531
531
  BasicModel
532
532
  ----------
@@ -11,7 +11,7 @@ module Pod4
11
11
 
12
12
  class ConnectionPool < Connection
13
13
 
14
- PoolItem = Struct.new(:client, :thread_id)
14
+ PoolItem = Struct.new(:client, :thread_id, :stamp)
15
15
 
16
16
  class Pool
17
17
  def initialize
@@ -21,12 +21,16 @@ module Pod4
21
21
 
22
22
  def <<(cl)
23
23
  @mutex.synchronize do
24
- @items << PoolItem.new(cl, Thread.current.object_id)
24
+ @items << PoolItem.new(cl, Thread.current.object_id, Time.now)
25
25
  end
26
26
  end
27
27
 
28
+ def get(id)
29
+ @items.find{|x| x.thread_id == id }
30
+ end
31
+
28
32
  def get_current
29
- @items.find{|x| x.thread_id == Thread.current.object_id }
33
+ get(Thread.current.object_id)
30
34
  end
31
35
 
32
36
  def get_free
@@ -37,13 +41,19 @@ module Pod4
37
41
  end
38
42
  end
39
43
 
40
- def release
41
- pi = get_current
44
+ def release(id=nil)
45
+ pi = id.nil? ? get_current : get(id)
46
+ pi.thread_id = nil if pi
47
+ end
48
+
49
+ def release_oldest
50
+ pi = @items.sort{|a,b| a.stamp <=> b.stamp}.first
42
51
  pi.thread_id = nil if pi
43
52
  end
44
53
 
45
- def drop
46
- @items.delete_if{|x| x.thread_id == Thread.current.object_id }
54
+ def drop(id=nil)
55
+ id ||= Thread.current.object_id
56
+ @items.delete_if{|x| x.thread_id == id }
47
57
  end
48
58
 
49
59
  def size
@@ -84,6 +94,8 @@ module Pod4
84
94
  # Return the client we gave this thread before.
85
95
  # Failing that, assign a free one from the pool.
86
96
  # Failing that, ask the interface to give us a new client.
97
+ # Failing that, if we've set a timeout, wait for a client to be freed; if we have not, release
98
+ # the oldest client and use that.
87
99
  #
88
100
  # Note: The interface passes itself in case we want to call it back to get a new client; but
89
101
  # clients are assigned to a _thread_. Every interface in a given thread gets the same pool
@@ -106,9 +118,16 @@ module Pod4
106
118
  end
107
119
 
108
120
  if @max_clients && @pool.size >= @max_clients
109
- raise Pod4::PoolTimeout if @max_wait && (Time.now - time > @max_wait)
110
- sleep 1
111
- next
121
+ if @max_wait
122
+ raise Pod4::PoolTimeout if @max_wait && (Time.now - time > @max_wait)
123
+ Pod4.logger.warn(__FILE__){ "waiting for a free client..." }
124
+ sleep 1
125
+ next
126
+ else
127
+ Pod4.logger.debug(__FILE__){ "releasing oldest client" }
128
+ @pool.release_oldest
129
+ next
130
+ end
112
131
  end
113
132
 
114
133
  cl = interface.new_connection(@data_layer_options)
@@ -222,7 +222,7 @@ module Pod4
222
222
  return thing if thing.nil?
223
223
 
224
224
  # For all current cases, attempting to typecast a blank string should return nil
225
- return nil if thing =~ /\A\s*\Z/
225
+ return nil if thing.is_a?(String) && thing =~ /\A\s*\Z/
226
226
 
227
227
  # The order we try these in matters
228
228
  return tc_bigdecimal(thing) if type == BigDecimal
data/lib/pod4/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Pod4
2
- VERSION = '1.0.1'
2
+ VERSION = '1.0.3'
3
3
  end
data/pod4.gemspec CHANGED
@@ -3,12 +3,13 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
3
  require 'pod4/version'
4
4
 
5
5
  Gem::Specification.new do |spec|
6
- spec.name = "pod4"
7
- spec.version = Pod4::VERSION
8
- spec.authors = ["Andy Jones"]
9
- spec.email = ["andy.jones@twosticksconsulting.co.uk"]
10
- spec.summary = %q|Totally not an ORM|
11
- spec.description = <<-DESC.gsub(/^\s+/, "")
6
+ spec.name = "pod4"
7
+ spec.version = Pod4::VERSION
8
+ spec.required_ruby_version = ">= 2.7.4"
9
+ spec.authors = ["Andy Jones"]
10
+ spec.email = ["andy.jones@twosticksconsulting.co.uk"]
11
+ spec.summary = %q|Totally not an ORM|
12
+ spec.description = <<-DESC.gsub(/^\s+/, "")
12
13
  Provides a simple, common framework to talk to a bunch of data sources,
13
14
  using model classes which consist of a bare minimum of DSL plus vanilla Ruby
14
15
  inheritance.
@@ -127,9 +127,9 @@ describe Pod4::ConnectionPool do
127
127
  end
128
128
  end
129
129
 
130
- context "when we reach the maximum" do
130
+ context "when we reach the maximum and there is a max_wait" do
131
131
  before(:each) do
132
- @connection = ConnectionPool.new(interface: ifce_class, max_clients: 1)
132
+ @connection = ConnectionPool.new(interface: ifce_class, max_clients: 1, max_wait: 10)
133
133
  @connection.data_layer_options = "meh"
134
134
  @interface = ifce_class.new
135
135
  @interface.set_conn "boz"
@@ -194,6 +194,42 @@ describe Pod4::ConnectionPool do
194
194
  end
195
195
  end # of when we reach the maximum, max_wait is set and the time is up
196
196
 
197
+ context "when we reach the maximum and no max_wait is set" do
198
+ before(:each) do
199
+ @connection = ConnectionPool.new(interface: ifce_class, max_clients: 2)
200
+ @connection.data_layer_options = "meh"
201
+ @interface = ifce_class.new
202
+ @interface.set_conn "foo"
203
+
204
+ # assign our 2 clients in the pool to a different thread
205
+ @threads = []
206
+ 2.times do
207
+ @threads << Thread.new { @connection.client(@interface); Thread.stop }
208
+ end
209
+ sleep 0.1 until @threads.all?{|t| t.stop? }
210
+
211
+ expect( @connection._pool.size ).to eq 2
212
+ @threads.each do |t|
213
+ expect( t ).not_to be_nil
214
+ expect( t ).not_to eq Thread.current.object_id
215
+ end
216
+ end
217
+
218
+ after(:each) { @threads.each{|t| t.kill} }
219
+
220
+ it "releases the oldest connection" do
221
+ oldest = @connection._pool.sort_by{|x| x.stamp}.first
222
+ expect( oldest.thread_id ).not_to eq Thread.current.object_id
223
+
224
+ @connection.client(@interface)
225
+ expect( @connection._pool.size ).to eq 2
226
+
227
+ conn = @connection._pool.find{|x| x.stamp == oldest.stamp }
228
+ expect( conn.thread_id ).to eq Thread.current.object_id
229
+ end
230
+
231
+ end # of when we reach the maximum and no max_wait is set
232
+
197
233
  end # of when max_clients != nil, there is no client for this thread and none free
198
234
 
199
235
  end # of #client
@@ -92,10 +92,18 @@ describe "SequelInterface (Pg)" do
92
92
  d
93
93
  end
94
94
 
95
- let(:db_url) { "postgres://pod4test:pod4test@centos7andy/pod4_test?search_path=public" }
95
+ #let(:db_url) { "postgres://pod4_test:#why#ring#@postgresql01/pod4_test?search_path=public" }
96
+
97
+ let(:db_hash) do
98
+ { host: 'postgresql01',
99
+ user: 'pod4_test',
100
+ password: '#why#ring#',
101
+ adapter: :postgres,
102
+ database: 'pod4_test' }
103
+ end
96
104
 
97
105
  let(:db) do
98
- db = Sequel.connect(db_url)
106
+ db = Sequel.connect(db_hash)
99
107
  db_setup(db)
100
108
  db
101
109
  end
@@ -119,7 +127,7 @@ describe "SequelInterface (Pg)" do
119
127
  describe "#new" do
120
128
 
121
129
  context "when passed a Sequel DB object" do
122
- let(:ifce) { sequel_interface_class.new(Sequel.connect db_url) }
130
+ let(:ifce) { sequel_interface_class.new(Sequel.connect db_hash) }
123
131
 
124
132
  it "uses it to create a connection" do
125
133
  expect( ifce._connection ).to be_a Connection
@@ -133,7 +141,7 @@ describe "SequelInterface (Pg)" do
133
141
  end
134
142
 
135
143
  context "when passed a String" do
136
- let(:ifce) { sequel_interface_class.new(db_url) }
144
+ let(:ifce) { sequel_interface_class.new(db_hash) }
137
145
 
138
146
  it "uses it to create a connection" do
139
147
  expect( ifce._connection ).to be_a Connection
@@ -160,7 +168,7 @@ describe "SequelInterface (Pg)" do
160
168
  # Normally we'd expect on _every_ use, but Sequel is different
161
169
  it "calls Connection#client on first use" do
162
170
  # When we pass a connection object we are expected to set the data layer option
163
- conn.data_layer_options = Sequel.connect(db_url)
171
+ conn.data_layer_options = Sequel.connect(db_hash)
164
172
 
165
173
  expect( ifce._connection ).to receive(:client).with(ifce).and_call_original
166
174
 
@@ -6,13 +6,13 @@ DB[:tds] =
6
6
  password: 'pod4test' }
7
7
 
8
8
  DB[:pg] =
9
- { host: 'centos7andy',
9
+ { host: 'postgresql01.jhallpr.com',
10
10
  dbname: 'pod4_test',
11
- user: 'pod4test',
12
- password: 'pod4test' }
11
+ user: 'pod4_test',
12
+ password: '#why#ring#' }
13
13
 
14
14
  DB[:sequel] =
15
- { host: 'centos7andy',
16
- user: 'pod4test',
17
- password: 'pod4test' }
15
+ { host: 'postgresql01',
16
+ user: 'pod4_test',
17
+ password: '#why#ring#' }
18
18
 
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pod4
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Jones
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2021-08-24 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: devnull
@@ -117,7 +116,6 @@ homepage: https://bitbucket.org/andy-twosticks/pod4
117
116
  licenses:
118
117
  - MIT
119
118
  metadata: {}
120
- post_install_message:
121
119
  rdoc_options: []
122
120
  require_paths:
123
121
  - lib
@@ -125,15 +123,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
125
123
  requirements:
126
124
  - - ">="
127
125
  - !ruby/object:Gem::Version
128
- version: '0'
126
+ version: 2.7.4
129
127
  required_rubygems_version: !ruby/object:Gem::Requirement
130
128
  requirements:
131
129
  - - ">="
132
130
  - !ruby/object:Gem::Version
133
131
  version: '0'
134
132
  requirements: []
135
- rubygems_version: 3.1.6
136
- signing_key:
133
+ rubygems_version: 3.6.7
137
134
  specification_version: 4
138
135
  summary: Totally not an ORM
139
136
  test_files: