pod4 0.10.6 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
+