pod4 0.10.6 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.bugs/bugs +2 -1
- data/.bugs/details/b5368c7ef19065fc597b5692314da71772660963.txt +53 -0
- data/.hgtags +1 -0
- data/Gemfile +5 -5
- data/README.md +157 -46
- data/lib/pod4/basic_model.rb +9 -22
- data/lib/pod4/connection.rb +67 -0
- data/lib/pod4/connection_pool.rb +154 -0
- data/lib/pod4/errors.rb +20 -0
- data/lib/pod4/interface.rb +34 -12
- data/lib/pod4/model.rb +32 -27
- data/lib/pod4/nebulous_interface.rb +25 -30
- data/lib/pod4/null_interface.rb +22 -16
- data/lib/pod4/pg_interface.rb +84 -104
- data/lib/pod4/sequel_interface.rb +138 -82
- data/lib/pod4/tds_interface.rb +83 -70
- data/lib/pod4/tweaking.rb +105 -0
- data/lib/pod4/version.rb +1 -1
- data/md/breaking_changes.md +80 -0
- data/spec/common/basic_model_spec.rb +67 -70
- data/spec/common/connection_pool_parallelism_spec.rb +154 -0
- data/spec/common/connection_pool_spec.rb +246 -0
- data/spec/common/connection_spec.rb +129 -0
- data/spec/common/model_ai_missing_id_spec.rb +256 -0
- data/spec/common/model_plus_encrypting_spec.rb +16 -4
- data/spec/common/model_plus_tweaking_spec.rb +128 -0
- data/spec/common/model_plus_typecasting_spec.rb +10 -4
- data/spec/common/model_spec.rb +283 -363
- data/spec/common/nebulous_interface_spec.rb +159 -108
- data/spec/common/null_interface_spec.rb +88 -65
- data/spec/common/sequel_interface_pg_spec.rb +217 -161
- data/spec/common/shared_examples_for_interface.rb +50 -50
- data/spec/jruby/sequel_encrypting_jdbc_pg_spec.rb +1 -1
- data/spec/jruby/sequel_interface_jdbc_ms_spec.rb +3 -3
- data/spec/jruby/sequel_interface_jdbc_pg_spec.rb +3 -23
- data/spec/mri/pg_encrypting_spec.rb +1 -1
- data/spec/mri/pg_interface_spec.rb +311 -223
- data/spec/mri/sequel_encrypting_spec.rb +1 -1
- data/spec/mri/sequel_interface_spec.rb +177 -180
- data/spec/mri/tds_encrypting_spec.rb +1 -1
- data/spec/mri/tds_interface_spec.rb +296 -212
- data/tags +340 -174
- metadata +19 -11
- data/md/fixme.md +0 -3
- data/md/roadmap.md +0 -125
- data/md/typecasting.md +0 -80
- data/spec/common/model_new_validate_spec.rb +0 -204
data/lib/pod4/basic_model.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
|
-
require
|
1
|
+
require "octothorpe"
|
2
2
|
|
3
|
-
require_relative
|
4
|
-
require_relative
|
5
|
-
require_relative
|
3
|
+
require_relative "metaxing"
|
4
|
+
require_relative "errors"
|
5
|
+
require_relative "alert"
|
6
6
|
|
7
7
|
|
8
8
|
module Pod4
|
@@ -20,17 +20,15 @@ module Pod4
|
|
20
20
|
class BasicModel
|
21
21
|
extend Metaxing
|
22
22
|
|
23
|
-
|
24
23
|
# The value of the ID field on the record
|
25
24
|
attr_reader :model_id
|
26
25
|
|
27
26
|
# one of Model::STATII
|
28
27
|
attr_reader :model_status
|
29
28
|
|
30
|
-
# Valid values for @model_status: :error :warning :okay :deleted or :
|
29
|
+
# Valid values for @model_status: :error :warning :okay :deleted or :unknown
|
31
30
|
STATII = %i|error warning okay deleted empty|
|
32
31
|
|
33
|
-
|
34
32
|
class << self
|
35
33
|
|
36
34
|
##
|
@@ -44,27 +42,23 @@ module Pod4
|
|
44
42
|
raise NotImplemented, "no call to set_interface in the model"
|
45
43
|
end
|
46
44
|
|
47
|
-
end
|
48
|
-
##
|
49
|
-
|
45
|
+
end # of class << self
|
50
46
|
|
51
47
|
##
|
52
48
|
# Initialize a model by passing it a unique id value.
|
53
49
|
# Override this to set initial values for your column attributes.
|
54
50
|
#
|
55
51
|
def initialize(id=nil)
|
56
|
-
@model_status = :
|
52
|
+
@model_status = :unknown
|
57
53
|
@model_id = id
|
58
54
|
@alerts = []
|
59
55
|
end
|
60
56
|
|
61
|
-
|
62
57
|
##
|
63
58
|
# Syntactic sugar; same as self.class.interface, which returns the interface instance.
|
64
59
|
#
|
65
60
|
def interface; self.class.interface; end
|
66
61
|
|
67
|
-
|
68
62
|
##
|
69
63
|
# Return the list of alerts.
|
70
64
|
#
|
@@ -72,11 +66,10 @@ module Pod4
|
|
72
66
|
#
|
73
67
|
def alerts; @alerts.dup; end
|
74
68
|
|
75
|
-
|
76
69
|
##
|
77
70
|
# Clear down the alerts.
|
78
71
|
#
|
79
|
-
# Note that set model_status to :okay. Theoretically it might need to be :
|
72
|
+
# Note that we set model_status to :okay. Theoretically it might need to be :unknown or :deleted,
|
80
73
|
# but if you are calling clear_alerts before a call to `read` or after a call to `delete`, then
|
81
74
|
# you have more problems than I can solve.
|
82
75
|
#
|
@@ -85,7 +78,6 @@ module Pod4
|
|
85
78
|
@model_status = :okay
|
86
79
|
end
|
87
80
|
|
88
|
-
|
89
81
|
##
|
90
82
|
# Raise a Pod4 exception for the model if any alerts are status :error; otherwise do
|
91
83
|
# nothing.
|
@@ -100,13 +92,10 @@ module Pod4
|
|
100
92
|
raise ValidationError.from_alert(al) if al && al.type == :error
|
101
93
|
self
|
102
94
|
end
|
103
|
-
|
104
95
|
alias :or_die :raise_exceptions
|
105
96
|
|
106
|
-
|
107
97
|
private
|
108
98
|
|
109
|
-
|
110
99
|
##
|
111
100
|
# Add a Pod4::Alert to the model instance @alerts attribute
|
112
101
|
#
|
@@ -123,10 +112,8 @@ module Pod4
|
|
123
112
|
st = @alerts.sort.first.type
|
124
113
|
@model_status = st if %i|error warning|.include?(st)
|
125
114
|
end
|
126
|
-
|
127
115
|
|
128
|
-
end
|
129
|
-
##
|
116
|
+
end # of BasicModel
|
130
117
|
|
131
118
|
|
132
119
|
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require_relative 'interface'
|
2
|
+
|
3
|
+
|
4
|
+
module Pod4
|
5
|
+
|
6
|
+
|
7
|
+
class Connection
|
8
|
+
|
9
|
+
attr_reader :interface_class
|
10
|
+
attr_accessor :data_layer_options
|
11
|
+
|
12
|
+
##
|
13
|
+
# Intitialise a Connection. You must pass a Pod4::Interface class. The connection object will
|
14
|
+
# only accept calls from instances of this class.
|
15
|
+
#
|
16
|
+
# `conn = Pod4::Connection.new(interface: MyInterface)`
|
17
|
+
#
|
18
|
+
def initialize(args)
|
19
|
+
raise ArgumentError, "Connection#new needs a Hash" unless args.is_a? Hash
|
20
|
+
raise ArgumentError, "You must pass a Pod4::Interface" \
|
21
|
+
unless args[:interface] \
|
22
|
+
&& args[:interface].is_a?(Class) \
|
23
|
+
&& args[:interface].ancestors.include?(Interface)
|
24
|
+
|
25
|
+
@interface_class = args[:interface]
|
26
|
+
@data_layer_options = nil
|
27
|
+
@client = nil
|
28
|
+
@options = nil
|
29
|
+
end
|
30
|
+
|
31
|
+
##
|
32
|
+
# When an interface wants a connection, it calls connection.client. If the connection does
|
33
|
+
# not have one, it asks the interface for one....
|
34
|
+
#
|
35
|
+
# Interface is an instance of whatever class you passed to Connection when you initialised
|
36
|
+
# it. That is: when an interface wants a connection, it passes `self`.
|
37
|
+
#
|
38
|
+
def client(interface)
|
39
|
+
fail_bad_interfaces(interface)
|
40
|
+
@client ||= interface.new_connection(@data_layer_options)
|
41
|
+
@client
|
42
|
+
end
|
43
|
+
|
44
|
+
##
|
45
|
+
# Close the connection.
|
46
|
+
# In the case of a single connection, this is probably not going to get used much. But.
|
47
|
+
#
|
48
|
+
def close(interface)
|
49
|
+
fail_bad_interfaces(interface)
|
50
|
+
interface.close_connection
|
51
|
+
@client = nil
|
52
|
+
return self
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def fail_bad_interfaces(f)
|
58
|
+
raise ArgumentError, "That's not a #@interface_class", caller \
|
59
|
+
unless f.kind_of?(@interface_class)
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
end # of Connection
|
64
|
+
|
65
|
+
|
66
|
+
end
|
67
|
+
|
@@ -0,0 +1,154 @@
|
|
1
|
+
require "time"
|
2
|
+
require "thread"
|
3
|
+
|
4
|
+
require_relative "interface"
|
5
|
+
require_relative "connection"
|
6
|
+
require_relative "errors"
|
7
|
+
|
8
|
+
|
9
|
+
module Pod4
|
10
|
+
|
11
|
+
|
12
|
+
class ConnectionPool < Connection
|
13
|
+
|
14
|
+
PoolItem = Struct.new(:client, :thread_id)
|
15
|
+
|
16
|
+
class Pool
|
17
|
+
def initialize
|
18
|
+
@items = []
|
19
|
+
@mutex = Mutex.new
|
20
|
+
end
|
21
|
+
|
22
|
+
def <<(cl)
|
23
|
+
@mutex.synchronize do
|
24
|
+
@items << PoolItem.new(cl, Thread.current.object_id)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def get_current
|
29
|
+
@items.find{|x| x.thread_id == Thread.current.object_id }
|
30
|
+
end
|
31
|
+
|
32
|
+
def get_free
|
33
|
+
@mutex.synchronize do
|
34
|
+
pi = @items.find{|x| x.thread_id.nil? }
|
35
|
+
pi.thread_id = Thread.current.object_id if pi
|
36
|
+
pi
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def release
|
41
|
+
pi = get_current
|
42
|
+
pi.thread_id = nil if pi
|
43
|
+
end
|
44
|
+
|
45
|
+
def drop
|
46
|
+
@items.delete_if{|x| x.thread_id == Thread.current.object_id }
|
47
|
+
end
|
48
|
+
|
49
|
+
def size
|
50
|
+
@items.size
|
51
|
+
end
|
52
|
+
|
53
|
+
def _dump
|
54
|
+
@mutex.synchronize do
|
55
|
+
@items
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end # of Pool
|
59
|
+
|
60
|
+
attr_reader :max_clients, :max_wait
|
61
|
+
|
62
|
+
DEFAULT_MAX_CLIENTS = 10
|
63
|
+
|
64
|
+
##
|
65
|
+
# As Connection, but with some options you can set.
|
66
|
+
#
|
67
|
+
# * max_clients -- if this many clients are assigned to threads, wait until one is freed.
|
68
|
+
# pass nil for no maximum. Tries to default to something sensible.
|
69
|
+
#
|
70
|
+
# * max_wait -- throw a Pod4::PoolTimeout if you wait more than this time in seconds.
|
71
|
+
# Pass nil to wait forever. Default is nil, because you would need to handle that timeout.
|
72
|
+
#
|
73
|
+
def initialize(args)
|
74
|
+
super(args)
|
75
|
+
|
76
|
+
@max_clients = args[:max_clients] || DEFAULT_MAX_CLIENTS
|
77
|
+
@max_wait = args[:max_wait]
|
78
|
+
@pool = Pool.new
|
79
|
+
end
|
80
|
+
|
81
|
+
##
|
82
|
+
# Return a client for the interface to use.
|
83
|
+
#
|
84
|
+
# Return the client we gave this thread before.
|
85
|
+
# Failing that, assign a free one from the pool.
|
86
|
+
# Failing that, ask the interface to give us a new client.
|
87
|
+
#
|
88
|
+
# Note: The interface passes itself in case we want to call it back to get a new client; but
|
89
|
+
# clients are assigned to a _thread_. Every interface in a given thread gets the same pool
|
90
|
+
# item, the same client object.
|
91
|
+
#
|
92
|
+
def client(interface)
|
93
|
+
time = Time.now
|
94
|
+
cl = nil
|
95
|
+
|
96
|
+
# NB: We are constrained to use loop in order for our test to work
|
97
|
+
loop do
|
98
|
+
if (pi = @pool.get_current)
|
99
|
+
cl = pi.client
|
100
|
+
break
|
101
|
+
end
|
102
|
+
|
103
|
+
if (pi = @pool.get_free)
|
104
|
+
cl = pi.client
|
105
|
+
break
|
106
|
+
end
|
107
|
+
|
108
|
+
if @max_clients && @pool.size >= @max_clients
|
109
|
+
raise Pod4::PoolTimeout if @max_wait && (Time.now - time > @max_wait)
|
110
|
+
sleep 1
|
111
|
+
next
|
112
|
+
end
|
113
|
+
|
114
|
+
cl = interface.new_connection(@data_layer_options)
|
115
|
+
@pool << cl
|
116
|
+
break
|
117
|
+
end # of loop
|
118
|
+
|
119
|
+
cl
|
120
|
+
end
|
121
|
+
|
122
|
+
##
|
123
|
+
# De-assign the client for the current thread from that thread.
|
124
|
+
#
|
125
|
+
# We never ask the interface to close the connection to the database. There is no advantage in
|
126
|
+
# doing that for us.
|
127
|
+
#
|
128
|
+
# Note: The interface passes itself in case we want to call it back to actually close the
|
129
|
+
# client; but clients are assigned to a _thread_.
|
130
|
+
#
|
131
|
+
def close(interface)
|
132
|
+
@pool.release
|
133
|
+
end
|
134
|
+
|
135
|
+
##
|
136
|
+
# Remove the client object entirely -- for example, because the connection to the database has
|
137
|
+
# expired.
|
138
|
+
#
|
139
|
+
def drop(interface)
|
140
|
+
@pool.drop
|
141
|
+
end
|
142
|
+
|
143
|
+
##
|
144
|
+
# Dump the internal pool (for test purposes only)
|
145
|
+
#
|
146
|
+
def _pool
|
147
|
+
@pool._dump
|
148
|
+
end
|
149
|
+
|
150
|
+
end # of ConnectionPool
|
151
|
+
|
152
|
+
|
153
|
+
end
|
154
|
+
|
data/lib/pod4/errors.rb
CHANGED
@@ -94,6 +94,26 @@ module Pod4
|
|
94
94
|
end
|
95
95
|
|
96
96
|
|
97
|
+
##
|
98
|
+
# Raised if ConnectionPool times out waiting for a client to become free.
|
99
|
+
# This can only happen if your pool has a maximum number of clients set, and a max_wait value
|
100
|
+
# set; and if all the clients are currently in use.
|
101
|
+
#
|
102
|
+
class PoolTimeout < Pod4Error
|
103
|
+
attr_reader :field
|
104
|
+
|
105
|
+
def self.from_alert(alert)
|
106
|
+
self.new(alert.message, alert.field)
|
107
|
+
end
|
108
|
+
|
109
|
+
def initialize(message=nil, field=nil)
|
110
|
+
super(message || $! && $!.message)
|
111
|
+
@field = field.to_s.to_sym
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
|
97
117
|
|
98
118
|
end
|
99
119
|
|
data/lib/pod4/interface.rb
CHANGED
@@ -38,9 +38,15 @@ module Pod4
|
|
38
38
|
# A field name in the data source, the name of the unique ID field.
|
39
39
|
#
|
40
40
|
def id_fld
|
41
|
-
raise NotImplemented, "Interface
|
41
|
+
raise NotImplemented, "Interface has no 'id_fld' method (use `set_id_fld`?)"
|
42
42
|
end
|
43
43
|
|
44
|
+
##
|
45
|
+
# true if id_fld autoincrements
|
46
|
+
#
|
47
|
+
def id_ai
|
48
|
+
raise NotImplemented, "Interface has no 'id_ai' method (use `set_id_fld`?)"
|
49
|
+
end
|
44
50
|
|
45
51
|
##
|
46
52
|
# Individual implementations are likely to have very different initialize methods, which will
|
@@ -51,7 +57,6 @@ module Pod4
|
|
51
57
|
raise NotImplemented, "Interface needs to define an 'initialize' method"
|
52
58
|
end
|
53
59
|
|
54
|
-
|
55
60
|
##
|
56
61
|
# List accepts a parameter as selection criteria, and returns an array of Octothorpes. Exactly
|
57
62
|
# what the selection criteria look like will vary from interface to interface. So will the
|
@@ -61,46 +66,63 @@ module Pod4
|
|
61
66
|
# Note that list should ALWAYS return an array; never nil.
|
62
67
|
#
|
63
68
|
def list(selection=nil)
|
64
|
-
raise NotImplemented, "Interface needs to define 'list' method"
|
69
|
+
raise NotImplemented, "Interface needs to define a 'list' method"
|
65
70
|
end
|
66
71
|
|
67
|
-
|
68
72
|
##
|
69
73
|
# Create accepts a record parameter (Hash or OT, but again, the format of this will vary)
|
70
74
|
# representing a record, and creates the record. Should return the ID for the new record.
|
71
75
|
#
|
72
76
|
def create(record)
|
73
|
-
raise NotImplemented, "Interface needs to define 'create' method"
|
77
|
+
raise NotImplemented, "Interface needs to define a 'create' method"
|
74
78
|
end
|
75
79
|
|
76
|
-
|
77
80
|
##
|
78
81
|
# Read accepts an ID, and returns an Octothorpe representing the unique record for that ID. If
|
79
82
|
# there is no record matching the ID then it returns an empty Octothorpe.
|
80
83
|
#
|
81
84
|
def read(id)
|
82
|
-
raise NotImplemented, "Interface needs to define 'read' method"
|
85
|
+
raise NotImplemented, "Interface needs to define a 'read' method"
|
83
86
|
end
|
84
87
|
|
85
|
-
|
86
88
|
##
|
87
89
|
# Update accepts an ID and a record parameter. It updates the record on the data source that
|
88
90
|
# matches the ID using the record parameter. It returns self.
|
89
91
|
#
|
90
92
|
def update(id, record)
|
91
|
-
raise NotImplemented, "Interface needs to define 'update' method"
|
93
|
+
raise NotImplemented, "Interface needs to define a 'update' method"
|
92
94
|
end
|
93
95
|
|
94
|
-
|
95
96
|
##
|
96
97
|
# delete removes the record with the given ID. returns self.
|
97
98
|
#
|
98
99
|
def delete(id)
|
99
|
-
raise NotImplemented, "Interface needs to define 'delete' method"
|
100
|
+
raise NotImplemented, "Interface needs to define a 'delete' method"
|
100
101
|
end
|
101
102
|
|
103
|
+
##
|
104
|
+
# Called by a Connection object to start a database connection
|
105
|
+
#
|
106
|
+
def new_connection(args)
|
107
|
+
raise NotImplemented, "Interface needs to define a 'new_connection' method"
|
108
|
+
end
|
109
|
+
|
110
|
+
##
|
111
|
+
# Called by a Connection Object to close the connection.
|
112
|
+
#
|
113
|
+
def close_connection(conn)
|
114
|
+
raise NotImplemented, "Interface needs to define a 'close_connection' method"
|
115
|
+
end
|
116
|
+
|
117
|
+
##
|
118
|
+
# For testing purposes you should expose a _connection method that returns the Connection
|
119
|
+
# object the Interface uses
|
120
|
+
#
|
121
|
+
def _connection
|
122
|
+
raise NotImplemented, "Interface needs to define a '_connection' method"
|
123
|
+
end
|
102
124
|
|
103
|
-
end
|
125
|
+
end # of Interface
|
104
126
|
|
105
127
|
|
106
128
|
end
|