pgbus 0.16.7 → 0.17.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 +4 -4
- data/CHANGELOG.md +2 -0
- data/Rakefile +3 -168
- data/lib/generators/pgbus/templates/upgrade_pgmq.rb.erb +22 -0
- data/lib/pgbus/pgmq_schema/fixups/pgmq_v1.13.0.sql +49 -0
- data/lib/pgbus/pgmq_schema/pgmq_v1.13.0.sql +2193 -0
- data/lib/pgbus/pgmq_schema.rb +41 -3
- data/lib/pgbus/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 11b7ad58d91fe1eb9c0388008907523c77324f286f27a0e97a94353e4a8577b0
|
|
4
|
+
data.tar.gz: '0707593bb7c826f48f9a84db9a666057aaed3407b0d1cb559007f57caf080be2'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0f1422dea206dbeaac10b1fd7dfa74da2338c007cffb48d7a2b5429bf3a94e6f3e78b5795832d0a2f45346fe4350d4c7355d16b773247420894ea5234c50db85
|
|
7
|
+
data.tar.gz: 6d30c3583b7e6b5fa9dc89cf42115f57b182dea18b2d363f1ec413d6096ba32b76d75f008dcf7a2f25de3baf9ceba7e17f1edbb382b96e2aeb23bf52a8824af2
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
- **Concurrency is now visible: a section on the Locks page, three gauges, a home stat card and an MCP tool.** `limits_concurrency … on_conflict: :block` became durable in #460 — a parked job is never dropped and a key never runs past `to:` — but nothing showed it, so an operator whose per-record pipeline looked "stuck" had no way to see that the key's slot was held, how many jobs were parked behind it, or how long the oldest had waited, short of SQL. `pgbus_semaphores` and `pgbus_blocked_executions` appeared on no page, in no gauge, in no tool. One new read, `Web::DataSource#concurrency_stats`, now feeds four surfaces: the Locks page's **Concurrency** section (parked jobs, oldest wait, slots held, keys at limit, then a row per key with value / limit, lease state, parked count and oldest wait, auto-refreshing like the other dashboard panels); a **Parked jobs** card on the dashboard home; three unlabelled gauges on both `/pgbus/api/metrics` and the AppSignal probe (`pgbus_concurrency_blocked_executions`, `pgbus_concurrency_blocked_oldest_age_seconds`, `pgbus_concurrency_slots_held` — keys are per-record, so a per-key label would be an unbounded series); and a read-only `pgbus_concurrency` MCP tool returning the same data. The key rows come from a FULL OUTER JOIN, not a LEFT one: a key can have parked jobs and no semaphore (its holder died and the sweep removed the row) or a semaphore and nothing parked, and both shapes matter to an operator. Each row carries two guarded escape hatches. **Release** drops the semaphore and promotes through `Concurrency::BlockedExecution.promote_next`, so it takes each slot through the same guarded upsert an enqueue uses and can never reintroduce the over-admission #460 closed; when the lease is still fresh the confirm says so, because a job is then probably still running and releasing lets another start beside it. **Discard parked** drops a single key's parked jobs, naming the exact count in the confirm, and resolves the bookkeeping those jobs will never resolve themselves — a parked batch child is marked failed so its batch stops waiting and `on_failure` fires (#413), and an `:until_executed` uniqueness lock is released rather than orphaned (#423); each discard logs a warning naming the job and emits `pgbus.blocked_execution_discarded`. There is deliberately no cross-key "discard all parked": not losing work is the whole point of `:block`. Nothing is added to the enqueue or execute path. Refs #461.
|
|
8
8
|
|
|
9
|
+
- **PGMQ: vendored schema v1.13.0, and the upgrade migration now carries upstream's table fixups.** The 1.12.0 → 1.13.0 delta is entirely partitioned-queue work plus one metrics attribute: `pgmq.create_partitioned` gains a `premake` argument (its three-argument form dropped, so pgmq-ruby's three-argument call binds to the new signature with the default), new partitioned queues use `msg_id … GENERATED BY DEFAULT AS IDENTITY` rather than `GENERATED ALWAYS`, and `pgmq.metrics_result` gains `default_partition_length` (NULL for non-partitioned queues, ignored by pgmq-ruby, which reads rows by name). Nothing pgbus calls through `Pgbus::Client` changes, and pgbus creates only partitioned *archives*, whose `msg_id` is a plain column — so no client or pgmq-ruby change. Fresh embedded installs get 1.13.0; existing *embedded* installs: `rake pgbus:pgmq:status`, then `rails generate pgbus:upgrade_pgmq` + `rails db:migrate` (`rails db:migrate:pgbus` on a separate-database install). An `:extension` install is upgraded by PostgreSQL itself (`ALTER EXTENSION pgmq UPDATE`) once the server has the new version packaged — not by this migration, whose function drop refuses to remove extension-owned objects. **New:** because the upgrade works by dropping every pgmq function and composite type and re-running the target schema, it could never carry an upstream change to an existing *table*. The generated migration now reads the version in `pgbus_pgmq_schema_versions` and applies the fixup vendored for each version between it and the target (`lib/pgbus/pgmq_schema/fixups/pgmq_v<VERSION>.sql`, discovered by the same glob as the schema files, via `PgmqSchema.fixups_sql`). This hop's fixup is upstream's own guarded block moving existing partitioned queues to `GENERATED BY DEFAULT`; it selects on `pgmq.meta.is_partitioned`, so it is a no-op unless partitioned queues were created through PGMQ directly. Fixups must be idempotent — an install with no recorded version gets all of them. Closes #459.
|
|
10
|
+
|
|
9
11
|
- **The batches list and a batch's progress panel now refresh themselves.** Batches was the only dashboard list whose `turbo-frame` was missing `data-auto-refresh`, and the batch detail page had no frame at all, so watching a batch drain meant reloading by hand. Both now poll on the existing `web_refresh_interval` (5s, pauses while you interact) like every other list — the batch progress panel is served by `GET /batches/:id?frame=progress`. This reuses the dashboard's turbo-frame polling rather than the SSE stack in `lib/pgbus/web/stream_app.rb`: that is the host-app-facing stream feature, and pointing it at the dashboard would open a PG `LISTEN` connection per viewer to move four integers.
|
|
10
12
|
|
|
11
13
|
### Fixed
|
data/Rakefile
CHANGED
|
@@ -16,7 +16,7 @@ require "rubocop/rake_task"
|
|
|
16
16
|
# on the unresolvable gem inheritance. Passing explicit paths stops the
|
|
17
17
|
# discovery. docs/ lints itself in the Docs site workflow. (Same fix as docs-kit.)
|
|
18
18
|
RuboCop::RakeTask.new do |task|
|
|
19
|
-
task.patterns = %w[app benchmarks config lib spec Gemfile Rakefile pgbus.gemspec]
|
|
19
|
+
task.patterns = %w[app benchmarks config lib rakelib spec Gemfile Rakefile pgbus.gemspec]
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
namespace :bench do
|
|
@@ -164,173 +164,8 @@ task :build do
|
|
|
164
164
|
sh("rm -rf /tmp/gem-verify #{gem_file}")
|
|
165
165
|
end
|
|
166
166
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
require_relative "lib/pgbus/version"
|
|
170
|
-
|
|
171
|
-
def info(msg) = puts "\e[34m→\e[0m #{msg}"
|
|
172
|
-
def success(msg) = puts "\e[32m✓\e[0m #{msg}"
|
|
173
|
-
def skip(msg) = puts "\e[33m⊘\e[0m #{msg} \e[33m(skipped)\e[0m"
|
|
174
|
-
def warn(msg) = puts "\e[33m⚠\e[0m #{msg}"
|
|
175
|
-
def error(msg) = puts "\e[31m✗\e[0m #{msg}"
|
|
176
|
-
def header(msg) = puts "\n\e[1;36m#{msg}\e[0m\n#{"─" * msg.length}"
|
|
177
|
-
|
|
178
|
-
new_version = args[:version]
|
|
179
|
-
abort "\e[31mUsage: rake release[X.Y.Z] or rake release[X.Y.Z,force]\e[0m" unless new_version
|
|
180
|
-
|
|
181
|
-
force = args[:force]&.to_s&.downcase == "force"
|
|
182
|
-
|
|
183
|
-
dirty = `git status --porcelain`.strip
|
|
184
|
-
abort "\e[31mAborting: working directory is not clean.\e[0m\n#{dirty}" unless dirty.empty?
|
|
185
|
-
|
|
186
|
-
current = Pgbus::VERSION
|
|
187
|
-
prerelease = new_version.match?(/alpha|beta|rc|pre/) || new_version == "pre"
|
|
188
|
-
|
|
189
|
-
if new_version == "pre"
|
|
190
|
-
new_version = current
|
|
191
|
-
prerelease = true
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
tag = "v#{new_version}"
|
|
195
|
-
version_file = "lib/pgbus/version.rb"
|
|
196
|
-
|
|
197
|
-
title = "Release #{tag}"
|
|
198
|
-
title += " (force)" if force
|
|
199
|
-
header title
|
|
200
|
-
info "Current version: #{current}"
|
|
201
|
-
info "New version: #{new_version}"
|
|
202
|
-
info "Pre-release: #{prerelease}"
|
|
203
|
-
|
|
204
|
-
# Step 0: Force cleanup — delete existing release and tag
|
|
205
|
-
if force
|
|
206
|
-
header "Force cleanup"
|
|
207
|
-
if system("gh release view #{tag} >/dev/null 2>&1")
|
|
208
|
-
sh("gh release delete #{tag} --yes --cleanup-tag")
|
|
209
|
-
success "Deleted release and remote tag #{tag}"
|
|
210
|
-
else
|
|
211
|
-
skip "No release #{tag} to delete"
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
if system("git rev-parse #{tag} >/dev/null 2>&1")
|
|
215
|
-
sh("git tag -d #{tag}")
|
|
216
|
-
success "Deleted local tag #{tag}"
|
|
217
|
-
else
|
|
218
|
-
skip "No local tag #{tag} to delete"
|
|
219
|
-
end
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
# Step 1: Update version file
|
|
223
|
-
header "Version"
|
|
224
|
-
if new_version == current
|
|
225
|
-
skip "Version already #{new_version}"
|
|
226
|
-
else
|
|
227
|
-
content = File.read(version_file)
|
|
228
|
-
content.sub!(/VERSION = ".*"/, "VERSION = \"#{new_version}\"")
|
|
229
|
-
File.write(version_file, content)
|
|
230
|
-
success "Updated #{version_file}"
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
# Step 1b: Regenerate the frozen lockfiles that pin the pgbus path gem, so the
|
|
234
|
-
# bump ships with them in sync. These are installed with `--frozen`/deployment
|
|
235
|
-
# in CI, so if they still name the OLD version they instant-fail (the root
|
|
236
|
-
# Gemfile.lock on every main-Gemfile leg AND release.yml's own `bundle install`,
|
|
237
|
-
# the Rails 7.1 leg with exit 16, and docs-CI on any docs change). Regenerating
|
|
238
|
-
# here keeps the version-pin drift out of the release commit instead of
|
|
239
|
-
# surfacing on the next PR — or, worse, in the Release workflow itself.
|
|
240
|
-
header "Frozen lockfiles"
|
|
241
|
-
# The ONLY thing a version bump changes in these frozen lockfiles is the pgbus
|
|
242
|
-
# path-gem pin — so bump exactly that line, in place, with a string edit.
|
|
243
|
-
#
|
|
244
|
-
# We deliberately do NOT run `bundle lock` here: a full re-resolve trips over
|
|
245
|
-
# constraints that have nothing to do with pgbus. Concretely, docs/Gemfile.lock
|
|
246
|
-
# carries a broad PLATFORMS list (…-gnu / …-musl / arm-linux) for which a
|
|
247
|
-
# platform gem like `thruster` ships no variant, so `bundle lock` fails with
|
|
248
|
-
# "Could not find gems matching 'thruster' valid for all resolution platforms"
|
|
249
|
-
# on any machine whose cache doesn't already hold those exact gems — aborting
|
|
250
|
-
# the release. `bundle lock --local` was even worse (wrong-file write + no
|
|
251
|
-
# fetch). A targeted pin edit sidesteps all of it, is deterministic on any
|
|
252
|
-
# machine, and produces the minimal 2-line diff (the PATH spec + the
|
|
253
|
-
# DEPENDENCIES pin). See #338/#341 and the surgical-bump fix.
|
|
254
|
-
frozen_lockfiles = %w[Gemfile.lock gemfiles/rails_7_1.gemfile.lock docs/Gemfile.lock]
|
|
255
|
-
regenerated_lockfiles = []
|
|
256
|
-
frozen_lockfiles.each do |lockfile|
|
|
257
|
-
unless File.exist?(lockfile)
|
|
258
|
-
skip "#{lockfile} not present"
|
|
259
|
-
next
|
|
260
|
-
end
|
|
261
|
-
|
|
262
|
-
content = File.read(lockfile)
|
|
263
|
-
# Matches both the PATH-source spec (" pgbus (X.Y.Z)") and the
|
|
264
|
-
# DEPENDENCIES pin (" pgbus (X.Y.Z)"), leaving everything else untouched.
|
|
265
|
-
bumped = content.gsub(/^(\s+pgbus) \([^)]*\)$/, "\\1 (#{new_version})")
|
|
266
|
-
|
|
267
|
-
if bumped == content
|
|
268
|
-
skip "#{lockfile} — no pgbus pin to bump"
|
|
269
|
-
next
|
|
270
|
-
end
|
|
271
|
-
|
|
272
|
-
File.write(lockfile, bumped)
|
|
273
|
-
regenerated_lockfiles << lockfile
|
|
274
|
-
success "Bumped pgbus pin in #{lockfile}"
|
|
275
|
-
end
|
|
276
|
-
|
|
277
|
-
# Step 2: Verify gem builds cleanly
|
|
278
|
-
header "Build verification"
|
|
279
|
-
sh("gem build pgbus.gemspec --strict")
|
|
280
|
-
sh("rm -f pgbus-*.gem")
|
|
281
|
-
success "Gem builds cleanly"
|
|
282
|
-
|
|
283
|
-
# Step 3: Commit version bump (+ any re-synced lockfiles)
|
|
284
|
-
header "Git commit"
|
|
285
|
-
paths_to_stage = [version_file, *regenerated_lockfiles]
|
|
286
|
-
staged_changes = paths_to_stage.any? do |path|
|
|
287
|
-
!`git diff #{path}`.strip.empty? || !`git diff --cached #{path}`.strip.empty?
|
|
288
|
-
end
|
|
289
|
-
if staged_changes
|
|
290
|
-
paths_to_stage.each { |path| sh("git add #{path}") }
|
|
291
|
-
sh("git commit -m 'chore: bump version to #{new_version}'")
|
|
292
|
-
success "Committed version bump"
|
|
293
|
-
else
|
|
294
|
-
skip "No version change to commit"
|
|
295
|
-
end
|
|
296
|
-
|
|
297
|
-
# Step 4: Push to origin
|
|
298
|
-
header "Git push"
|
|
299
|
-
local_sha = `git rev-parse HEAD`.strip
|
|
300
|
-
remote_sha = `git rev-parse origin/main 2>/dev/null`.strip
|
|
301
|
-
if local_sha == remote_sha
|
|
302
|
-
skip "origin/main already at #{local_sha[0..6]}"
|
|
303
|
-
else
|
|
304
|
-
sh("git push origin main")
|
|
305
|
-
success "Pushed to origin/main"
|
|
306
|
-
end
|
|
307
|
-
|
|
308
|
-
# Step 5: Create release
|
|
309
|
-
header "Release"
|
|
310
|
-
tag_exists = system("git rev-parse #{tag} >/dev/null 2>&1")
|
|
311
|
-
release_exists = system("gh release view #{tag} >/dev/null 2>&1")
|
|
312
|
-
|
|
313
|
-
if release_exists
|
|
314
|
-
skip "Release #{tag} already exists (use force to re-create)"
|
|
315
|
-
elsif tag_exists
|
|
316
|
-
info "Tag #{tag} exists, creating release from it"
|
|
317
|
-
pre_flag = prerelease ? "--prerelease" : ""
|
|
318
|
-
sh("gh release create #{tag} --generate-notes #{pre_flag}".strip)
|
|
319
|
-
success "Release #{tag} created from existing tag"
|
|
320
|
-
else
|
|
321
|
-
pre_flag = prerelease ? "--prerelease" : ""
|
|
322
|
-
sh("gh release create #{tag} --generate-notes --target main #{pre_flag}".strip)
|
|
323
|
-
success "Release #{tag} created"
|
|
324
|
-
end
|
|
325
|
-
|
|
326
|
-
puts ""
|
|
327
|
-
success "\e[1mRelease #{tag} complete!\e[0m CI will handle the rest:"
|
|
328
|
-
puts " • Run tests"
|
|
329
|
-
puts " • Build + verify gem"
|
|
330
|
-
puts " • Sign with Sigstore"
|
|
331
|
-
puts " • Publish to RubyGems"
|
|
332
|
-
puts " • Upload assets to the release"
|
|
333
|
-
end
|
|
167
|
+
# `rake release[X.Y.Z]` lives in rakelib/release.rake (shared across the
|
|
168
|
+
# zoolutions gems); `bin/release` is its interactive front door.
|
|
334
169
|
|
|
335
170
|
namespace :frontend do
|
|
336
171
|
# app/frontend/pgbus/style.css is a committed build artifact — the engine ships
|
|
@@ -8,6 +8,16 @@ class UpgradePgmqToV<%= target_version_slug.camelize %> < ActiveRecord::Migratio
|
|
|
8
8
|
# This uses the vendored SQL which doesn't require the pgmq extension.
|
|
9
9
|
execute Pgbus::PgmqSchema.install_sql("<%= target_version %>")
|
|
10
10
|
|
|
11
|
+
# Apply the table fixups upstream ships for every hop between the
|
|
12
|
+
# version this database records and the target. Dropping and re-creating
|
|
13
|
+
# the functions cannot carry a change to an existing TABLE (1.13.0 moves
|
|
14
|
+
# partitioned queues' msg_id to GENERATED BY DEFAULT), so those ride
|
|
15
|
+
# here. Runs after install_sql because the fixups call
|
|
16
|
+
# pgmq.format_table_name, which the drop step removed. Each fixup is
|
|
17
|
+
# idempotent, so an install with no recorded version gets them all.
|
|
18
|
+
fixups = Pgbus::PgmqSchema.fixups_sql(after: installed_pgmq_version, upto: "<%= target_version %>")
|
|
19
|
+
execute fixups unless fixups.empty?
|
|
20
|
+
|
|
11
21
|
# Re-install the per-queue NOTIFY insert triggers the function drop
|
|
12
22
|
# cascaded away (pgmq.notify_insert_throttle records which queues had
|
|
13
23
|
# them and at what throttle; enable_notify_insert is idempotent).
|
|
@@ -34,4 +44,16 @@ class UpgradePgmqToV<%= target_version_slug.camelize %> < ActiveRecord::Migratio
|
|
|
34
44
|
"PGMQ schema downgrade is not supported. " \
|
|
35
45
|
"Restore from a database backup if needed."
|
|
36
46
|
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# The version this database last recorded, or nil when it has never been
|
|
51
|
+
# tracked (a pre-tracking install, or one bootstrapped by hand).
|
|
52
|
+
def installed_pgmq_version
|
|
53
|
+
return nil unless connection.table_exists?("pgbus_pgmq_schema_versions")
|
|
54
|
+
|
|
55
|
+
connection.select_value(
|
|
56
|
+
"SELECT version FROM pgbus_pgmq_schema_versions ORDER BY installed_at DESC LIMIT 1"
|
|
57
|
+
)
|
|
58
|
+
end
|
|
37
59
|
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
-- PGMQ 1.13.0 table fixup.
|
|
2
|
+
--
|
|
3
|
+
-- Adapted from the upstream migration
|
|
4
|
+
-- pgmq-extension/sql/pgmq--1.12.0--1.13.0.sql at tag v1.13.0
|
|
5
|
+
-- (blob a380ee56e856c5799dcd1486342ce910b96da64e), which is the only part of
|
|
6
|
+
-- that file pgbus's upgrade cannot already reproduce: dropping every function
|
|
7
|
+
-- and composite type and re-running the target schema recreates functions and
|
|
8
|
+
-- types, but never touches an existing table.
|
|
9
|
+
--
|
|
10
|
+
-- One deliberate deviation from upstream: the guard resolves the queue table by
|
|
11
|
+
-- OID via to_regclass instead of matching information_schema.columns.table_name
|
|
12
|
+
-- against the formatted name. See the comment on the guard for why.
|
|
13
|
+
--
|
|
14
|
+
-- Every file in this directory MUST be idempotent and self-guarded. The
|
|
15
|
+
-- upgrade migration applies each fixup between the recorded installed version
|
|
16
|
+
-- and the target, and an install whose version was never recorded gets all of
|
|
17
|
+
-- them, so a fixup can be replayed against a database that already has it.
|
|
18
|
+
|
|
19
|
+
-- Partitioned queues: msg_id becomes GENERATED BY DEFAULT AS IDENTITY.
|
|
20
|
+
-- pg_partman's partition_data_* tooling moves rows out of the default
|
|
21
|
+
-- partition by inserting them with their existing msg_id, which
|
|
22
|
+
-- GENERATED ALWAYS refuses. pgmq's own writers never supply msg_id, so this
|
|
23
|
+
-- changes nothing for them.
|
|
24
|
+
DO $$
|
|
25
|
+
DECLARE
|
|
26
|
+
queue_record RECORD;
|
|
27
|
+
qtable TEXT;
|
|
28
|
+
BEGIN
|
|
29
|
+
FOR queue_record IN SELECT queue_name FROM pgmq.meta WHERE is_partitioned LOOP
|
|
30
|
+
qtable := pgmq.format_table_name(queue_record.queue_name, 'q');
|
|
31
|
+
|
|
32
|
+
-- Resolved by OID rather than by comparing qtable to a catalog name:
|
|
33
|
+
-- validate_queue_name caps the queue at 47 *characters*, but PostgreSQL
|
|
34
|
+
-- truncates identifiers at 63 *bytes*, so a multibyte name can produce a
|
|
35
|
+
-- qtable string that never equals the stored relation name. A name
|
|
36
|
+
-- comparison would silently skip such a queue and leave msg_id at
|
|
37
|
+
-- ALWAYS, which is the exact failure this fixup exists to prevent.
|
|
38
|
+
IF EXISTS (
|
|
39
|
+
SELECT 1
|
|
40
|
+
FROM pg_catalog.pg_attribute
|
|
41
|
+
WHERE attrelid = pg_catalog.to_regclass(FORMAT('pgmq.%I', qtable))
|
|
42
|
+
AND attname = 'msg_id'
|
|
43
|
+
AND attidentity = 'a'
|
|
44
|
+
) THEN
|
|
45
|
+
EXECUTE FORMAT('ALTER TABLE pgmq.%I ALTER COLUMN msg_id SET GENERATED BY DEFAULT', qtable);
|
|
46
|
+
END IF;
|
|
47
|
+
END LOOP;
|
|
48
|
+
END;
|
|
49
|
+
$$;
|