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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 851e2e812bbf8ed39a1942d5cbe6a76814969a1599cc0e48cfa6e9e3b3ed8b6e
4
- data.tar.gz: 3736d4d8eceebdfabc61bff42fc7c3dfc2628d4ea639af2da4661e7ffbf4eea6
3
+ metadata.gz: 8177f7980e33d9ef9a42fa130f732f1e8109175c417798e4e1046b0f72ae0a85
4
+ data.tar.gz: 43fca371b69dd3aaf154fca4b2d912edd6fd4709c6c6f09bc525749c2023fa92
5
5
  SHA512:
6
- metadata.gz: 81cff65a765e54e42e30f1d8c0aa029b88fa47fefce482e26d33650ec612452f51421624faf1591e144e8ca64bfdc427bee9628728cdb6a9a31b38e8c06a0c74
7
- data.tar.gz: 593e47e81f8b412ebb34083f921d414d33dc3a3f4b3ea59126a39ca1d200046b7984910a572f2303764d02640976e450a0af1de2d2cbed7d6e735a2e82e92d10
6
+ metadata.gz: 9cdddfb02085dfa5b48209aa0f4aace1825ba91d60f44fa9c5d736b78b8180086cecad8e25894f2b0cd16a3156c3e497af412ba67f0fcecf11684ae798f75872
7
+ data.tar.gz: f94264b6a3610ca388156430feb567ac405d6b785a92a15c4557b153b3edbbb4c22280c306cac3f0ed7b4b5e8792c93d423da8854c74078d6b1b851da5fe2ebc
data/Gemfile.lock CHANGED
@@ -58,7 +58,7 @@ GEM
58
58
  loog (> 0)
59
59
  tago (> 0)
60
60
  racc (1.8.1)
61
- rack (3.2.3)
61
+ rack (3.2.4)
62
62
  rainbow (3.1.1)
63
63
  rake (13.3.1)
64
64
  random-port (0.7.6)
data/Rakefile CHANGED
@@ -30,6 +30,7 @@ require 'yard'
30
30
  desc 'Build Yard documentation'
31
31
  YARD::Rake::YardocTask.new do |t|
32
32
  t.files = ['lib/**/*.rb']
33
+ t.options = ['--fail-on-warning']
33
34
  end
34
35
 
35
36
  require 'rubocop/rake_task'
@@ -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 = Queue.new
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, *args)
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, *args)
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, *args)
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(&block)
92
- @pool.transaction(&block)
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, *args)
78
+ def exec(sql, *)
71
79
  start = Time.now
72
- ret = @pool.exec(sql, *args)
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
- private_constant :MODS, :ALTS, :MODS_RE, :ALTS_RE
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] pgsql PostgreSQL connection 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] start_refresher Latch for start timers once
41
- # @param [Integer] refresh Interval in seconds for recalculate stale queries
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
- pgsql,
48
+ pool,
47
49
  stash = {
48
50
  queries: {},
49
51
  tables: {},
50
52
  entrance: Concurrent::ReentrantReadWriteLock.new,
51
- start_refresher: Concurrent::AtomicBoolean.new(false)
53
+ launched: Concurrent::AtomicBoolean.new(false)
52
54
  },
53
- refresh: 5,
55
+ refill_interval: 5,
54
56
  top: 100,
55
57
  threads: 5,
56
58
  loog: Loog::NULL
57
59
  )
58
- @pgsql = pgsql
60
+ @pool = pool
59
61
  @stash = stash
60
62
  @entrance = stash[:entrance]
61
- @refresh = refresh
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 = @pgsql.exec(pure, params, result)
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]['stale'] = true
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
- @entrance.with_write_lock { @stash[:queries][pure] ||= {} }
92
- ret = @stash.dig(:queries, pure, key, 'ret')
93
- if ret.nil? || @stash.dig(:queries, pure, key, 'stale')
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] = { 'ret' => ret, 'params' => params, 'result' => result }
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
- count(pure, key)
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
- @pgsql.transaction do |t|
154
+ @pool.transaction do |t|
121
155
  yield Pgtk::Stash.new(
122
156
  t, @stash,
123
- refresh: @refresh,
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(*args)
136
- start_refresher
167
+ def start(*)
168
+ launch!
137
169
  Pgtk::Stash.new(
138
- @pgsql.start(*args), @stash,
139
- refresh: @refresh,
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 count(query, key)
163
- return if @stash.dig(:queries, query, key).nil?
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: 24 * 60 * 60, executor: threadpool) do
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]['count'] = 0
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: @refresh, executor: threadpool) do
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['count'] }, v.values.any? { |vv| vv['stale'] }] }
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]['stale']
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
- @stash[:queries][q][k] = { 'ret' => ret, 'params' => params, 'result' => result, 'count' => 1 }
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
@@ -11,5 +11,5 @@ require_relative '../pgtk'
11
11
  # License:: MIT
12
12
  module Pgtk
13
13
  # Current version of the library.
14
- VERSION = '0.19.1'
14
+ VERSION = '0.20.0'
15
15
  end
data/lib/pgtk/wire.rb CHANGED
@@ -72,7 +72,7 @@ class Pgtk::Wire::Env
72
72
  Pgtk::Wire::Direct.new(
73
73
  host: uri.host,
74
74
  port: uri.port,
75
- dbname: uri.path[1..-1],
75
+ dbname: uri.path[1..],
76
76
  user: uri.userinfo.split(':')[0],
77
77
  password: uri.userinfo.split(':')[1]
78
78
  ).connection
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.3'
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.19.1
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.3'
183
+ version: '3.2'
184
184
  required_rubygems_version: !ruby/object:Gem::Requirement
185
185
  requirements:
186
186
  - - ">="