pgtk 0.19.1 → 0.20.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 +4 -4
- data/Gemfile.lock +1 -1
- data/Rakefile +1 -0
- data/lib/pgtk/impatient.rb +10 -0
- data/lib/pgtk/pool.rb +54 -1
- data/lib/pgtk/retry.rb +14 -6
- data/lib/pgtk/spy.rb +11 -3
- data/lib/pgtk/stash.rb +68 -60
- data/lib/pgtk/version.rb +1 -1
- data/lib/pgtk/wire.rb +1 -1
- data/pgtk.gemspec +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8177f7980e33d9ef9a42fa130f732f1e8109175c417798e4e1046b0f72ae0a85
|
|
4
|
+
data.tar.gz: 43fca371b69dd3aaf154fca4b2d912edd6fd4709c6c6f09bc525749c2023fa92
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9cdddfb02085dfa5b48209aa0f4aace1825ba91d60f44fa9c5d736b78b8180086cecad8e25894f2b0cd16a3156c3e497af412ba67f0fcecf11684ae798f75872
|
|
7
|
+
data.tar.gz: f94264b6a3610ca388156430feb567ac405d6b785a92a15c4557b153b3edbbb4c22280c306cac3f0ed7b4b5e8792c93d423da8854c74078d6b1b851da5fe2ebc
|
data/Gemfile.lock
CHANGED
data/Rakefile
CHANGED
data/lib/pgtk/impatient.rb
CHANGED
|
@@ -74,6 +74,16 @@ class Pgtk::Impatient
|
|
|
74
74
|
@pool.version
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
+
# Convert internal state into text.
|
|
78
|
+
def dump
|
|
79
|
+
[
|
|
80
|
+
@pool.dump,
|
|
81
|
+
'',
|
|
82
|
+
"Pgtk::Impatient (timeout=#{@timeout}s):",
|
|
83
|
+
@off.map { |re| " #{re}" }
|
|
84
|
+
].join("\n")
|
|
85
|
+
end
|
|
86
|
+
|
|
77
87
|
# Execute a SQL query with a timeout.
|
|
78
88
|
#
|
|
79
89
|
# @param [String] sql The SQL query with params inside (possibly)
|
data/lib/pgtk/pool.rb
CHANGED
|
@@ -54,7 +54,7 @@ class Pgtk::Pool
|
|
|
54
54
|
def initialize(wire, log: Loog::NULL)
|
|
55
55
|
@wire = wire
|
|
56
56
|
@log = log
|
|
57
|
-
@pool =
|
|
57
|
+
@pool = IterableQueue.new
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
# Get the version of PostgreSQL server.
|
|
@@ -64,6 +64,19 @@ class Pgtk::Pool
|
|
|
64
64
|
@version ||= exec('SHOW server_version')[0]['server_version'].split[0]
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
+
# Get as much details about it as possible.
|
|
68
|
+
#
|
|
69
|
+
# @return [String] Summary of inner state
|
|
70
|
+
def dump
|
|
71
|
+
[
|
|
72
|
+
"PgSQL version: #{version}",
|
|
73
|
+
"#{@pool.size} connections:",
|
|
74
|
+
@pool.map do |c|
|
|
75
|
+
" ##{c.backend_pid} #{c.pipeline_status} #{c.status} #{c.transaction_status}"
|
|
76
|
+
end
|
|
77
|
+
].flatten.join("\n")
|
|
78
|
+
end
|
|
79
|
+
|
|
67
80
|
# Start it with a fixed number of connections. The amount of connections
|
|
68
81
|
# is specified in +max+ argument and should be big enough to handle
|
|
69
82
|
# the amount of parallel connections you may have to the database. However,
|
|
@@ -164,6 +177,46 @@ class Pgtk::Pool
|
|
|
164
177
|
end
|
|
165
178
|
end
|
|
166
179
|
|
|
180
|
+
# Thread-safe queue implementation that supports iteration.
|
|
181
|
+
# Unlike Ruby's Queue class, this implementation allows safe iteration
|
|
182
|
+
# over all elements while maintaining thread safety for concurrent access.
|
|
183
|
+
#
|
|
184
|
+
# This class is used internally by Pool to store database connections
|
|
185
|
+
# and provide the ability to iterate over them for inspection purposes.
|
|
186
|
+
class IterableQueue
|
|
187
|
+
def initialize
|
|
188
|
+
@items = []
|
|
189
|
+
@mutex = Mutex.new
|
|
190
|
+
@condition = ConditionVariable.new
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def <<(item)
|
|
194
|
+
@mutex.synchronize do
|
|
195
|
+
@items << item
|
|
196
|
+
@condition.signal
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def pop
|
|
201
|
+
@mutex.synchronize do
|
|
202
|
+
@condition.wait(@mutex) while @items.empty?
|
|
203
|
+
@items.shift
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def size
|
|
208
|
+
@mutex.synchronize do
|
|
209
|
+
@items.size
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def map(&)
|
|
214
|
+
@mutex.synchronize do
|
|
215
|
+
@items.map(&)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
167
220
|
# A temporary class to execute a single SQL request.
|
|
168
221
|
class Txn
|
|
169
222
|
def initialize(conn, log)
|
data/lib/pgtk/retry.rb
CHANGED
|
@@ -63,24 +63,32 @@ class Pgtk::Retry
|
|
|
63
63
|
@pool.version
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
+
# Convert internal state into text.
|
|
67
|
+
def dump
|
|
68
|
+
[
|
|
69
|
+
@pool.dump,
|
|
70
|
+
'',
|
|
71
|
+
"Pgtk::Retry (attempts=#{@attempts})"
|
|
72
|
+
].join("\n")
|
|
73
|
+
end
|
|
74
|
+
|
|
66
75
|
# Execute a SQL query with automatic retry for SELECT queries.
|
|
67
76
|
#
|
|
68
77
|
# @param [String] sql The SQL query with params inside (possibly)
|
|
69
|
-
# @param [Array] args List of arguments
|
|
70
78
|
# @return [Array] Result rows
|
|
71
|
-
def exec(sql, *
|
|
79
|
+
def exec(sql, *)
|
|
72
80
|
query = sql.is_a?(Array) ? sql.join(' ') : sql
|
|
73
81
|
if query.strip.upcase.start_with?('SELECT')
|
|
74
82
|
attempt = 0
|
|
75
83
|
begin
|
|
76
|
-
@pool.exec(sql, *
|
|
84
|
+
@pool.exec(sql, *)
|
|
77
85
|
rescue StandardError => e
|
|
78
86
|
attempt += 1
|
|
79
87
|
raise e if attempt >= @attempts
|
|
80
88
|
retry
|
|
81
89
|
end
|
|
82
90
|
else
|
|
83
|
-
@pool.exec(sql, *
|
|
91
|
+
@pool.exec(sql, *)
|
|
84
92
|
end
|
|
85
93
|
end
|
|
86
94
|
|
|
@@ -88,7 +96,7 @@ class Pgtk::Retry
|
|
|
88
96
|
#
|
|
89
97
|
# @yield [Object] Yields the transaction object
|
|
90
98
|
# @return [Object] Result of the block
|
|
91
|
-
def transaction(&
|
|
92
|
-
@pool.transaction(&
|
|
99
|
+
def transaction(&)
|
|
100
|
+
@pool.transaction(&)
|
|
93
101
|
end
|
|
94
102
|
end
|
data/lib/pgtk/spy.rb
CHANGED
|
@@ -62,14 +62,22 @@ class Pgtk::Spy
|
|
|
62
62
|
@pool.version
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
+
# Convert internal state into text.
|
|
66
|
+
def dump
|
|
67
|
+
[
|
|
68
|
+
@pool.dump,
|
|
69
|
+
'',
|
|
70
|
+
'Pgtk::Spy'
|
|
71
|
+
].join("\n")
|
|
72
|
+
end
|
|
73
|
+
|
|
65
74
|
# Execute a SQL query and track its execution.
|
|
66
75
|
#
|
|
67
76
|
# @param [String] sql The SQL query with params inside (possibly)
|
|
68
|
-
# @param [Array] args List of arguments
|
|
69
77
|
# @return [Array] Result rows
|
|
70
|
-
def exec(sql, *
|
|
78
|
+
def exec(sql, *)
|
|
71
79
|
start = Time.now
|
|
72
|
-
ret = @pool.exec(sql, *
|
|
80
|
+
ret = @pool.exec(sql, *)
|
|
73
81
|
@block&.call(sql.is_a?(Array) ? sql.join(' ') : sql, Time.now - start)
|
|
74
82
|
ret
|
|
75
83
|
end
|
data/lib/pgtk/stash.rb
CHANGED
|
@@ -28,42 +28,72 @@ class Pgtk::Stash
|
|
|
28
28
|
ALTS = ['UPDATE', 'INSERT INTO', 'DELETE FROM', 'TRUNCATE', 'ALTER TABLE', 'DROP TABLE'].freeze
|
|
29
29
|
ALTS_RE = Regexp.new("(?<=^|\\s)(?:#{ALTS.join('|')})\\s([a-z]+)(?=[^a-z]|$)")
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
SEPARATOR = ' --%*@#~($-- '
|
|
32
|
+
|
|
33
|
+
private_constant :MODS, :ALTS, :MODS_RE, :ALTS_RE, :SEPARATOR
|
|
32
34
|
|
|
33
35
|
# Initialize a new Stash with query caching.
|
|
34
36
|
#
|
|
35
|
-
# @param [Object]
|
|
37
|
+
# @param [Object] pool Original object
|
|
36
38
|
# @param [Hash] stash Optional existing stash to use (default: new empty stash)
|
|
37
39
|
# @option [Hash] queries Internal cache data (default: {})
|
|
38
40
|
# @option [Hash] tables Internal cache data (default: {})
|
|
39
41
|
# @option [Concurrent::ReentrantReadWriteLock] entrance Lock for write internal state
|
|
40
|
-
# @option [Concurrent::AtomicBoolean]
|
|
41
|
-
# @param [Integer]
|
|
42
|
+
# @option [Concurrent::AtomicBoolean] launched Latch for start timers once
|
|
43
|
+
# @param [Integer] refill_interval Interval in seconds for recalculate stale queries
|
|
42
44
|
# @param [Integer] top Number of queries to recalculate
|
|
43
45
|
# @param [Integer] threads Number of threads in threadpool
|
|
44
46
|
# @param [Loog] loog Logger for debugging (default: null logger)
|
|
45
47
|
def initialize(
|
|
46
|
-
|
|
48
|
+
pool,
|
|
47
49
|
stash = {
|
|
48
50
|
queries: {},
|
|
49
51
|
tables: {},
|
|
50
52
|
entrance: Concurrent::ReentrantReadWriteLock.new,
|
|
51
|
-
|
|
53
|
+
launched: Concurrent::AtomicBoolean.new(false)
|
|
52
54
|
},
|
|
53
|
-
|
|
55
|
+
refill_interval: 5,
|
|
54
56
|
top: 100,
|
|
55
57
|
threads: 5,
|
|
56
58
|
loog: Loog::NULL
|
|
57
59
|
)
|
|
58
|
-
@
|
|
60
|
+
@pool = pool
|
|
59
61
|
@stash = stash
|
|
60
62
|
@entrance = stash[:entrance]
|
|
61
|
-
@
|
|
63
|
+
@refill_interval = refill_interval
|
|
62
64
|
@top = top
|
|
63
65
|
@threads = threads
|
|
64
66
|
@loog = loog
|
|
65
67
|
end
|
|
66
68
|
|
|
69
|
+
# Get the PostgreSQL server version.
|
|
70
|
+
# @return [String] Version string of the database server
|
|
71
|
+
def version
|
|
72
|
+
@pool.version
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Convert internal state into text.
|
|
76
|
+
def dump
|
|
77
|
+
qq =
|
|
78
|
+
@stash[:queries].map do |k, v|
|
|
79
|
+
[
|
|
80
|
+
k.dup, # the query
|
|
81
|
+
v.values.count, # how many keys?
|
|
82
|
+
v.values.sum { |vv| vv[:popularity] }, # total popularity of all keys
|
|
83
|
+
v.values.count { |vv| vv[:stale] } # how many stale keys?
|
|
84
|
+
]
|
|
85
|
+
end
|
|
86
|
+
[
|
|
87
|
+
@pool.dump,
|
|
88
|
+
'',
|
|
89
|
+
"Pgtk::Stash (refill_interval=#{@refill_interval}s, top=#{@top}q, threads=#{@threads}t):",
|
|
90
|
+
" #{'not ' unless @stash[:launched]}launched",
|
|
91
|
+
" #{@stash[:tables].count} tables in cache",
|
|
92
|
+
" #{@stash[:queries].count} queries in cache:",
|
|
93
|
+
qq.sort_by { -_1[2] }.take(20).map { |a| " #{a[1]}/#{a[2]}p/#{a[3]}s: #{a[0]}" }
|
|
94
|
+
].join("\n")
|
|
95
|
+
end
|
|
96
|
+
|
|
67
97
|
# Execute a SQL query with optional caching.
|
|
68
98
|
#
|
|
69
99
|
# Read queries are cached, while write queries bypass the cache and invalidate related entries.
|
|
@@ -76,26 +106,25 @@ class Pgtk::Stash
|
|
|
76
106
|
pure = (query.is_a?(Array) ? query.join(' ') : query).gsub(/\s+/, ' ').strip
|
|
77
107
|
if MODS_RE.match?(pure) || /(^|\s)pg_[a-z_]+\(/.match?(pure)
|
|
78
108
|
tables = pure.scan(ALTS_RE).map(&:first).uniq
|
|
79
|
-
ret = @
|
|
109
|
+
ret = @pool.exec(pure, params, result)
|
|
80
110
|
@entrance.with_write_lock do
|
|
81
111
|
tables.each do |t|
|
|
82
112
|
@stash[:tables][t]&.each do |q|
|
|
83
113
|
@stash[:queries][q].each_key do |key|
|
|
84
|
-
@stash[:queries][q][key][
|
|
114
|
+
@stash[:queries][q][key][:stale] = true
|
|
85
115
|
end
|
|
86
116
|
end
|
|
87
117
|
end
|
|
88
118
|
end
|
|
89
119
|
else
|
|
90
|
-
key = params.map(&:to_s).join(
|
|
91
|
-
|
|
92
|
-
ret
|
|
93
|
-
|
|
94
|
-
ret = @pgsql.exec(pure, params, result)
|
|
120
|
+
key = params.map(&:to_s).join(SEPARATOR)
|
|
121
|
+
ret = @stash.dig(:queries, pure, key, :ret)
|
|
122
|
+
if ret.nil? || @stash.dig(:queries, pure, key, :stale)
|
|
123
|
+
ret = @pool.exec(pure, params, result)
|
|
95
124
|
unless pure.include?(' NOW() ')
|
|
96
125
|
@entrance.with_write_lock do
|
|
97
126
|
@stash[:queries][pure] ||= {}
|
|
98
|
-
@stash[:queries][pure][key] = {
|
|
127
|
+
@stash[:queries][pure][key] = { ret:, params:, result: }
|
|
99
128
|
tables = pure.scan(/(?<=^|\s)(?:FROM|JOIN) ([a-z_]+)(?=\s|$)/).map(&:first).uniq
|
|
100
129
|
tables.each do |t|
|
|
101
130
|
@stash[:tables][t] = [] if @stash[:tables][t].nil?
|
|
@@ -105,7 +134,12 @@ class Pgtk::Stash
|
|
|
105
134
|
end
|
|
106
135
|
end
|
|
107
136
|
end
|
|
108
|
-
|
|
137
|
+
if @stash.dig(:queries, query, key)
|
|
138
|
+
@entrance.with_write_lock do
|
|
139
|
+
@stash[:queries][query][key][:popularity] ||= 0
|
|
140
|
+
@stash[:queries][query][key][:popularity] += 1
|
|
141
|
+
end
|
|
142
|
+
end
|
|
109
143
|
end
|
|
110
144
|
ret
|
|
111
145
|
end
|
|
@@ -117,10 +151,10 @@ class Pgtk::Stash
|
|
|
117
151
|
# @yield [Pgtk::Stash] A stash connected to the transaction
|
|
118
152
|
# @return [Object] The result of the block
|
|
119
153
|
def transaction
|
|
120
|
-
@
|
|
154
|
+
@pool.transaction do |t|
|
|
121
155
|
yield Pgtk::Stash.new(
|
|
122
156
|
t, @stash,
|
|
123
|
-
|
|
157
|
+
refill_interval: @refill_interval,
|
|
124
158
|
top: @top,
|
|
125
159
|
threads: @threads,
|
|
126
160
|
loog: @loog
|
|
@@ -129,73 +163,47 @@ class Pgtk::Stash
|
|
|
129
163
|
end
|
|
130
164
|
|
|
131
165
|
# Start a new connection pool with the given arguments.
|
|
132
|
-
#
|
|
133
|
-
# @param args Arguments to pass to the underlying pool's start method
|
|
134
166
|
# @return [Pgtk::Stash] A new stash that shares the same cache
|
|
135
|
-
def start(*
|
|
136
|
-
|
|
167
|
+
def start(*)
|
|
168
|
+
launch!
|
|
137
169
|
Pgtk::Stash.new(
|
|
138
|
-
@
|
|
139
|
-
|
|
170
|
+
@pool.start(*), @stash,
|
|
171
|
+
refill_interval: @refill_interval,
|
|
140
172
|
top: @top,
|
|
141
173
|
threads: @threads,
|
|
142
174
|
loog: @loog
|
|
143
175
|
)
|
|
144
176
|
end
|
|
145
177
|
|
|
146
|
-
# Get the PostgreSQL server version.
|
|
147
|
-
#
|
|
148
|
-
# @return [String] Version string of the database server
|
|
149
|
-
def version
|
|
150
|
-
@pgsql.version
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
# Get statistics on the most used queries
|
|
154
|
-
#
|
|
155
|
-
# @return [Array<Array<String, Integer>>] Array of query and hits in desc hits order
|
|
156
|
-
def stats
|
|
157
|
-
@stash[:queries].map { |k, v| [k.dup, v.values.sum { |vv| vv['count'] }] }.sort_by { -_1[1] }
|
|
158
|
-
end
|
|
159
|
-
|
|
160
178
|
private
|
|
161
179
|
|
|
162
|
-
def
|
|
163
|
-
|
|
164
|
-
@entrance.with_write_lock do
|
|
165
|
-
@stash[:queries][query][key]['count'] ||= 0
|
|
166
|
-
@stash[:queries][query][key]['count'] += 1
|
|
167
|
-
end
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
def start_refresher
|
|
171
|
-
raise 'Cannot start cache refresh multiple times on same cache data' unless @stash[:start_refresher].make_true
|
|
180
|
+
def launch!
|
|
181
|
+
raise 'Cannot launch multiple times on same cache data' unless @stash[:launched].make_true
|
|
172
182
|
Concurrent::FixedThreadPool.new(@threads).then do |threadpool|
|
|
173
|
-
Concurrent::TimerTask.execute(execution_interval:
|
|
183
|
+
Concurrent::TimerTask.execute(execution_interval: 60 * 60, executor: threadpool) do
|
|
174
184
|
@entrance.with_write_lock do
|
|
175
185
|
@stash[:queries].each_key do |q|
|
|
176
186
|
@stash[:queries][q].each_key do |k|
|
|
177
|
-
@stash[:queries][q][k][
|
|
187
|
+
@stash[:queries][q][k][:popularity] = 0
|
|
178
188
|
end
|
|
179
189
|
end
|
|
180
190
|
end
|
|
181
191
|
end
|
|
182
|
-
Concurrent::TimerTask.execute(execution_interval: @
|
|
192
|
+
Concurrent::TimerTask.execute(execution_interval: @refill_interval, executor: threadpool) do
|
|
183
193
|
@stash[:queries]
|
|
184
|
-
.map { |k, v| [k, v.values.sum { |vv| vv[
|
|
194
|
+
.map { |k, v| [k, v.values.sum { |vv| vv[:popularity] }, v.values.any? { |vv| vv[:stale] }] }
|
|
185
195
|
.select { _1[2] }
|
|
186
196
|
.sort_by { -_1[1] }
|
|
187
197
|
.first(@top)
|
|
188
198
|
.each do |a|
|
|
189
199
|
q = a[0]
|
|
190
200
|
@stash[:queries][q].each_key do |k|
|
|
191
|
-
next unless @stash[:queries][q][k][
|
|
201
|
+
next unless @stash[:queries][q][k][:stale]
|
|
192
202
|
threadpool.post do
|
|
193
|
-
params = @stash[:queries][q][k]['params']
|
|
194
|
-
result = @stash[:queries][q][k]['result']
|
|
195
|
-
ret = @pgsql.exec(q, params, result)
|
|
196
203
|
@entrance.with_write_lock do
|
|
197
|
-
@stash[:queries][q]
|
|
198
|
-
|
|
204
|
+
h = @stash[:queries][q][k]
|
|
205
|
+
h[:stale] = false
|
|
206
|
+
h[:ret] = @pool.exec(q, h[:params], h[:result])
|
|
199
207
|
end
|
|
200
208
|
end
|
|
201
209
|
end
|
data/lib/pgtk/version.rb
CHANGED
data/lib/pgtk/wire.rb
CHANGED
data/pgtk.gemspec
CHANGED
|
@@ -10,7 +10,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
|
10
10
|
require_relative 'lib/pgtk/version'
|
|
11
11
|
Gem::Specification.new do |s|
|
|
12
12
|
s.required_rubygems_version = Gem::Requirement.new('>= 0') if s.respond_to? :required_rubygems_version=
|
|
13
|
-
s.required_ruby_version = '>= 2
|
|
13
|
+
s.required_ruby_version = '>= 3.2'
|
|
14
14
|
s.name = 'pgtk'
|
|
15
15
|
s.version = Pgtk::VERSION
|
|
16
16
|
s.license = 'MIT'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pgtk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.20.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yegor Bugayenko
|
|
@@ -180,7 +180,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
180
180
|
requirements:
|
|
181
181
|
- - ">="
|
|
182
182
|
- !ruby/object:Gem::Version
|
|
183
|
-
version: '2
|
|
183
|
+
version: '3.2'
|
|
184
184
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
185
185
|
requirements:
|
|
186
186
|
- - ">="
|