pod4 0.10.6 → 1.0.0

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.
Files changed (48) hide show
  1. checksums.yaml +5 -5
  2. data/.bugs/bugs +2 -1
  3. data/.bugs/details/b5368c7ef19065fc597b5692314da71772660963.txt +53 -0
  4. data/.hgtags +1 -0
  5. data/Gemfile +5 -5
  6. data/README.md +157 -46
  7. data/lib/pod4/basic_model.rb +9 -22
  8. data/lib/pod4/connection.rb +67 -0
  9. data/lib/pod4/connection_pool.rb +154 -0
  10. data/lib/pod4/errors.rb +20 -0
  11. data/lib/pod4/interface.rb +34 -12
  12. data/lib/pod4/model.rb +32 -27
  13. data/lib/pod4/nebulous_interface.rb +25 -30
  14. data/lib/pod4/null_interface.rb +22 -16
  15. data/lib/pod4/pg_interface.rb +84 -104
  16. data/lib/pod4/sequel_interface.rb +138 -82
  17. data/lib/pod4/tds_interface.rb +83 -70
  18. data/lib/pod4/tweaking.rb +105 -0
  19. data/lib/pod4/version.rb +1 -1
  20. data/md/breaking_changes.md +80 -0
  21. data/spec/common/basic_model_spec.rb +67 -70
  22. data/spec/common/connection_pool_parallelism_spec.rb +154 -0
  23. data/spec/common/connection_pool_spec.rb +246 -0
  24. data/spec/common/connection_spec.rb +129 -0
  25. data/spec/common/model_ai_missing_id_spec.rb +256 -0
  26. data/spec/common/model_plus_encrypting_spec.rb +16 -4
  27. data/spec/common/model_plus_tweaking_spec.rb +128 -0
  28. data/spec/common/model_plus_typecasting_spec.rb +10 -4
  29. data/spec/common/model_spec.rb +283 -363
  30. data/spec/common/nebulous_interface_spec.rb +159 -108
  31. data/spec/common/null_interface_spec.rb +88 -65
  32. data/spec/common/sequel_interface_pg_spec.rb +217 -161
  33. data/spec/common/shared_examples_for_interface.rb +50 -50
  34. data/spec/jruby/sequel_encrypting_jdbc_pg_spec.rb +1 -1
  35. data/spec/jruby/sequel_interface_jdbc_ms_spec.rb +3 -3
  36. data/spec/jruby/sequel_interface_jdbc_pg_spec.rb +3 -23
  37. data/spec/mri/pg_encrypting_spec.rb +1 -1
  38. data/spec/mri/pg_interface_spec.rb +311 -223
  39. data/spec/mri/sequel_encrypting_spec.rb +1 -1
  40. data/spec/mri/sequel_interface_spec.rb +177 -180
  41. data/spec/mri/tds_encrypting_spec.rb +1 -1
  42. data/spec/mri/tds_interface_spec.rb +296 -212
  43. data/tags +340 -174
  44. metadata +19 -11
  45. data/md/fixme.md +0 -3
  46. data/md/roadmap.md +0 -125
  47. data/md/typecasting.md +0 -80
  48. data/spec/common/model_new_validate_spec.rb +0 -204
@@ -1,3 +1,3 @@
1
1
  module Pod4
2
- VERSION = '0.10.6'
2
+ VERSION = '1.0.0'
3
3
  end
@@ -0,0 +1,80 @@
1
+ A list of breaking / major changes by version.
2
+
3
+ 1.0
4
+ ===
5
+
6
+ Interfaces Can Now Note If Their ID Autoincrements
7
+ --------------------------------------------------
8
+
9
+ Autoincrement defaults to true if missing. So any models without auto-incrementing keys will need
10
+ to change to specifically name them as such.
11
+
12
+ You can now add the id field to `attr_columns` even if the ID field autoincrements. Which means
13
+ that you can refer to the id field by name as an attribute instead of using `@model_id`, if you
14
+ want.
15
+
16
+ Some minor changes that arise from this:
17
+
18
+ * #to_ot now always includes the ID field, whether or not it is named in `attr_columns`.
19
+
20
+ * If you manually update the ID field even though autoincrement is true, that change will not be
21
+ stored in the database / whatever. We don't pass that on.
22
+
23
+ * If you change the ID field in a non-autoincrement model, `@model_id` is now updated to reflect
24
+ that when you call #update. (This was not true before 1.0.)
25
+
26
+ Connection Objects
27
+ ------------------
28
+
29
+ This is technically not a breaking change. No existing code needs to be rewritten; interfaces
30
+ create connection objects for you if you don't use them. But, this is a really big change
31
+ internally, and as such I would be surprised if it didn't effect existing < 1.0 code.
32
+
33
+ This counts double if you use PgInterface and TdsInterface, since these are now being served one
34
+ connection per thread and are finally really threadsafe.
35
+
36
+ NullInterface
37
+ -------------
38
+
39
+ The behaviour of NullInterface has changed. Prior to 1.0 it did not simulate an auto-incrementing
40
+ ID field. Now it does, and that behaviour is the default.
41
+
42
+ Existing code that assumes the previous behaviour should be fixed by setting the id_ai attribute to
43
+ false:
44
+
45
+ ```
46
+ ifce = NullInterface.new(:code, :name, [])
47
+ ifce.id_ai = false
48
+ set_interface ifce
49
+ ```
50
+
51
+ DSL To Declare a Custom List Method
52
+ -----------------------------------
53
+
54
+ This is provided by the new Tweaking mixin, so it's not a breaking change.
55
+
56
+ Model Status :empty
57
+ -------------------
58
+
59
+ This has been renamed to :unknown to reflect that it is also the status of objects created by #list;
60
+ :unknown means that validation has not been run yet. This definitely counts as a breaking change,
61
+ although you would only be effected if you were testing for :empty in a model...
62
+
63
+
64
+ 0.10.1
65
+ ======
66
+
67
+ Validate Method Takes A Parameter
68
+ ---------------------------------
69
+
70
+ You now need to give your #validate method a parameter, the operation that is being validated --
71
+ one of :create :read :update or :delete.
72
+
73
+ In fact you could optionally do this since 0.9. But in 0.10.1 we removed the slightly confusing
74
+ feature where if validation failed on a #delete, we deleted the record anyway. So 0.10.1 marks the
75
+ point where, for all practical purposes, you have to give your method a parameter and check it at
76
+ least for :delete.
77
+
78
+ Models that don't do this will not allow deletion of records that fail validation, which presumably
79
+ is an anti-feature for you.
80
+
@@ -1,10 +1,10 @@
1
- require 'octothorpe'
1
+ require "octothorpe"
2
2
 
3
- require 'pod4/basic_model'
4
- require 'pod4/null_interface'
3
+ require "pod4/basic_model"
4
+ require "pod4/null_interface"
5
5
 
6
6
 
7
- describe 'WeirdModel' do
7
+ describe "BasicModel" do
8
8
 
9
9
  ##
10
10
  # We define a model class to test, since in normal operation we would never use Model directly,
@@ -15,7 +15,7 @@ describe 'WeirdModel' do
15
15
  # unless we specifically say `.and_call_original` instead of `.and_return`.
16
16
  #
17
17
  # This is actually quite nice, but more than a little confusing when you see it for the first
18
- # time. Its use isn't spelled out in the RSpec docs AFAICS.
18
+ # time. Its use isn"t spelled out in the RSpec docs AFAICS.
19
19
  #
20
20
  let(:weird_model_class) do
21
21
  Class.new Pod4::BasicModel do
@@ -32,104 +32,105 @@ describe 'WeirdModel' do
32
32
  let(:model) { weird_model_class.new(20) }
33
33
 
34
34
 
35
- describe 'Model.set_interface' do
36
- it 'requires an Interface object' do
35
+ describe "Model.set_interface" do
36
+
37
+ it "requires an Interface object" do
37
38
  expect( weird_model_class ).to respond_to(:set_interface).with(1).argument
38
39
  end
39
40
 
40
- # it 'sets interface' - covered by the interface test
41
+ # it "sets interface" - covered by the interface test
41
42
  end
42
- ##
43
43
 
44
44
 
45
- describe 'Model.interface' do
46
- it 'is the interface object' do
45
+ describe "Model.interface" do
46
+
47
+ it "is the interface object" do
47
48
  expect( weird_model_class.interface ).to be_a_kind_of NullInterface
48
49
  expect( weird_model_class.interface.id_fld ).to eq :id
49
50
  end
51
+
50
52
  end
51
- ##
52
53
 
53
54
 
54
- describe '#new' do
55
+ describe "#new" do
55
56
 
56
- it 'takes an optional ID' do
57
+ it "takes an optional ID" do
57
58
  expect{ weird_model_class.new }.not_to raise_exception
58
59
  expect{ weird_model_class.new(1) }.not_to raise_exception
59
60
  end
60
61
 
61
- it 'sets the ID attribute' do
62
+ it "sets the ID attribute" do
62
63
  expect( weird_model_class.new(23).model_id ).to eq 23
63
64
  end
64
65
 
65
- it 'sets the status to empty' do
66
- expect( weird_model_class.new.model_status ).to eq :empty
66
+ it "sets the status to unknown" do
67
+ expect( weird_model_class.new.model_status ).to eq :unknown
67
68
  end
68
69
 
69
- it 'initializes the alerts attribute' do
70
+ it "initializes the alerts attribute" do
70
71
  expect( weird_model_class.new.alerts ).to eq([])
71
72
  end
72
73
 
73
- it 'doesn''t freak out if the ID is not an integer' do
74
+ it "doesn""t freak out if the ID is not an integer" do
74
75
  expect{ weird_model_class.new("france") }.not_to raise_exception
75
76
  expect( weird_model_class.new("france").model_id ).to eq "france"
76
77
  end
77
78
 
78
- end
79
- ##
79
+ end # of #new
80
+
80
81
 
82
+ describe "#interface" do
81
83
 
82
- describe '#interface' do
83
- it 'returns the interface set in the class definition, again' do
84
+ it "returns the interface set in the class definition, again" do
84
85
  expect( weird_model_class.new.interface ).to be_a_kind_of NullInterface
85
86
  expect( weird_model_class.new.interface.id_fld ).to eq :id
86
87
  end
87
- end
88
- ##
88
+
89
+ end # of #interface
89
90
 
90
91
 
91
- describe '#alerts' do
92
- it 'returns the list of alerts against the model' do
92
+ describe "#alerts" do
93
+
94
+ it "returns the list of alerts against the model" do
93
95
  cm = weird_model_class.new
94
- cm.fake_an_alert(:warning, :foo, 'one')
95
- cm.fake_an_alert(:error, :bar, 'two')
96
+ cm.fake_an_alert(:warning, :foo, "one")
97
+ cm.fake_an_alert(:error, :bar, "two")
96
98
 
97
99
  expect( cm.alerts.size ).to eq 2
98
100
  expect( cm.alerts.map{|a| a.message} ).to match_array(%w|one two|)
99
101
  end
100
- end
101
- ##
102
102
 
103
+ end # of #alerts
103
104
 
104
- describe '#add_alert' do
105
- # add_alert is a protected method, which is only supposed to be called
106
- # within the validate method of a subclass of Method. So we test it by
107
- # calling our alert faking method
108
105
 
109
- it 'requires type, message or type, field, message' do
106
+ describe "#add_alert" do
107
+ # add_alert is a private method, which is only supposed to be called within the a subclass of
108
+ # Method. So we test it by calling our alert faking method
109
+
110
+ it "requires type, message or type, field, message" do
110
111
  expect{ model.fake_an_alert }.to raise_exception ArgumentError
111
112
  expect{ model.fake_an_alert(nil) }.to raise_exception ArgumentError
112
- expect{ model.fake_an_alert('foo') }.to raise_exception ArgumentError
113
+ expect{ model.fake_an_alert("foo") }.to raise_exception ArgumentError
113
114
 
114
- expect{ model.fake_an_alert(:error, 'foo') }.not_to raise_exception
115
- expect{ model.fake_an_alert(:warning, :name, 'bar') }.
115
+ expect{ model.fake_an_alert(:error, "foo") }.not_to raise_exception
116
+ expect{ model.fake_an_alert(:warning, :name, "bar") }.
116
117
  not_to raise_exception
117
118
 
118
119
  end
119
120
 
120
- it 'only allows valid types' do
121
+ it "only allows valid types" do
121
122
  [:brian, :werning, nil, :alert, :danger].each do |l|
122
- expect{ model.fake_an_alert(l, 'foo') }.to raise_exception ArgumentError
123
+ expect{ model.fake_an_alert(l, "foo") }.to raise_exception ArgumentError
123
124
  end
124
125
 
125
126
  [:warning, :error, :success, :info].each do |l|
126
- expect{ model.fake_an_alert(l, 'foo') }.not_to raise_exception
127
+ expect{ model.fake_an_alert(l, "foo") }.not_to raise_exception
127
128
  end
128
129
 
129
130
  end
130
131
 
131
- it 'creates an Alert and adds it to @alerts' do
132
- lurch = 'Dnhhhhhh'
132
+ it "creates an Alert and adds it to @alerts" do
133
+ lurch = "Dnhhhhhh"
133
134
  model.fake_an_alert(:error, :price, lurch)
134
135
 
135
136
  expect( model.alerts.size ).to eq 1
@@ -137,80 +138,76 @@ describe 'WeirdModel' do
137
138
  expect( model.alerts.first.message ).to eq lurch
138
139
  end
139
140
 
140
- it 'sets @model_status if the type is worse than @model_status' do
141
- model.fake_an_alert(:warning, :price, 'xoo')
141
+ it "sets @model_status if the type is worse than @model_status" do
142
+ model.fake_an_alert(:warning, :price, "xoo")
142
143
  expect( model.model_status ).to eq :warning
143
144
 
144
- model.fake_an_alert(:success, :price, 'flom')
145
+ model.fake_an_alert(:success, :price, "flom")
145
146
  expect( model.model_status ).to eq :warning
146
147
 
147
- model.fake_an_alert(:info, :price, 'flom')
148
+ model.fake_an_alert(:info, :price, "flom")
148
149
  expect( model.model_status ).to eq :warning
149
150
 
150
- model.fake_an_alert(:error, :price, 'qar')
151
+ model.fake_an_alert(:error, :price, "qar")
151
152
  expect( model.model_status ).to eq :error
152
153
 
153
- model.fake_an_alert(:warning, :price, 'drazq')
154
+ model.fake_an_alert(:warning, :price, "drazq")
154
155
  expect( model.model_status ).to eq :error
155
156
  end
156
157
 
157
- it 'ignores a new alert if identical to an existing one' do
158
- lurch = 'Dnhhhhhh'
158
+ it "ignores a new alert if identical to an existing one" do
159
+ lurch = "Dnhhhhhh"
159
160
  2.times { model.fake_an_alert(:error, :price, lurch) }
160
161
 
161
162
  expect( model.alerts.size ).to eq 1
162
163
  end
163
164
 
164
- end
165
- ##
165
+ end # of #add_alert
166
166
 
167
167
 
168
- describe '#clear_alerts' do
168
+ describe "#clear_alerts" do
169
169
  before do
170
170
  model.fake_an_alert(:error, "bad stuff")
171
171
  model.clear_alerts
172
172
  end
173
173
 
174
- it 'resets the @alerts array' do
174
+ it "resets the @alerts array" do
175
175
  expect( model.alerts ).to eq([])
176
176
  end
177
177
 
178
- it 'sets model_status to :okay' do
178
+ it "sets model_status to :okay" do
179
179
  expect( model.model_status ).to eq :okay
180
180
  end
181
181
 
182
-
183
- end
184
- ##
182
+ end # of #clear_alerts
185
183
 
186
184
 
187
- describe '#raise_exceptions' do
185
+ describe "#raise_exceptions" do
188
186
 
189
- it 'is also known as .or_die' do
187
+ it "is also known as .or_die" do
190
188
  cm = weird_model_class.new
191
189
  expect( cm.method(:raise_exceptions) ).to eq( cm.method(:or_die) )
192
190
  end
193
191
 
194
- it 'raises ValidationError if model status is :error' do
195
- model.fake_an_alert(:error, :price, 'qar')
192
+ it "raises ValidationError if model status is :error" do
193
+ model.fake_an_alert(:error, :price, "qar")
196
194
  expect{ model.raise_exceptions }.to raise_exception Pod4::ValidationError
197
195
  end
198
196
 
199
- it 'does nothing if model status is not :error' do
197
+ it "does nothing if model status is not :error" do
200
198
  expect{ model.raise_exceptions }.not_to raise_exception
201
199
 
202
- model.fake_an_alert(:info, :price, 'qar')
200
+ model.fake_an_alert(:info, :price, "qar")
203
201
  expect{ model.raise_exceptions }.not_to raise_exception
204
202
 
205
- model.fake_an_alert(:success, :price, 'qar')
203
+ model.fake_an_alert(:success, :price, "qar")
206
204
  expect{ model.raise_exceptions }.not_to raise_exception
207
205
 
208
- model.fake_an_alert(:warning, :price, 'qar')
206
+ model.fake_an_alert(:warning, :price, "qar")
209
207
  expect{ model.raise_exceptions }.not_to raise_exception
210
208
  end
211
209
 
212
- end
213
- ##
210
+ end # of #raise_exceptions
214
211
 
215
212
 
216
213
  end
@@ -0,0 +1,154 @@
1
+ require "pod4/connection_pool"
2
+
3
+
4
+ ##
5
+ # These tests cover how connection pool handles being called by simultaneous threads. Note that
6
+ # none of these tests _can ever_ fail when running under MRI, because of the GIL.
7
+ #
8
+ # Under jRuby, though, they can fail. Probably! We're relying on >1 thread making the same call
9
+ # simultaneously, with 50 threads all trying to act at the same time. That's not actually _certain_
10
+ # to happen. Without the Mutex in ConnectionPool::Pool, these seem to fail MOST of the time. For
11
+ # me.
12
+ #
13
+ # These tests are in a seperate spec file because they screw up the test suite. One of two things
14
+ # happens:
15
+ #
16
+ # * A timeout waiting for threads to be "done" or be killed
17
+ # * A Stomp timeout error(!?)
18
+ #
19
+ # You can duplicate this by running these three tests, in this order:
20
+ #
21
+ # 1. This one
22
+ # 2. NebulousInterface
23
+ # 3. SequelInterface (pg)
24
+ #
25
+ # (It passes when you run it on its own.)
26
+ #
27
+ # My working theory is that we just run out of threads, somehow? It might be something to do with
28
+ # this jRuby bug: https://github.com/jruby/jruby/issues/5476
29
+ #
30
+ # For the time being I've renamed this test file `_spoc` instead of `_spec` so that it's not part
31
+ # of the test suite.
32
+ #
33
+ describe Pod4::ConnectionPool do
34
+
35
+ def make_threads(count, connection, interface)
36
+ threads = []
37
+
38
+ 1.upto(count) do |idx|
39
+ threads << Thread.new do
40
+ # Set things up and wait
41
+ Thread.current[:idx] = idx # might be useful for debugging
42
+ Thread.stop
43
+
44
+ # wait for the given sync time; call #client; signal done; then wait
45
+ sleep 0.1 until Time.now >= Thread.current[:time]
46
+ connection.client(interface)
47
+ Thread.current[:done1] = true
48
+ Thread.stop
49
+
50
+ # call #close; signal done; then wait
51
+ connection.close(interface)
52
+ Thread.current[:done2] = true
53
+ Thread.stop
54
+ end
55
+ end
56
+
57
+ threads
58
+ end
59
+
60
+ let(:ifce_class) do
61
+ Class.new Pod4::Interface do
62
+ def initialize; end
63
+ def close_connection; end
64
+ def new_connection(opts); @conn; end
65
+
66
+ def set_conn(c); @conn = c; end
67
+ end
68
+ end
69
+
70
+
71
+ describe "(Parallelism)" do
72
+
73
+ before(:each) do
74
+ # If a thread suffers an exception, that's probably because of a race condition somewhere.
75
+ # eg: without the Mutex on ConnectionPool::Pool, nils get assigned to the pool somehow.
76
+ Thread.abort_on_exception = true
77
+
78
+ @connection = ConnectionPool.new(interface: ifce_class, max_clients: 55)
79
+ @connection.data_layer_options = "meh"
80
+ @interface = ifce_class.new
81
+ @interface.set_conn "floom"
82
+
83
+ # Set up 50 threads to call things at the same time.
84
+ @threads = make_threads(50, @connection, @interface)
85
+ end
86
+
87
+ after(:each) { @threads.each{|t| t.kill} }
88
+
89
+ it "assigns new items to the pool from multiple threads successfully" do
90
+ test_start = Time.now
91
+
92
+ # Ask all the threads to restart, calling #client all at the same time
93
+ # (Unfortunately it's in the hands of Ruby's scheduler whether the thread gets restarted)
94
+ at = Time.now + 2
95
+ @threads.each{|t| t[:time] = at }
96
+ @threads.each{|t| t.run }
97
+ sleep 0.1 until (@threads.all?{|t| t[:done1] } || Time.now >= test_start + 5)
98
+
99
+ # We have no control over whether the scheduler will actually restart each thread!
100
+ # Best we can do is count the number of threads that ran
101
+ count = @threads.count{|t| t[:done1] }
102
+
103
+ expect( @connection._pool.size ).to eq count
104
+ expect( @connection._pool.select{|x| x.thread_id.nil? }.size ).to eq 0
105
+ end
106
+
107
+ #
108
+ # Note that we don't have to test the safety of the operation of retrieving the client for an
109
+ # already assigned thread, or for freeing that client for use by other threads -- since only
110
+ # one thread can ever access that pool item...
111
+ #
112
+
113
+ it "reassigns items to new threads from multiple threads successfully" do
114
+ test_start = Time.now
115
+
116
+ # ask all the threads to connect -- again, the scheduler might let us down.
117
+ at = Time.now
118
+ @threads.each{|t| t[:time] = at }
119
+ @threads.each{|t| t.run }
120
+ sleep 0.1 until (@threads.all?{|t| t[:done1] } || Time.now >= test_start + 5)
121
+ count1 = @threads.count{|t| t[:done1] }
122
+
123
+ # Release all the connections (that got run in the connect phase...)
124
+ # (Again, just because we ask a thread to run, that doesn't mean it does!)
125
+ @threads.select{|t| t[:done1] }.each{|t| t.run }
126
+ sleep 0.1 until (@threads.all?{|t| t[:done2] } || Time.now >= test_start + 10)
127
+ count2 = @threads.count{|t| t[:done2] }
128
+
129
+ # Make some new threads. These should reuse connections from the pool. Make a couple less
130
+ # than should be free.
131
+ newthreads = make_threads(count2 - 2, @connection, @interface)
132
+
133
+ at = Time.now + 2
134
+ newthreads.each{|t| t[:time] = at }
135
+ newthreads.each{|t| t.run }
136
+ sleep 0.1 until (newthreads.all?{|t| t[:done1] } || Time.now >= test_start + 15)
137
+
138
+ count3 = newthreads.count{|t| t[:done1] }
139
+
140
+ # So at this point count1 is the number of threads in @threads that were connected; count2
141
+ # the number that were then released; count3 the number of threads in newthreads that were
142
+ # (re-)connected.
143
+ expect( @connection._pool.size ).to eq count1
144
+ expect( @connection._pool.select{|x| x.thread_id.nil? }.size ).to eq(count1 - count3)
145
+
146
+ # tidy up
147
+ newthreads.each{|t| t.kill }
148
+ end
149
+
150
+ end # of (Parallelism)
151
+
152
+
153
+ end
154
+