pgtk 0.27.0 → 0.28.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: 51f0c148d2990a05c012d2da4494bd303f333e428b7c188f9d3efe9a7ff6e078
4
- data.tar.gz: 33a5d9b992f0bc16d70cd922a5e15c054f0643e5b376eb039c62d5c129ddada2
3
+ metadata.gz: 15ef00bc464354f5067a8346303914924549a69127dd7d089511b432ceb61cb5
4
+ data.tar.gz: f0714a56d3d31bdc90f6a8b24bd871b4ac2256dabe32be94191afb92e2034f74
5
5
  SHA512:
6
- metadata.gz: 19539334e9bf2f48f5a3a640ab79cb6c737d2e6e7d21d03524c0146cb264f58197adf77cb04fcb4262e5ecc4eab4906f9000e279307a8321d16b7d26806152c3
7
- data.tar.gz: 1eab8e38e7d81f9f8dd02e68f8afbc8489a7379f9ee8f74cafb8f9cf3a105b7d11af67204baf9fbe0e8ee3af438f6391aef4a46fb9a96a16e708975a55dbf977
6
+ metadata.gz: 186a1581c002df615bef9ddecf615140ee0cfe4abd328f7ea841b891d09ffbb3f08ae058eb534abcb4c7002fd2b903d082e4b1d5a577b4f9d615a3b8ee8bd35d
7
+ data.tar.gz: efa556d86eea1b11834d8c19f3620abcc17d3567d3db0d9452d20c4cdd56ad59db13b38ecded753f86ca14f2d36fc95e7e2f67d43386246071ff3d65675bede0
data/Gemfile.lock CHANGED
@@ -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.7)
55
+ qbash (0.4.8)
56
56
  backtrace (> 0)
57
57
  elapsed (> 0)
58
58
  loog (> 0)
@@ -102,7 +102,7 @@ GEM
102
102
  simplecov_json_formatter (0.1.4)
103
103
  slop (4.10.1)
104
104
  tago (0.4.0)
105
- threads (0.4.1)
105
+ threads (0.5.0)
106
106
  backtrace (~> 0)
107
107
  concurrent-ruby (~> 1.0)
108
108
  timeout (0.4.4)
data/README.md CHANGED
@@ -228,8 +228,8 @@ stash = Pgtk::Stash.new(
228
228
  cap: 10_000, # Maximum cached query results (default: 10,000)
229
229
  cap_interval: 60, # Seconds between cache size enforcement (default: 60)
230
230
  refill_interval: 16, # Seconds between stale query refilling (default: 16)
231
- refill_delay: 0.5, # Seconds to wait before refilling the cache (default: 0)
232
- retire: 60, # Maximum age in seconds to keep a query in cache (default: 15 min)
231
+ refill_delay: 0.5, # Seconds to wait before refilling the cache
232
+ retire: 60, # Maximum age in seconds to keep a query in cache
233
233
  retire_interval: 5.5, # How often to retire (default: 60)
234
234
  threads: 4, # Worker threads for background refilling (default: 4)
235
235
  max_queue_length: 128 # Maximum refilling tasks in queue (default: 128)
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2019-2025 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ require 'nokogiri'
7
+ require 'rake/tasklib'
8
+ require_relative '../pgtk'
9
+
10
+ # Liquicheck rake task for check Liquibase XML files.
11
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
12
+ # Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
13
+ # License:: MIT
14
+ class Pgtk::LiquicheckTask < Rake::TaskLib
15
+ # Task name
16
+ # @return [Symbol]
17
+ attr_accessor :name
18
+
19
+ # Base directory where Liquibase XML files will be stored
20
+ # @return [String]
21
+ attr_accessor :dir
22
+
23
+ # Migration XML files pattern
24
+ # @return [String]
25
+ attr_accessor :pattern
26
+
27
+ def initialize(*args, &task_block)
28
+ super()
29
+ @name = args.shift || :liquicheck
30
+ @dir = 'liquibase'
31
+ @pattern = '*/*.xml'
32
+ desc 'Check the quality of Liquibase XML files' unless ::Rake.application.last_description
33
+ task(name, *args) do |_, task_args|
34
+ RakeFileUtils.send(:verbose, true) do
35
+ yield(*[self, task_args].slice(0, task_block.arity)) if block_given?
36
+ run
37
+ end
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def run
44
+ raise "Option 'dir' is mandatory" if !@dir || @dir.empty?
45
+ raise "Option 'pattern' is mandatory" if !@pattern || @pattern.empty?
46
+ errors = {}
47
+ Dir[File.join(File.expand_path(File.join(Dir.pwd, @dir)), @pattern)].each do |file|
48
+ doc = Nokogiri::XML(File.open(file))
49
+ doc.remove_namespaces!
50
+ path = doc.at_xpath('databaseChangeLog')&.attr('logicalFilePath')&.to_s
51
+ on(errors, file) do
52
+ must_have(path, 'logicalFilePath is empty')
53
+ must_equal(
54
+ path,
55
+ File.basename(file),
56
+ "logicalFilePath #{path.inspect} does not equal the xml file name #{File.basename(file).inspect}"
57
+ )
58
+ end
59
+ doc.xpath('databaseChangeLog/changeSet').each do |node|
60
+ id = node.attr('id')&.to_s
61
+ author = node.attr('author')&.to_s
62
+ context = node.attr('context')&.to_s
63
+ on(errors, file) do
64
+ must_have(id, 'ID is empty')
65
+ must_match(id, /[-a-z]+/, "ID #{id.inspect} has not suffix in #{context} context") if context
66
+ end
67
+ on(errors, file) do
68
+ must_have(author, 'author is empty')
69
+ must_match(
70
+ author,
71
+ /\A[-_ A-Za-z0-9]+\z/,
72
+ "author #{author.inspect} has illegal symbols"
73
+ )
74
+ end
75
+ on(errors, file) do
76
+ must_have(id, 'ID is empty')
77
+ must_have(path, 'logicalFilePath is empty')
78
+ must_match(
79
+ path.gsub(/[-_.a-z]/, ''),
80
+ /\A#{id.gsub(/[-a-z]/, '')}\z/,
81
+ "ID #{id.inspect} is not the beginning of a logicalFilePath #{path.inspect}"
82
+ )
83
+ end
84
+ end
85
+ end
86
+ return if errors.empty?
87
+ puts 'There are such errors in the Liquibase XML files.'
88
+ errors.each do |f, e|
89
+ puts "In file '#{f}':"
90
+ e.uniq.each do |msg|
91
+ puts " * #{msg}"
92
+ end
93
+ puts
94
+ end
95
+ exit(1)
96
+ end
97
+
98
+ def on(errors, file, &)
99
+ yield if block_given?
100
+ rescue MustError => e
101
+ (errors[file] ||= []) << e.message
102
+ end
103
+
104
+ def must_have(prop, msg)
105
+ (raise MustError, msg) if prop.nil? || prop.empty?
106
+ end
107
+
108
+ def must_equal(lprop, rprop, msg)
109
+ (raise MustError, msg) if lprop != rprop
110
+ end
111
+
112
+ def must_match(prop, regex, msg)
113
+ (raise MustError, msg) unless prop.match?(regex)
114
+ end
115
+
116
+ MustError = Class.new(StandardError)
117
+ private_constant :MustError
118
+ end
data/lib/pgtk/stash.rb CHANGED
@@ -116,67 +116,69 @@ class Pgtk::Stash
116
116
  #
117
117
  # @return [String] Multi-line text representation of the current cache state
118
118
  def dump
119
- qq =
120
- @stash[:queries].map do |q, kk|
121
- {
122
- q: q.dup, # the query
123
- c: kk.values.count, # how many keys?
124
- p: kk.values.sum { |vv| vv[:popularity] }, # total popularity of all keys
125
- s: kk.values.count { |vv| vv[:stale] }, # how many stale keys?
126
- u: kk.values.map { |vv| vv[:used] }.max || Time.now # when was it used
127
- }
128
- end
129
- [
130
- @pool.dump,
131
- '',
119
+ @entrance.with_read_lock do
120
+ qq =
121
+ @stash[:queries].map do |q, kk|
122
+ {
123
+ q: q.dup, # the query
124
+ c: kk.values.count, # how many keys?
125
+ p: kk.values.sum { |vv| vv[:popularity] }, # total popularity of all keys
126
+ s: kk.values.count { |vv| vv[:stale] }, # how many stale keys?
127
+ u: kk.values.map { |vv| vv[:used] }.max || Time.now # when was it used
128
+ }
129
+ end
132
130
  [
133
- 'Pgtk::Stash (',
131
+ @pool.dump,
132
+ '',
134
133
  [
135
- "threads=#{@threads}",
136
- "max_queue_length=#{@max_queue_length}",
137
- if @refill_interval
138
- [
139
- "refill_interval=#{@refill_interval}s",
140
- "refill_delay=#{@refill_delay}s"
141
- ]
142
- else
143
- 'no refilling'
144
- end,
145
- if @cap_interval
146
- [
147
- "cap_interval=#{@cap_interval}s",
148
- "cap=#{@cap}"
149
- ]
150
- else
151
- 'no capping'
152
- end,
153
- if @retire_interval
154
- [
155
- "retire_interval=#{@retire_interval}s",
156
- "retire=#{@retire}"
157
- ]
158
- else
159
- 'no retirement'
160
- end
161
- ].flatten.join(', '),
162
- '):'
163
- ].join,
164
- if @tpool
165
- " #{@tpool.queue_length} tasks in the thread pool"
166
- else
167
- ' Not launched yet'
168
- end,
169
- " #{stash_size} queries cached (#{stash_size > @cap ? 'above' : 'below'} the cap)",
170
- " #{@stash[:tables].count} tables in cache",
171
- " #{qq.sum { |a| a[:s] }} stale queries in cache:",
172
- qq.select { |a| a[:s].positive? }.sort_by { -_1[:p] }.take(8).map do |a|
173
- " #{a[:c]}/#{a[:p]}p/#{a[:s]}s/#{a[:u].ago}: #{a[:q]}"
174
- end,
175
- " #{qq.count { |a| a[:s].zero? }} other queries in cache:",
176
- qq.select { |a| a[:s].zero? }.sort_by { -_1[:p] }.take(16).map do |a|
177
- " #{a[:c]}/#{a[:p]}p/#{a[:s]}s/#{a[:u].ago}: #{a[:q]}"
178
- end
179
- ].join("\n")
134
+ 'Pgtk::Stash (',
135
+ [
136
+ "threads=#{@threads}",
137
+ "max_queue_length=#{@max_queue_length}",
138
+ if @refill_interval
139
+ [
140
+ "refill_interval=#{@refill_interval}s",
141
+ "refill_delay=#{@refill_delay}s"
142
+ ]
143
+ else
144
+ 'no refilling'
145
+ end,
146
+ if @cap_interval
147
+ [
148
+ "cap_interval=#{@cap_interval}s",
149
+ "cap=#{@cap}"
150
+ ]
151
+ else
152
+ 'no capping'
153
+ end,
154
+ if @retire_interval
155
+ [
156
+ "retire_interval=#{@retire_interval}s",
157
+ "retire=#{@retire}"
158
+ ]
159
+ else
160
+ 'no retirement'
161
+ end
162
+ ].flatten.join(', '),
163
+ '):'
164
+ ].join,
165
+ if @tpool
166
+ " #{@tpool.queue_length} tasks in the thread pool"
167
+ else
168
+ ' Not launched yet'
169
+ end,
170
+ " #{stash_size} queries cached (#{stash_size > @cap ? 'above' : 'below'} the cap)",
171
+ " #{@stash[:tables].count} tables in cache",
172
+ " #{qq.sum { |a| a[:s] }} stale queries in cache:",
173
+ qq.select { |a| a[:s].positive? }.sort_by { -_1[:p] }.take(8).map do |a|
174
+ " #{a[:c]}/#{a[:p]}p/#{a[:s]}s/#{a[:u].ago}: #{a[:q]}"
175
+ end,
176
+ " #{qq.count { |a| a[:s].zero? }} other queries in cache:",
177
+ qq.select { |a| a[:s].zero? }.sort_by { -_1[:p] }.take(16).map do |a|
178
+ " #{a[:c]}/#{a[:p]}p/#{a[:s]}s/#{a[:u].ago}: #{a[:q]}"
179
+ end
180
+ ].join("\n")
181
+ end
180
182
  end
181
183
 
182
184
  # Execute a SQL query with optional caching.
@@ -258,7 +260,9 @@ class Pgtk::Stash
258
260
  #
259
261
  # @return [Integer] Total count of cached query results
260
262
  def stash_size
261
- @stash[:queries].values.sum { |kk| kk.values.size }
263
+ @entrance.with_write_lock do
264
+ @stash[:queries].values.sum { |kk| kk.values.size }
265
+ end
262
266
  end
263
267
 
264
268
  # Launch background tasks for cache management.
@@ -298,12 +302,13 @@ class Pgtk::Stash
298
302
  end
299
303
  return unless @refill_interval
300
304
  Concurrent::TimerTask.execute(execution_interval: @refill_interval, executor: @tpool) do
301
- @stash[:queries]
305
+ qq =
306
+ @stash[:queries]
302
307
  .map { |k, v| [k, v.values.sum { |vv| vv[:popularity] }, v.values.any? { |vv| vv[:stale] }] }
303
308
  .select { _1[2] }
304
309
  .sort_by { -_1[1] }
305
- .each do |a|
306
- q = a[0]
310
+ .map { |a| a[0] }
311
+ qq.each do |q|
307
312
  @stash[:queries][q].each_key do |k|
308
313
  next unless @stash[:queries][q][k][:stale]
309
314
  next if @stash[:queries][q][k][:stale] > Time.now - @refill_delay
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.27.0'
14
+ VERSION = '0.28.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.27.0
4
+ version: 0.28.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko
@@ -156,6 +156,7 @@ files:
156
156
  - lib/pgtk.rb
157
157
  - lib/pgtk/impatient.rb
158
158
  - lib/pgtk/liquibase_task.rb
159
+ - lib/pgtk/liquicheck_task.rb
159
160
  - lib/pgtk/pgsql_task.rb
160
161
  - lib/pgtk/pool.rb
161
162
  - lib/pgtk/retry.rb