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