pgbus 0.5.0 → 0.5.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: 903778ddd22823971fc4d9d888d10b657597c542e49d365fb7ad1863167d4f6c
4
- data.tar.gz: f652a6675bfbc206d4ab3d43a44a704fbdd0247f57cae1252ef310772ec6e1bd
3
+ metadata.gz: bb5b86968b21bebedf9b33d6d69338b86f7dfb5304b5efa714b270bc8128fa10
4
+ data.tar.gz: f5d6118149ebcd7c7f773ab399fbdc97079883dd895339c5918c156fc416bbfe
5
5
  SHA512:
6
- metadata.gz: 15fcf091f8b8de0823ff2a6f807fcb9fb3339700994221ded4dcbf22c942f01a636e0eaa53d6be9b44544113a281bf15d299e146b32b30b2e604b8fdd4c36447
7
- data.tar.gz: 885ca1ccbdf1f65a83dde14cdc25fd6c9f63993c0692c1c6b088696d27b593bd3a81d27d7f5bc5fccb5a368882af51e8bd1c0b4bfbc0bba27a8ca567bf297405
6
+ metadata.gz: 7649ee8dfd9d9ff155e510c014a28fb070d79357d34ed4e8a8c0369c502c90b806d01775a0b01f480709fa9399d7ecc3edddb7d4e092813d42014d412833d9ce
7
+ data.tar.gz: 756c31dd3fd970dca11c78cbb17af8b74e798cd42e3782b9993e4081982cc340a1224b6c0b829c259ea6d13abcec035510c2b4e0e6b8c39f73a34b1b9cd2bc48
data/CHANGELOG.md CHANGED
@@ -1,3 +1,18 @@
1
+ ## [0.5.1] - 2026-04-08
2
+
3
+ ### Fixed
4
+
5
+ - **Capsule DSL: anonymous duplicate capsules are now allowed.** Configurations like `c.workers = "*: 3; *: 3; *: 3; *: 3; *: 3"` (the legacy YAML pattern of 5 forks × 3 threads, all reading every queue) were rejected at boot in 0.5.0 with `Pgbus::Configuration::CapsuleDSL::ParseError: wildcard '*' appears in two capsules`. PGMQ tolerates multiple processes reading the same queue natively (`FOR UPDATE SKIP LOCKED`), and this is the canonical way to scale CPU parallelism across forks, so the rejection was wrong.
6
+
7
+ The fix introduces a "named vs anonymous" distinction:
8
+
9
+ - The string DSL parser is now purely syntactic — it no longer enforces overlap rules.
10
+ - `Pgbus::Configuration#workers=` auto-assigns `:name` only to capsules whose first queue would yield a *unique* name AND is not the bare wildcard. Wildcards stay anonymous; collision-prone first-queues stay anonymous.
11
+ - `Pgbus::Configuration#validate_no_queue_overlap!` (called by `c.capsule :name, ...`) now only checks against existing **named** capsules. Anonymous capsules can overlap freely with each other and with named capsules.
12
+ - Net result: `"*: 3; *: 3; *: 3"` produces 3 anonymous capsules (3 forks), `"critical: 5; default: 10"` produces 2 named capsules (CLI `--capsule critical` still works), and named-vs-named overlap is still rejected as before.
13
+
14
+ No changes required to user configuration — legacy YAML patterns and the modern DSL both work as documented.
15
+
1
16
  ## [Unreleased]
2
17
 
3
18
  ### Breaking Changes
@@ -62,9 +62,12 @@ module Pgbus
62
62
  validate_input_type!
63
63
  validate_input_not_empty!
64
64
 
65
- capsules = split_capsules(@input).map { |segment| parse_capsule(segment) }
66
- validate_no_duplicate_queues_across_capsules!(capsules)
67
- capsules
65
+ # Pure tokenization: split, parse each capsule, return them in order.
66
+ # Cross-capsule overlap rules live in Pgbus::Configuration#workers=
67
+ # because they depend on whether the resulting capsules are named or
68
+ # anonymous, and naming is a Configuration concern (not a parser one).
69
+ # Within-capsule duplicate-queue checks still happen in parse_capsule.
70
+ split_capsules(@input).map { |segment| parse_capsule(segment) }
68
71
  end
69
72
 
70
73
  private
@@ -168,23 +171,6 @@ module Pgbus
168
171
  seen[q] = true
169
172
  end
170
173
  end
171
-
172
- def validate_no_duplicate_queues_across_capsules!(capsules)
173
- return if capsules.size < 2
174
-
175
- seen = {}
176
- capsules.each_with_index do |capsule, idx|
177
- capsule[:queues].each do |q|
178
- if seen[q]
179
- label = (q == WILDCARD ? "wildcard '*'" : "queue #{q.inspect}")
180
- raise ParseError,
181
- "#{label} appears in two capsules (positions #{seen[q]} and #{idx + 1}) " \
182
- "in #{@input.inspect} — each queue can only be assigned to one capsule"
183
- end
184
- seen[q] = idx + 1
185
- end
186
- end
187
- end
188
174
  end
189
175
  end
190
176
  end
@@ -230,12 +230,34 @@ module Pgbus
230
230
  # dispatcher-only processes).
231
231
  #
232
232
  # Raises ArgumentError for any other type.
233
+ #
234
+ # NAMING SEMANTICS for the String form:
235
+ #
236
+ # The parser produces anonymous capsules (no :name). The setter then
237
+ # auto-assigns a :name to capsules whose first queue would yield a
238
+ # *unique* name across the parsed list AND is not the bare wildcard
239
+ # (`*`). Anything else stays anonymous.
240
+ #
241
+ # "critical: 5; default: 10" -> two NAMED capsules ("critical", "default")
242
+ # "*: 5" -> one anonymous capsule (wildcard never names)
243
+ # "*: 3; *: 3; *: 3" -> three anonymous capsules — legal,
244
+ # represents "3 forks all reading every
245
+ # queue", restoring the legacy YAML
246
+ # `5 × {queues: ["*"], threads: 3}` shape
247
+ # "default: 5; default: 3" -> two anonymous capsules — same logic
248
+ #
249
+ # The point of the carve-out is the legacy "I want N forks of the same
250
+ # worker pool" pattern: it must keep working since PGMQ tolerates it
251
+ # natively (multiple processes reading the same queue with FOR UPDATE
252
+ # SKIP LOCKED). The CLI's --capsule selector only matches NAMED
253
+ # capsules, so anonymous duplicates can't be ambiguously addressed.
233
254
  def workers=(value)
234
255
  @workers = case value
235
256
  when nil
236
257
  nil
237
258
  when String
238
- CapsuleDSL.parse(value).map { |entry| entry.merge(name: entry[:queues].first.to_s) }
259
+ parsed = CapsuleDSL.parse(value)
260
+ assign_auto_names(parsed)
239
261
  when Array
240
262
  value
241
263
  else
@@ -445,33 +467,58 @@ module Pgbus
445
467
  raw&.to_s
446
468
  end
447
469
 
448
- # Validates that no queue in +new_queues+ would overlap with any
449
- # existing capsule. The wildcard '*' counts as overlapping with EVERY
450
- # other queue (and vice versa) because at runtime '*' is expanded to
451
- # all known queues. Raises ArgumentError on overlap.
470
+ # Auto-assign :name to parsed capsules where the first queue token would
471
+ # yield a unique name and is not the bare wildcard. See the long comment
472
+ # on +workers=+ for the why. Returns the same array with :name merged in
473
+ # where applicable.
474
+ def assign_auto_names(parsed_capsules)
475
+ first_queue_counts = parsed_capsules.each_with_object(Hash.new(0)) do |capsule, h|
476
+ h[capsule[:queues].first] += 1
477
+ end
478
+
479
+ parsed_capsules.map do |capsule|
480
+ first = capsule[:queues].first
481
+ nameable = first != CapsuleDSL::WILDCARD && first_queue_counts[first] == 1
482
+ nameable ? capsule.merge(name: first.to_s) : capsule
483
+ end
484
+ end
485
+
486
+ # Validates that the new capsule (added via +c.capsule :name, ...+) does
487
+ # not overlap with any existing NAMED capsule. Anonymous capsules (parsed
488
+ # from the string DSL with auto-naming skipped, e.g. wildcards or
489
+ # would-collide first-queues) are intentionally invisible here — they
490
+ # represent "N forks of the same pool" and are allowed to overlap with
491
+ # each other and with named capsules.
492
+ #
493
+ # The wildcard '*' counts as overlapping with EVERY other queue (and
494
+ # vice versa) because at runtime '*' is expanded to all known queues.
495
+ # Raises ArgumentError on overlap.
452
496
  def validate_no_queue_overlap!(new_queues)
453
- existing = (@workers || []).flat_map { |c| c[:queues] || c["queues"] || [] }
454
- return if existing.empty?
497
+ existing_named = (@workers || []).select { |c| capsule_name(c) }
498
+ return if existing_named.empty?
499
+
500
+ existing_queues = existing_named.flat_map { |c| c[:queues] || c["queues"] || [] }
501
+ return if existing_queues.empty?
455
502
 
456
- if existing.include?(CapsuleDSL::WILDCARD)
503
+ if existing_queues.include?(CapsuleDSL::WILDCARD)
457
504
  raise ArgumentError,
458
- "an existing capsule already uses '*' (matches every queue) — " \
505
+ "an existing named capsule already uses '*' (matches every queue) — " \
459
506
  "the new capsule's queues #{new_queues.inspect} would overlap with it"
460
507
  end
461
508
 
462
509
  if new_queues.include?(CapsuleDSL::WILDCARD)
463
510
  raise ArgumentError,
464
- "the new capsule uses '*' (matches every queue) but other capsules " \
465
- "are already defined with queues #{existing.inspect} — " \
511
+ "the new capsule uses '*' (matches every queue) but other named capsules " \
512
+ "are already defined with queues #{existing_queues.inspect} — " \
466
513
  "the wildcard would overlap with all of them"
467
514
  end
468
515
 
469
- conflict = new_queues.find { |q| existing.include?(q) }
516
+ conflict = new_queues.find { |q| existing_queues.include?(q) }
470
517
  return unless conflict
471
518
 
472
519
  raise ArgumentError,
473
- "queue #{conflict.inspect} is already assigned to another capsule — " \
474
- "each queue can only belong to one capsule"
520
+ "queue #{conflict.inspect} is already assigned to another named capsule — " \
521
+ "named capsules cannot share queues"
475
522
  end
476
523
 
477
524
  def sum_thread_counts(entries, default_threads:, group:)
data/lib/pgbus/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pgbus
4
- VERSION = "0.5.0"
4
+ VERSION = "0.5.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pgbus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson