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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 57546e93aaf2c029a795f61302d943e4f64e7d1e03b77d339636bdfb73d271d5
4
- data.tar.gz: 467c0493e69e8402b83ea6b537ac69760d5e712ac088b856307d2418449878f9
3
+ metadata.gz: 88c5e888b9c116ec175abea3c43fa02d2a878a3edbc61c901be4a07f5a7730d4
4
+ data.tar.gz: 608f79d18ee7799c1a4bd200d8157a64f25033431ab972fe41072ac7223dae44
5
5
  SHA512:
6
- metadata.gz: 83b61249a1888eed186c7faa7814c9a07f28fa9268d8d7fad1dfc0b9d7f4b304d4c835116fce6e32b6e651d7a2fc12731417c52922daae8ad9759419b32d2344
7
- data.tar.gz: 5107f90f035a11e3ab17c8718f759c6869ea24d3c195dafb1845d546587244ad1e6f82dca7da5b505055cefdafa97f7b0e6390f17374e2458f773e54f7f9dd75
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.15.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.5)
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.47.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.3.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 Original object
38
- # @param [Hash] stash Optional existing stash to use (default: new empty stash)
39
- # @option [Hash] queries Internal cache data (default: {})
40
- # @option [Hash] tables Internal cache data (default: {})
41
- # @param [Integer] refill_interval Interval in seconds for recalculate stale queries
42
- # @param [Integer] max_queue_length Number of refilling tasks in the queue
43
- # @param [Integer] threads Number of threads in tpool
44
- # @param [Loog] loog Logger for debugging (default: null logger)
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 a new connection pool with the given arguments.
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 |k, v|
82
- [
83
- k.dup, # the query
84
- v.values.count, # how many keys?
85
- v.values.sum { |vv| vv[:popularity] }, # total popularity of all keys
86
- v.values.count { |vv| vv[:stale] } # how many stale keys?
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
- "Pgtk::Stash (refill_interval=#{@refill_interval}s, max_queue_length=#{@max_queue_length}, threads=#{@threads}):",
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[3] }} stale quer(ies) in cache:",
97
- qq.select { |a| a[3].positive? }.sort_by { -_1[2] }.take(16).map { |a| " #{a[1]}/#{a[2]}p/#{a[3]}s: #{a[0]}" },
98
- " #{qq.count { |a| a[3].zero? }} other quer(ies) in cache:",
99
- qq.select { |a| a[3].zero? }.sort_by { -_1[2] }.take(8).map { |a| " #{a[1]}/#{a[2]}p/#{a[3]}s: #{a[0]}" }
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 Should be 0 for text results, 1 for binary
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
- retire = 60 * 60
180
- Concurrent::TimerTask.execute(execution_interval: retire, executor: @tpool) do
181
- @entrance.with_write_lock do
182
- @stash[:queries].each_key do |q|
183
- @stash[:queries][q].delete_if { |_, h| h[:used] < Time.now - retire }
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
@@ -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.23.0'
14
+ VERSION = '0.24.1'
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.23.0
4
+ version: 0.24.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko