pgtk 0.18.2 → 0.19.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: ffaf4c44a63b471c11803b43d5e9774a25d7d261136f69e9d4a03bd99b4bd2f7
4
- data.tar.gz: d3a3eee3b145b4edf8f4b3cf715efcb668b6d2b4c56574f8f23542484128b3a2
3
+ metadata.gz: 1cab54d9805f427ff6187a6cdec4fa2cd5424a0c4959191169e674eb2400a09a
4
+ data.tar.gz: 2d584fffabf7b943fc087d9bad1edb68296826b5b987e34725d5a252503f9f46
5
5
  SHA512:
6
- metadata.gz: 30982738a66af8cc678f7833a8497ff77d4745bf2f54dda935b4929e2d19d413f6c30ff37228aae42ce98537fa78649b45d0811e2d393ff95f6d00f3b3c799b3
7
- data.tar.gz: 030365d8a107dae67836b4f27cb6abef084c0d7bc9f60cbc1f8cc7e84b962cebd4b6eb3262d0a916b1a6bc8a0936b58a2d1196fb00c4e832621f6e04ece59bb3
6
+ metadata.gz: 6f3b1bd6f15acd85f50194b1aa9653f1b892b0dea6501dcacaa1c2cc38ccd3f609331fa3cccc7a3c79f86285861b28e77e87d6c84d4f7f9b81f272e938fed216
7
+ data.tar.gz: d583bfab4f9647f08235891899ab938dd4d778d49115c6a4bee4a72298810364fd419cdb271e145610001e8742f432ade4a6ed212b7bc74c62ce89caf0cc8f27
data/Gemfile.lock CHANGED
@@ -26,7 +26,7 @@ GEM
26
26
  loog (~> 0.6)
27
27
  tago (~> 0.1)
28
28
  joined (0.4.0)
29
- json (2.15.1)
29
+ json (2.15.2)
30
30
  language_server-protocol (3.17.0.5)
31
31
  lint_roller (1.1.0)
32
32
  logger (1.7.0)
@@ -45,7 +45,7 @@ GEM
45
45
  nokogiri (1.18.10-x86_64-linux-gnu)
46
46
  racc (~> 1.4)
47
47
  parallel (1.27.0)
48
- parser (3.3.9.0)
48
+ parser (3.3.10.0)
49
49
  ast (~> 2.4.1)
50
50
  racc
51
51
  pg (1.6.2-arm64-darwin)
@@ -60,12 +60,12 @@ GEM
60
60
  racc (1.8.1)
61
61
  rack (3.2.3)
62
62
  rainbow (3.1.1)
63
- rake (13.3.0)
63
+ rake (13.3.1)
64
64
  random-port (0.7.6)
65
65
  tago (~> 0.0)
66
66
  regexp_parser (2.11.3)
67
67
  rexml (3.4.4)
68
- rubocop (1.81.6)
68
+ rubocop (1.81.7)
69
69
  json (~> 2.3)
70
70
  language_server-protocol (~> 3.17.0.2)
71
71
  lint_roller (~> 1.1.0)
@@ -102,7 +102,7 @@ GEM
102
102
  simplecov_json_formatter (0.1.4)
103
103
  slop (4.10.1)
104
104
  tago (0.3.0)
105
- timeout (0.4.3)
105
+ timeout (0.4.4)
106
106
  unicode-display_width (3.2.0)
107
107
  unicode-emoji (~> 4.1)
108
108
  unicode-emoji (4.1.0)
data/lib/pgtk/stash.rb CHANGED
@@ -22,17 +22,46 @@ require_relative '../pgtk'
22
22
  # Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
23
23
  # License:: MIT
24
24
  class Pgtk::Stash
25
+ MODS = %w[INSERT DELETE UPDATE LOCK VACUUM TRANSACTION COMMIT ROLLBACK REINDEX TRUNCATE CREATE ALTER DROP SET].freeze
26
+ MODS_RE = Regexp.new("(^|\\s)(#{MODS.join('|')})(\\s|$)")
27
+
28
+ ALTS = ['UPDATE', 'INSERT INTO', 'DELETE FROM', 'TRUNCATE', 'ALTER TABLE', 'DROP TABLE'].freeze
29
+ ALTS_RE = Regexp.new("(?<=^|\\s)(?:#{ALTS.join('|')})\\s([a-z]+)(?=[^a-z]|$)")
30
+
31
+ private_constant :MODS, :ALTS, :MODS_RE, :ALTS_RE
32
+
25
33
  # Initialize a new Stash with query caching.
26
34
  #
27
35
  # @param [Object] pgsql PostgreSQL connection object
28
36
  # @param [Hash] stash Optional existing stash to use (default: new empty stash)
37
+ # @option [Hash] queries Internal cache data (default: {})
38
+ # @option [Hash] tables Internal cache data (default: {})
39
+ # @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
+ # @param [Integer] top Number of queries to recalculate
43
+ # @param [Integer] threads Number of threads in threadpool
29
44
  # @param [Loog] loog Logger for debugging (default: null logger)
30
- def initialize(pgsql, stash = {})
45
+ def initialize(
46
+ pgsql,
47
+ stash = {
48
+ queries: {},
49
+ tables: {},
50
+ entrance: Concurrent::ReentrantReadWriteLock.new,
51
+ start_refresher: Concurrent::AtomicBoolean.new(false)
52
+ },
53
+ refresh: 5,
54
+ top: 100,
55
+ threads: 5,
56
+ loog: Loog::NULL
57
+ )
31
58
  @pgsql = pgsql
32
59
  @stash = stash
33
- @stash[:queries] ||= {}
34
- @stash[:tables] ||= {}
35
- @entrance = Concurrent::ReentrantReadWriteLock.new
60
+ @entrance = stash[:entrance]
61
+ @refresh = refresh
62
+ @top = top
63
+ @threads = threads
64
+ @loog = loog
36
65
  end
37
66
 
38
67
  # Execute a SQL query with optional caching.
@@ -44,30 +73,29 @@ class Pgtk::Stash
44
73
  # @param [Integer] result Should be 0 for text results, 1 for binary
45
74
  # @return [PG::Result] Query result
46
75
  def exec(query, params = [], result = 0)
47
- mods = %w[INSERT DELETE UPDATE LOCK VACUUM TRANSACTION COMMIT ROLLBACK REINDEX TRUNCATE CREATE ALTER DROP SET]
48
- alts = ['UPDATE', 'INSERT INTO', 'DELETE FROM', 'TRUNCATE', 'ALTER TABLE', 'DROP TABLE']
49
76
  pure = (query.is_a?(Array) ? query.join(' ') : query).gsub(/\s+/, ' ').strip
50
- if Regexp.new("(^|\\s)(#{mods.join('|')})(\\s|$)").match?(pure) || /(^|\s)pg_[a-z_]+\(/.match?(pure)
51
- tables = pure.scan(Regexp.new("(?<=^|\\s)(?:#{alts.join('|')})\\s([a-z]+)(?=[^a-z]|$)")).map(&:first).uniq
77
+ if MODS_RE.match?(pure) || /(^|\s)pg_[a-z_]+\(/.match?(pure)
78
+ tables = pure.scan(ALTS_RE).map(&:first).uniq
52
79
  ret = @pgsql.exec(pure, params, result)
53
80
  @entrance.with_write_lock do
54
81
  tables.each do |t|
55
82
  @stash[:tables][t]&.each do |q|
56
- @stash[:queries].delete(q)
83
+ @stash[:queries][q].each_key do |key|
84
+ @stash[:queries][q][key]['stale'] = true
85
+ end
57
86
  end
58
- @stash[:tables].delete(t)
59
87
  end
60
88
  end
61
89
  else
62
90
  key = params.map(&:to_s).join(' -*&%^- ')
63
91
  @entrance.with_write_lock { @stash[:queries][pure] ||= {} }
64
- ret = @stash[:queries][pure][key]
65
- if ret.nil?
92
+ ret = @stash.dig(:queries, pure, key, 'ret')
93
+ if ret.nil? || @stash.dig(:queries, pure, key, 'stale')
66
94
  ret = @pgsql.exec(pure, params, result)
67
95
  unless pure.include?(' NOW() ')
68
96
  @entrance.with_write_lock do
69
97
  @stash[:queries][pure] ||= {}
70
- @stash[:queries][pure][key] = ret
98
+ @stash[:queries][pure][key] = { 'ret' => ret, 'params' => params, 'result' => result }
71
99
  tables = pure.scan(/(?<=^|\s)(?:FROM|JOIN) ([a-z_]+)(?=\s|$)/).map(&:first).uniq
72
100
  tables.each do |t|
73
101
  @stash[:tables][t] = [] if @stash[:tables][t].nil?
@@ -77,6 +105,7 @@ class Pgtk::Stash
77
105
  end
78
106
  end
79
107
  end
108
+ count(pure, key)
80
109
  end
81
110
  ret
82
111
  end
@@ -89,7 +118,13 @@ class Pgtk::Stash
89
118
  # @return [Object] The result of the block
90
119
  def transaction
91
120
  @pgsql.transaction do |t|
92
- yield Pgtk::Stash.new(t, @stash)
121
+ yield Pgtk::Stash.new(
122
+ t, @stash,
123
+ refresh: @refresh,
124
+ top: @top,
125
+ threads: @threads,
126
+ loog: @loog
127
+ )
93
128
  end
94
129
  end
95
130
 
@@ -98,7 +133,14 @@ class Pgtk::Stash
98
133
  # @param args Arguments to pass to the underlying pool's start method
99
134
  # @return [Pgtk::Stash] A new stash that shares the same cache
100
135
  def start(*args)
101
- Pgtk::Stash.new(@pgsql.start(*args), @stash)
136
+ start_refresher
137
+ Pgtk::Stash.new(
138
+ @pgsql.start(*args), @stash,
139
+ refresh: @refresh,
140
+ top: @top,
141
+ threads: @threads,
142
+ loog: @loog
143
+ )
102
144
  end
103
145
 
104
146
  # Get the PostgreSQL server version.
@@ -107,4 +149,58 @@ class Pgtk::Stash
107
149
  def version
108
150
  @pgsql.version
109
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
+ private
161
+
162
+ def count(query, key)
163
+ @entrance.with_write_lock do
164
+ @stash[:queries][query][key]['count'] ||= 0
165
+ @stash[:queries][query][key]['count'] += 1
166
+ end
167
+ end
168
+
169
+ def start_refresher
170
+ raise 'Cannot start cache refresh multiple times on same cache data' unless @stash[:start_refresher].make_true
171
+ Concurrent::FixedThreadPool.new(@threads).then do |threadpool|
172
+ Concurrent::TimerTask.execute(execution_interval: 24 * 60 * 60, executor: threadpool) do
173
+ @entrance.with_write_lock do
174
+ @stash[:queries].each_key do |q|
175
+ @stash[:queries][q].each_key do |k|
176
+ @stash[:queries][q][k]['count'] = 0
177
+ end
178
+ end
179
+ end
180
+ end
181
+ Concurrent::TimerTask.execute(execution_interval: @refresh, executor: threadpool) do
182
+ @stash[:queries]
183
+ .map { |k, v| [k, v.values.sum { |vv| vv['count'] }, v.values.any? { |vv| vv['stale'] }] }
184
+ .select { _1[2] }
185
+ .sort_by { -_1[1] }
186
+ .first(@top)
187
+ .each do |a|
188
+ q = a[0]
189
+ @stash[:queries][q].each_key do |k|
190
+ next unless @stash[:queries][q][k]['stale']
191
+ threadpool.post do
192
+ params = @stash[:queries][q][k]['params']
193
+ result = @stash[:queries][q][k]['result']
194
+ ret = @pgsql.exec(q, params, result)
195
+ @entrance.with_write_lock do
196
+ @stash[:queries][q] ||= {}
197
+ @stash[:queries][q][k] = { 'ret' => ret, 'params' => params, 'result' => result, 'count' => 1 }
198
+ end
199
+ end
200
+ end
201
+ end
202
+ end
203
+ end
204
+ nil
205
+ end
110
206
  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.18.2'
14
+ VERSION = '0.19.0'
15
15
  end
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.18.2
4
+ version: 0.19.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko