pgtk 0.23.0 → 0.24.1
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 +4 -4
- data/README.md +13 -0
- data/lib/pgtk/pool.rb +2 -0
- data/lib/pgtk/stash.rb +95 -30
- data/lib/pgtk/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 88c5e888b9c116ec175abea3c43fa02d2a878a3edbc61c901be4a07f5a7730d4
|
|
4
|
+
data.tar.gz: 608f79d18ee7799c1a4bd200d8157a64f25033431ab972fe41072ac7223dae44
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 788a641261c89c9da31133e8d38a9cfcb1bbb91d6d1e2b43161ade3827930ff54baf6096e1ac7c54f13d7921928aefc163d2a5750000e3a916a17c212ea8097e
|
|
7
|
+
data.tar.gz: 237557831c63cc930743f152abd2a08cd5783d664f4a56f3a5faf24bd359097799463fa0eb66440085b99c46e50a338faf3b76677addc36b0a3f21fb0b415fbe
|
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.
|
|
29
|
+
json (2.16.0)
|
|
30
30
|
language_server-protocol (3.17.0.5)
|
|
31
31
|
lint_roller (1.1.0)
|
|
32
32
|
logger (1.7.0)
|
|
@@ -52,7 +52,7 @@ GEM
|
|
|
52
52
|
pg (1.6.2-x64-mingw-ucrt)
|
|
53
53
|
pg (1.6.2-x86_64-linux)
|
|
54
54
|
prism (1.6.0)
|
|
55
|
-
qbash (0.4.
|
|
55
|
+
qbash (0.4.7)
|
|
56
56
|
backtrace (> 0)
|
|
57
57
|
elapsed (> 0)
|
|
58
58
|
loog (> 0)
|
|
@@ -76,7 +76,7 @@ GEM
|
|
|
76
76
|
rubocop-ast (>= 1.47.1, < 2.0)
|
|
77
77
|
ruby-progressbar (~> 1.7)
|
|
78
78
|
unicode-display_width (>= 2.4.0, < 4.0)
|
|
79
|
-
rubocop-ast (1.
|
|
79
|
+
rubocop-ast (1.48.0)
|
|
80
80
|
parser (>= 3.3.7.2)
|
|
81
81
|
prism (~> 1.4)
|
|
82
82
|
rubocop-minitest (0.38.2)
|
|
@@ -101,7 +101,7 @@ GEM
|
|
|
101
101
|
simplecov-html (0.13.2)
|
|
102
102
|
simplecov_json_formatter (0.1.4)
|
|
103
103
|
slop (4.10.1)
|
|
104
|
-
tago (0.
|
|
104
|
+
tago (0.4.0)
|
|
105
105
|
threads (0.4.1)
|
|
106
106
|
backtrace (~> 0)
|
|
107
107
|
concurrent-ruby (~> 1.0)
|
data/README.md
CHANGED
|
@@ -220,6 +220,19 @@ require 'pgtk/stash'
|
|
|
220
220
|
stash = Pgtk::Stash.new(pgsql)
|
|
221
221
|
```
|
|
222
222
|
|
|
223
|
+
You can configure `Stash` with optional parameters:
|
|
224
|
+
|
|
225
|
+
```ruby
|
|
226
|
+
stash = Pgtk::Stash.new(
|
|
227
|
+
pgsql,
|
|
228
|
+
cap: 10_000, # Maximum cached query results (default: 10,000)
|
|
229
|
+
cap_interval: 60, # Seconds between cache size enforcement (default: 60)
|
|
230
|
+
refill_interval: 16, # Seconds between stale query refilling (default: 16)
|
|
231
|
+
threads: 4, # Worker threads for background refilling (default: 4)
|
|
232
|
+
max_queue_length: 128 # Maximum refilling tasks in queue (default: 128)
|
|
233
|
+
)
|
|
234
|
+
```
|
|
235
|
+
|
|
223
236
|
`Stash` automatically caches read queries and invalidates the cache
|
|
224
237
|
when tables are modified:
|
|
225
238
|
|
data/lib/pgtk/pool.rb
CHANGED
|
@@ -7,6 +7,7 @@ require 'pg'
|
|
|
7
7
|
require 'loog'
|
|
8
8
|
require 'tago'
|
|
9
9
|
require_relative '../pgtk'
|
|
10
|
+
require_relative 'version'
|
|
10
11
|
require_relative 'wire'
|
|
11
12
|
|
|
12
13
|
# Pool provides a connection pool for PostgreSQL database connections.
|
|
@@ -74,6 +75,7 @@ class Pgtk::Pool
|
|
|
74
75
|
def dump
|
|
75
76
|
[
|
|
76
77
|
'Pgtk::Pool',
|
|
78
|
+
" Pgtk version: #{Pgtk::VERSION}",
|
|
77
79
|
" PgSQL version: #{version}",
|
|
78
80
|
" #{@pool.size} connections:",
|
|
79
81
|
@pool.map do |c|
|
data/lib/pgtk/stash.rb
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
require 'concurrent-ruby'
|
|
7
7
|
require 'joined'
|
|
8
8
|
require 'loog'
|
|
9
|
+
require 'tago'
|
|
9
10
|
require_relative '../pgtk'
|
|
10
11
|
|
|
11
12
|
# Database query cache implementation.
|
|
@@ -18,6 +19,12 @@ require_relative '../pgtk'
|
|
|
18
19
|
#
|
|
19
20
|
# The implementation is very naive! Use it at your own risk.
|
|
20
21
|
#
|
|
22
|
+
# @example Basic usage
|
|
23
|
+
# pool = Pgtk::Pool.new(...)
|
|
24
|
+
# stash = Pgtk::Stash.new(pool, cap: 1000, refill_interval: 30)
|
|
25
|
+
# stash.start!
|
|
26
|
+
# result = stash.exec('SELECT * FROM users WHERE id = $1', [42])
|
|
27
|
+
#
|
|
21
28
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
|
22
29
|
# Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
|
|
23
30
|
# License:: MIT
|
|
@@ -34,20 +41,32 @@ class Pgtk::Stash
|
|
|
34
41
|
|
|
35
42
|
# Initialize a new Stash with query caching.
|
|
36
43
|
#
|
|
37
|
-
# @param [Object] pool
|
|
38
|
-
# @param [Hash] stash
|
|
39
|
-
#
|
|
40
|
-
# @
|
|
41
|
-
#
|
|
42
|
-
# @param [Integer] max_queue_length
|
|
43
|
-
#
|
|
44
|
-
# @param [
|
|
44
|
+
# @param [Object] pool The underlying connection pool that executes actual database queries
|
|
45
|
+
# @param [Hash] stash Internal cache structure containing queries and tables hashes for sharing state
|
|
46
|
+
# across transactions
|
|
47
|
+
# @param [Integer] refill_interval Interval in seconds between background tasks that recalculate stale
|
|
48
|
+
# cached queries
|
|
49
|
+
# @param [Integer] max_queue_length Maximum number of refilling tasks allowed in the thread pool queue
|
|
50
|
+
# before new tasks are skipped
|
|
51
|
+
# @param [Integer] threads Number of worker threads in the background thread pool for cache refilling
|
|
52
|
+
# operations
|
|
53
|
+
# @param [Integer] cap Maximum number of cached query results to retain; oldest queries are evicted when
|
|
54
|
+
# this limit is exceeded
|
|
55
|
+
# @param [Integer] cap_interval Interval in seconds between background tasks that enforce the cache size
|
|
56
|
+
# cap by removing old queries
|
|
57
|
+
# @param [Loog] loog Logger instance for debugging and monitoring cache operations (default: null logger)
|
|
58
|
+
# @param [Concurrent::ReentrantReadWriteLock] entrance Read-write lock for thread-safe cache access
|
|
59
|
+
# shared across instances
|
|
60
|
+
# @param [Concurrent::AtomicBoolean] launched Atomic boolean flag tracking whether background tasks have
|
|
61
|
+
# been started to prevent multiple launches
|
|
45
62
|
def initialize(
|
|
46
63
|
pool,
|
|
47
64
|
stash: { queries: {}, tables: {} },
|
|
48
65
|
refill_interval: 16,
|
|
49
66
|
max_queue_length: 128,
|
|
50
67
|
threads: 4,
|
|
68
|
+
cap: 10_000,
|
|
69
|
+
cap_interval: 60,
|
|
51
70
|
loog: Loog::NULL,
|
|
52
71
|
entrance: Concurrent::ReentrantReadWriteLock.new,
|
|
53
72
|
launched: Concurrent::AtomicBoolean.new(false)
|
|
@@ -59,11 +78,19 @@ class Pgtk::Stash
|
|
|
59
78
|
@refill_interval = refill_interval
|
|
60
79
|
@max_queue_length = max_queue_length
|
|
61
80
|
@threads = threads
|
|
81
|
+
@cap = cap
|
|
82
|
+
@cap_interval = cap_interval
|
|
62
83
|
@loog = loog
|
|
63
84
|
@tpool = Concurrent::FixedThreadPool.new(@threads)
|
|
64
85
|
end
|
|
65
86
|
|
|
66
|
-
# Start
|
|
87
|
+
# Start the connection pool and launch background cache management tasks.
|
|
88
|
+
#
|
|
89
|
+
# Initializes background timer tasks for cache refilling and size capping.
|
|
90
|
+
# The refill task periodically updates stale cached queries based on popularity.
|
|
91
|
+
# The cap task removes oldest queries when cache size exceeds the configured limit.
|
|
92
|
+
#
|
|
93
|
+
# @return [void]
|
|
67
94
|
def start!
|
|
68
95
|
launch!
|
|
69
96
|
@pool.start!
|
|
@@ -76,38 +103,54 @@ class Pgtk::Stash
|
|
|
76
103
|
end
|
|
77
104
|
|
|
78
105
|
# Convert internal state into text.
|
|
106
|
+
#
|
|
107
|
+
# Generates a detailed report of the cache state including query counts,
|
|
108
|
+
# popularity scores, stale queries, and thread pool status.
|
|
109
|
+
#
|
|
110
|
+
# @return [String] Multi-line text representation of the current cache state
|
|
79
111
|
def dump
|
|
80
112
|
qq =
|
|
81
|
-
@stash[:queries].map do |
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
113
|
+
@stash[:queries].map do |q, kk|
|
|
114
|
+
{
|
|
115
|
+
q: q.dup, # the query
|
|
116
|
+
c: kk.values.count, # how many keys?
|
|
117
|
+
p: kk.values.sum { |vv| vv[:popularity] }, # total popularity of all keys
|
|
118
|
+
s: kk.values.count { |vv| vv[:stale] }, # how many stale keys?
|
|
119
|
+
u: kk.values.map { |vv| vv[:used] }.max || Time.now # when was it used
|
|
120
|
+
}
|
|
88
121
|
end
|
|
89
122
|
[
|
|
90
123
|
@pool.dump,
|
|
91
124
|
'',
|
|
92
|
-
|
|
125
|
+
# rubocop:disable Layout/LineLength
|
|
126
|
+
"Pgtk::Stash (refill_interval=#{@refill_interval}s, max_queue_length=#{@max_queue_length}, threads=#{@threads}, cap=#{@cap}, cap_interval=#{@cap_interval}s):",
|
|
127
|
+
# rubocop:enable Layout/LineLength
|
|
93
128
|
" #{'not ' if @launched.false?}launched",
|
|
129
|
+
" #{stash_size} queries stashed (#{stash_size > @cap ? 'above' : 'below'} the cap)",
|
|
94
130
|
" #{@tpool.queue_length} task(s) in the thread pool",
|
|
95
131
|
" #{@stash[:tables].count} table(s) in cache",
|
|
96
|
-
" #{qq.sum { |a| a[
|
|
97
|
-
qq.select { |a| a[
|
|
98
|
-
|
|
99
|
-
|
|
132
|
+
" #{qq.sum { |a| a[:s] }} stale quer(ies) in cache:",
|
|
133
|
+
qq.select { |a| a[:s].positive? }.sort_by { -_1[:p] }.take(8).map do |a|
|
|
134
|
+
" #{a[:c]}/#{a[:p]}p/#{a[:s]}s/#{a[:u].ago}: #{a[:q]}"
|
|
135
|
+
end,
|
|
136
|
+
" #{qq.count { |a| a[:s].zero? }} other quer(ies) in cache:",
|
|
137
|
+
qq.select { |a| a[:s].zero? }.sort_by { -_1[:p] }.take(16).map do |a|
|
|
138
|
+
" #{a[:c]}/#{a[:p]}p/#{a[:s]}s/#{a[:u].ago}: #{a[:q]}"
|
|
139
|
+
end
|
|
100
140
|
].join("\n")
|
|
101
141
|
end
|
|
102
142
|
|
|
103
143
|
# Execute a SQL query with optional caching.
|
|
104
144
|
#
|
|
105
145
|
# Read queries are cached, while write queries bypass the cache and invalidate related entries.
|
|
146
|
+
# Queries containing modification keywords (INSERT, UPDATE, DELETE, etc.) are executed directly
|
|
147
|
+
# and trigger invalidation of cached queries for affected tables. Read queries (SELECT)
|
|
148
|
+
# are cached by query text and parameter values. Queries containing NOW() are never cached.
|
|
106
149
|
#
|
|
107
|
-
# @param [String, Array<String>] query The SQL query to execute
|
|
108
|
-
# @param [Array] params Query parameters
|
|
109
|
-
# @param [Integer] result
|
|
110
|
-
# @return [PG::Result] Query result
|
|
150
|
+
# @param [String, Array<String>] query The SQL query to execute as a string or array of strings to be joined
|
|
151
|
+
# @param [Array] params Query parameters for placeholder substitution in prepared statements (default: empty array)
|
|
152
|
+
# @param [Integer] result Result format code where 0 requests text format and 1 requests binary format (default: 0)
|
|
153
|
+
# @return [PG::Result] Query result object containing rows and metadata from the database
|
|
111
154
|
def exec(query, params = [], result = 0)
|
|
112
155
|
pure = (query.is_a?(Array) ? query.join(' ') : query).gsub(/\s+/, ' ').strip
|
|
113
156
|
if MODS_RE.match?(pure) || /(^|\s)pg_[a-z_]+\(/.match?(pure)
|
|
@@ -174,13 +217,35 @@ class Pgtk::Stash
|
|
|
174
217
|
|
|
175
218
|
private
|
|
176
219
|
|
|
220
|
+
# Calculate total number of cached query results.
|
|
221
|
+
#
|
|
222
|
+
# Counts all cached query-parameter combinations across all queries.
|
|
223
|
+
#
|
|
224
|
+
# @return [Integer] Total count of cached query results
|
|
225
|
+
def stash_size
|
|
226
|
+
@stash[:queries].values.sum { |kk| kk.values.size }
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Launch background tasks for cache management.
|
|
230
|
+
#
|
|
231
|
+
# Starts two concurrent timer tasks: one for enforcing cache size cap by removing
|
|
232
|
+
# oldest queries, and another for refilling stale cached queries based on popularity.
|
|
233
|
+
# This method can only be called once per cache instance.
|
|
234
|
+
#
|
|
235
|
+
# @return [nil]
|
|
236
|
+
# @raise [RuntimeError] if background tasks have already been launched on this cache instance
|
|
177
237
|
def launch!
|
|
178
238
|
raise 'Cannot launch multiple times on same cache data' unless @launched.make_true
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
@
|
|
183
|
-
@stash[:queries]
|
|
239
|
+
Concurrent::TimerTask.execute(execution_interval: @cap_interval, executor: @tpool) do
|
|
240
|
+
loop do
|
|
241
|
+
break if stash_size <= @cap
|
|
242
|
+
@entrance.with_write_lock do
|
|
243
|
+
@stash[:queries].each_key do |q|
|
|
244
|
+
m = @stash[:queries][q].values.map { |h| h[:used] }.min
|
|
245
|
+
next unless m
|
|
246
|
+
@stash[:queries][q].delete_if { |_, h| h[:used] == m }
|
|
247
|
+
@stash[:queries].delete_if { |_, kk| kk.empty? }
|
|
248
|
+
end
|
|
184
249
|
end
|
|
185
250
|
end
|
|
186
251
|
end
|
data/lib/pgtk/version.rb
CHANGED