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 +4 -4
- data/Gemfile.lock +2 -2
- data/README.md +2 -2
- data/lib/pgtk/liquicheck_task.rb +118 -0
- data/lib/pgtk/stash.rb +68 -63
- data/lib/pgtk/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 15ef00bc464354f5067a8346303914924549a69127dd7d089511b432ceb61cb5
|
|
4
|
+
data.tar.gz: f0714a56d3d31bdc90f6a8b24bd871b4ac2256dabe32be94191afb92e2034f74
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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.
|
|
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
|
|
232
|
-
retire: 60, # Maximum age in seconds to keep a query in cache
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
131
|
+
@pool.dump,
|
|
132
|
+
'',
|
|
134
133
|
[
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
"
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
"
|
|
178
|
-
|
|
179
|
-
].
|
|
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
|
-
@
|
|
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
|
-
|
|
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
|
-
.
|
|
306
|
-
|
|
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
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.
|
|
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
|