pgbus 0.2.3 → 0.2.5

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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/app/controllers/pgbus/application_controller.rb +14 -2
  4. data/app/models/pgbus/application_record.rb +4 -1
  5. data/app/models/pgbus/batch_entry.rb +1 -1
  6. data/app/models/pgbus/blocked_execution.rb +1 -1
  7. data/app/models/pgbus/job_lock.rb +1 -1
  8. data/app/models/pgbus/job_stat.rb +1 -1
  9. data/app/models/pgbus/outbox_entry.rb +1 -1
  10. data/app/models/pgbus/process_entry.rb +1 -1
  11. data/app/models/pgbus/processed_event.rb +1 -1
  12. data/app/models/pgbus/queue_state.rb +1 -1
  13. data/app/models/pgbus/recurring_execution.rb +1 -1
  14. data/app/models/pgbus/recurring_task.rb +1 -1
  15. data/app/models/pgbus/semaphore.rb +1 -1
  16. data/lib/active_job/queue_adapters/pgbus_adapter.rb +7 -0
  17. data/lib/pgbus/active_job/executor.rb +1 -1
  18. data/lib/pgbus/bus_record.rb +16 -0
  19. data/lib/pgbus/client.rb +1 -1
  20. data/lib/pgbus/config_loader.rb +1 -1
  21. data/lib/pgbus/configuration.rb +7 -4
  22. data/lib/pgbus/engine.rb +1 -1
  23. data/lib/pgbus/event_bus/handler.rb +1 -1
  24. data/lib/pgbus/process/consumer_priority.rb +1 -1
  25. data/lib/pgbus/process/dispatcher.rb +1 -1
  26. data/lib/pgbus/process/queue_lock.rb +1 -1
  27. data/lib/pgbus/process/supervisor.rb +3 -3
  28. data/lib/pgbus/process/worker.rb +4 -1
  29. data/lib/pgbus/queue_name_validator.rb +45 -0
  30. data/lib/pgbus/recurring/config_loader.rb +1 -1
  31. data/lib/pgbus/serializer.rb +28 -1
  32. data/lib/pgbus/version.rb +1 -1
  33. data/lib/pgbus/web/authentication.rb +20 -1
  34. data/lib/pgbus/web/data_source.rb +2 -5
  35. data/lib/pgbus.rb +12 -0
  36. data/lib/tasks/pgbus_pgmq.rake +1 -1
  37. metadata +17 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 85f2e8748cf70bb74cb736b23e3e0288be5ca0b946b0eb80c08e1d28650e98c0
4
- data.tar.gz: d1fd675bb9d9a5beb4607f8e0da684c606d096a835948abf60034bd601cdd7f2
3
+ metadata.gz: cfd2ea5ccec3e3715dbcf2dd409a652527cf1d9e18c60fe4d5797fd5cf5be6ec
4
+ data.tar.gz: 6983d4977aeb792ba0857884752a8b662a15164cde8cd6ee3bb5406d95d7f225
5
5
  SHA512:
6
- metadata.gz: 99b168d4276ef5149effdbf7d18955c542b9f8507a67d022381a61c7c36cacdf58ab414e166e528495e244313ef12d10e054119361bbf77ca2e6cc260f6d1982
7
- data.tar.gz: 606324c33ddd072b24135c91314258fc2c162e54b6610ec813457cae2b4df3ba5c0391fc5dbd8a1ff6e13f14032968320b41b9a1d9450d0b5fbe2d66686f2c9e
6
+ metadata.gz: c6f3e19f1e3fe2d5eae02fe52dcd2878f5bbcb13cd7387a41b3902ad91cb8c20ce2d1e3c606488bdce1c901126d1192f7a34747e8746c397d016d0c2541a8828
7
+ data.tar.gz: da5ed5a1a2012d757547e3e2f1f48586a84e7d096df91a793d9ba18f76da881e52dab650d4fb3c605c4da6de06b1e8b58b0d11c99bd90e30ef64b3c368c44093
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ### Breaking Changes
4
+
5
+ - **Queue names must be alphanumeric and underscores only.** Queue names containing dashes (e.g., `my-app-queue`) will now raise `ArgumentError`. Rename to underscored form (e.g., `my_app_queue`) before upgrading. This restriction prevents SQL injection via PGMQ queue identifiers, which are interpolated into table names and cannot be parameterized.
6
+
7
+ ### Security
8
+
9
+ - Add `bundler-audit` to CI for dependency vulnerability scanning
10
+ - Add `QueueNameValidator` to enforce strict queue name validation (alphanumeric + underscores, 61 char max)
11
+ - Add `config.allowed_global_id_models` to restrict which models can be deserialized from event payloads
12
+ - Add security headers to dashboard (X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy)
13
+ - Warn when dashboard `web_auth` is unconfigured
14
+ - Add `globalid` as an explicit runtime dependency (was used but only transitively available via activejob)
15
+
3
16
  ## [0.1.0] - 2026-03-30
4
17
 
5
18
  - Initial release
@@ -6,6 +6,7 @@ module Pgbus
6
6
 
7
7
  protect_from_forgery with: :exception
8
8
  before_action :set_locale
9
+ after_action :set_security_headers
9
10
 
10
11
  layout "pgbus/application"
11
12
 
@@ -21,6 +22,14 @@ module Pgbus
21
22
 
22
23
  private
23
24
 
25
+ def set_security_headers
26
+ response.headers["X-Frame-Options"] = "SAMEORIGIN"
27
+ response.headers["X-Content-Type-Options"] = "nosniff"
28
+ response.headers["X-XSS-Protection"] = "0"
29
+ response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
30
+ response.headers["Permissions-Policy"] = "camera=(), microphone=(), geolocation=()"
31
+ end
32
+
24
33
  def set_locale
25
34
  I18n.locale = extract_locale || I18n.default_locale
26
35
  end
@@ -48,8 +57,11 @@ module Pgbus
48
57
  end
49
58
 
50
59
  def available_locales
51
- @available_locales ||= Dir[Pgbus::Engine.root.join("config", "locales", "*.yml")]
52
- .map { |f| File.basename(f, ".yml").to_sym }
60
+ @available_locales ||= begin
61
+ pgbus_locales = Dir[Pgbus::Engine.root.join("config", "locales", "*.yml")]
62
+ .map { |f| File.basename(f, ".yml").to_sym }
63
+ pgbus_locales & I18n.available_locales
64
+ end
53
65
  end
54
66
  helper_method :available_locales
55
67
 
@@ -1,7 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pgbus
4
- class ApplicationRecord < ActiveRecord::Base
4
+ # Backward-compatible alias — host apps that subclassed
5
+ # Pgbus::ApplicationRecord will continue to work.
6
+ # New code should inherit from Pgbus::BusRecord directly.
7
+ class ApplicationRecord < BusRecord
5
8
  self.abstract_class = true
6
9
  end
7
10
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pgbus
4
- class BatchEntry < ApplicationRecord
4
+ class BatchEntry < BusRecord
5
5
  self.table_name = "pgbus_batches"
6
6
 
7
7
  COUNTER_COLUMNS = %w[completed_jobs discarded_jobs].freeze
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pgbus
4
- class BlockedExecution < ApplicationRecord
4
+ class BlockedExecution < BusRecord
5
5
  self.table_name = "pgbus_blocked_executions"
6
6
 
7
7
  scope :for_key, ->(key) { where(concurrency_key: key) }
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pgbus
4
- class JobLock < Pgbus::ApplicationRecord
4
+ class JobLock < BusRecord
5
5
  self.table_name = "pgbus_job_locks"
6
6
 
7
7
  # States:
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pgbus
4
- class JobStat < Pgbus::ApplicationRecord
4
+ class JobStat < BusRecord
5
5
  self.table_name = "pgbus_job_stats"
6
6
 
7
7
  scope :since, ->(time) { where("created_at >= ?", time) }
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pgbus
4
- class OutboxEntry < Pgbus::ApplicationRecord
4
+ class OutboxEntry < BusRecord
5
5
  self.table_name = "pgbus_outbox_entries"
6
6
 
7
7
  scope :unpublished, -> { where(published_at: nil) }
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pgbus
4
- class ProcessEntry < ApplicationRecord
4
+ class ProcessEntry < BusRecord
5
5
  self.table_name = "pgbus_processes"
6
6
 
7
7
  scope :stale, ->(threshold) { where("last_heartbeat_at < ?", threshold) }
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pgbus
4
- class ProcessedEvent < ApplicationRecord
4
+ class ProcessedEvent < BusRecord
5
5
  self.table_name = "pgbus_processed_events"
6
6
 
7
7
  scope :expired, ->(before) { where("processed_at < ?", before) }
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pgbus
4
- class QueueState < Pgbus::ApplicationRecord
4
+ class QueueState < BusRecord
5
5
  self.table_name = "pgbus_queue_states"
6
6
 
7
7
  scope :paused, -> { where(paused: true) }
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pgbus
4
- class RecurringExecution < ApplicationRecord
4
+ class RecurringExecution < BusRecord
5
5
  self.table_name = "pgbus_recurring_executions"
6
6
 
7
7
  validates :task_key, presence: true
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pgbus
4
- class RecurringTask < ApplicationRecord
4
+ class RecurringTask < BusRecord
5
5
  self.table_name = "pgbus_recurring_tasks"
6
6
 
7
7
  validates :key, presence: true, uniqueness: true
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pgbus
4
- class Semaphore < ApplicationRecord
4
+ class Semaphore < BusRecord
5
5
  self.table_name = "pgbus_semaphores"
6
6
 
7
7
  scope :expired, ->(now = Time.current) { where("expires_at < ? OR value <= 0", now) }
@@ -19,6 +19,13 @@ module ActiveJob
19
19
  true
20
20
  end
21
21
 
22
+ # Called by ActiveJob::Continuation (Rails 8.1+) at each checkpoint.
23
+ # When true, continuable jobs save their cursor and re-enqueue
24
+ # themselves so the worker can shut down gracefully.
25
+ def stopping?
26
+ Pgbus.stopping
27
+ end
28
+
22
29
  private
23
30
 
24
31
  def adapter
@@ -80,7 +80,7 @@ module Pgbus
80
80
  private
81
81
 
82
82
  def execute_job(job)
83
- if defined?(Rails) && Rails.application
83
+ if defined?(Rails) && Rails.respond_to?(:application) && Rails.application
84
84
  Rails.application.executor.wrap { job.perform_now }
85
85
  else
86
86
  job.perform_now
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+
5
+ module Pgbus
6
+ # Base class for all Pgbus ActiveRecord models.
7
+ #
8
+ # Lives in lib/pgbus/ so the main Zeitwerk gem loader picks it up
9
+ # regardless of Rails engine boot order. This avoids the NameError
10
+ # that occurs when a host app uses selective railtie requires
11
+ # (require "rails" + individual railties instead of require "rails/all")
12
+ # and the engine's app/models path isn't registered yet.
13
+ class BusRecord < ActiveRecord::Base
14
+ self.abstract_class = true
15
+ end
16
+ end
data/lib/pgbus/client.rb CHANGED
@@ -216,7 +216,7 @@ module Pgbus
216
216
 
217
217
  def purge_archive(queue_name, older_than:, batch_size: 1000)
218
218
  full_name = config.queue_name(queue_name)
219
- sanitized = full_name.gsub(/[^a-zA-Z0-9_]/, "")
219
+ sanitized = QueueNameValidator.sanitize!(full_name)
220
220
  total = 0
221
221
 
222
222
  sql = "DELETE FROM pgmq.a_#{sanitized} " \
@@ -8,7 +8,7 @@ module Pgbus
8
8
  module_function
9
9
 
10
10
  def load(path, env: nil)
11
- env ||= defined?(Rails) ? Rails.env : ENV.fetch("PGBUS_ENV", "development")
11
+ env ||= (defined?(Rails) && Rails.respond_to?(:env) && Rails.env) || ENV.fetch("PGBUS_ENV", "development")
12
12
  raw = File.read(path)
13
13
  parsed = YAML.safe_load(ERB.new(raw).result, permitted_classes: [Symbol], aliases: true)
14
14
  config_hash = parsed.fetch(env, parsed)
@@ -36,7 +36,7 @@ module Pgbus
36
36
  attr_accessor :outbox_enabled, :outbox_poll_interval, :outbox_batch_size, :outbox_retention
37
37
 
38
38
  # Event bus
39
- attr_accessor :idempotency_ttl
39
+ attr_accessor :idempotency_ttl, :allowed_global_id_models
40
40
 
41
41
  # Logging
42
42
  attr_accessor :logger
@@ -108,8 +108,9 @@ module Pgbus
108
108
  @outbox_retention = 24 * 3600 # 1 day
109
109
 
110
110
  @idempotency_ttl = 7 * 24 * 3600 # 7 days
111
+ @allowed_global_id_models = nil # nil = allow all (for backwards compat)
111
112
 
112
- @logger = defined?(Rails) ? Rails.logger : Logger.new($stdout)
113
+ @logger = (defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger) || Logger.new($stdout)
113
114
 
114
115
  @listen_notify = true
115
116
  @notify_throttle_ms = 250
@@ -138,7 +139,9 @@ module Pgbus
138
139
  end
139
140
 
140
141
  def queue_name(name)
141
- "#{queue_prefix}_#{name}"
142
+ full = "#{queue_prefix}_#{name}"
143
+ QueueNameValidator.validate!(full)
144
+ full
142
145
  end
143
146
 
144
147
  def dead_letter_queue_name(name)
@@ -198,7 +201,7 @@ module Pgbus
198
201
  connection_params
199
202
  elsif defined?(ActiveRecord::Base)
200
203
  if connects_to
201
- -> { Pgbus::ApplicationRecord.connection.raw_connection }
204
+ -> { Pgbus::BusRecord.connection.raw_connection }
202
205
  else
203
206
  -> { ActiveRecord::Base.connection.raw_connection }
204
207
  end
data/lib/pgbus/engine.rb CHANGED
@@ -27,7 +27,7 @@ module Pgbus
27
27
 
28
28
  initializer "pgbus.db" do
29
29
  ActiveSupport.on_load(:active_record) do
30
- Pgbus::ApplicationRecord.connects_to(**Pgbus.configuration.connects_to) if Pgbus.configuration.connects_to
30
+ Pgbus::BusRecord.connects_to(**Pgbus.configuration.connects_to) if Pgbus.configuration.connects_to
31
31
  end
32
32
  end
33
33
 
@@ -36,7 +36,7 @@ module Pgbus
36
36
 
37
37
  def build_event(raw)
38
38
  payload = raw["payload"]
39
- payload = GlobalID::Locator.locate(payload["_global_id"]) if payload.is_a?(Hash) && payload["_global_id"]
39
+ payload = Serializer.locate_global_id(payload["_global_id"]) if payload.is_a?(Hash) && payload["_global_id"]
40
40
 
41
41
  Event.new(
42
42
  event_id: raw["event_id"],
@@ -27,7 +27,7 @@ module Pgbus
27
27
  # that share at least one queue with the given queue list,
28
28
  # excluding the current worker (by PID).
29
29
  def self.max_active_priority(queues, my_pid)
30
- conn = Pgbus.configuration.connects_to ? Pgbus::ApplicationRecord.connection : ActiveRecord::Base.connection
30
+ conn = Pgbus.configuration.connects_to ? Pgbus::BusRecord.connection : ActiveRecord::Base.connection
31
31
  rows = conn.select_all(
32
32
  "SELECT metadata FROM pgbus_processes WHERE kind = 'worker' AND pid != $1 AND last_heartbeat_at > $2",
33
33
  "Pgbus ConsumerPriority",
@@ -180,7 +180,7 @@ module Pgbus
180
180
  batch_size = config.archive_compaction_batch_size || 1000
181
181
  prefix = config.queue_prefix
182
182
 
183
- conn = config.connects_to ? Pgbus::ApplicationRecord.connection : ActiveRecord::Base.connection
183
+ conn = config.connects_to ? Pgbus::BusRecord.connection : ActiveRecord::Base.connection
184
184
  queue_names = conn.select_values("SELECT queue_name FROM pgmq.meta ORDER BY queue_name")
185
185
 
186
186
  queue_names.each do |full_name|
@@ -77,7 +77,7 @@ module Pgbus
77
77
 
78
78
  def connection
79
79
  if Pgbus.configuration.connects_to
80
- Pgbus::ApplicationRecord.connection
80
+ Pgbus::BusRecord.connection
81
81
  else
82
82
  ActiveRecord::Base.connection
83
83
  end
@@ -143,7 +143,7 @@ module Pgbus
143
143
  return true if config.recurring_tasks_file && File.exist?(config.recurring_tasks_file.to_s)
144
144
 
145
145
  # Check default location
146
- if defined?(Rails)
146
+ if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
147
147
  default_path = Rails.root.join("config", "recurring.yml")
148
148
  return File.exist?(default_path.to_s)
149
149
  end
@@ -155,7 +155,7 @@ module Pgbus
155
155
  return if config.recurring_tasks&.any?
156
156
 
157
157
  path = config.recurring_tasks_file
158
- path ||= defined?(Rails) ? Rails.root.join("config", "recurring.yml") : nil
158
+ path ||= defined?(Rails) && Rails.respond_to?(:root) && Rails.root ? Rails.root.join("config", "recurring.yml") : nil
159
159
  return unless path && File.exist?(path.to_s)
160
160
 
161
161
  config.recurring_tasks = Recurring::ConfigLoader.load(path)
@@ -289,7 +289,7 @@ module Pgbus
289
289
  end
290
290
 
291
291
  def load_rails_app
292
- return unless defined?(Rails)
292
+ return unless defined?(Rails) && Rails.respond_to?(:application) && Rails.application
293
293
 
294
294
  Rails.application.eager_load! if Rails.application.respond_to?(:eager_load!)
295
295
  end
@@ -66,12 +66,14 @@ module Pgbus
66
66
 
67
67
  def graceful_shutdown
68
68
  Pgbus.logger.info { "[Pgbus] Worker shutting down gracefully..." }
69
+ Pgbus.stopping = true
69
70
  @lifecycle.transition_to(:draining)
70
71
  @wake_signal.notify!
71
72
  end
72
73
 
73
74
  def immediate_shutdown
74
75
  Pgbus.logger.warn { "[Pgbus] Worker shutting down immediately!" }
76
+ Pgbus.stopping = true
75
77
  @lifecycle.transition_to!(:stopped)
76
78
  @wake_signal.notify!
77
79
  @pool.kill
@@ -189,7 +191,7 @@ module Pgbus
189
191
  dlq_suffix = config.dead_letter_queue_suffix
190
192
  prefix = "#{config.queue_prefix}_"
191
193
 
192
- conn = Pgbus.configuration.connects_to ? Pgbus::ApplicationRecord.connection : ActiveRecord::Base.connection
194
+ conn = Pgbus.configuration.connects_to ? Pgbus::BusRecord.connection : ActiveRecord::Base.connection
193
195
  all_queues = conn.select_values("SELECT queue_name FROM pgmq.meta ORDER BY queue_name")
194
196
  resolved = all_queues
195
197
  .reject { |q| q.end_with?(dlq_suffix) }
@@ -210,6 +212,7 @@ module Pgbus
210
212
  def check_recycle
211
213
  return unless @lifecycle.running? && recycle_needed?
212
214
 
215
+ Pgbus.stopping = true
213
216
  @lifecycle.transition_to(:draining)
214
217
  @wake_signal.notify!
215
218
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pgbus
4
+ # Validates and sanitizes PGMQ queue names for safe use in SQL identifiers.
5
+ #
6
+ # PGMQ queue names are interpolated into SQL as table/sequence names
7
+ # (e.g., pgmq.q_<name>, pgmq.a_<name>). This module enforces strict
8
+ # validation to prevent SQL injection via crafted queue names.
9
+ module QueueNameValidator
10
+ # PostgreSQL identifier limit is 63 bytes (NAMEDATALEN - 1).
11
+ # PGMQ prefixes with "q_" or "a_" (2 chars), so limit the name itself.
12
+ MAX_QUEUE_NAME_LENGTH = 61
13
+
14
+ # Only alphanumeric characters and underscores are allowed.
15
+ VALID_QUEUE_NAME_PATTERN = /\A[a-zA-Z0-9_]+\z/
16
+
17
+ module_function
18
+
19
+ # Validates a queue name for safe SQL identifier use.
20
+ # Returns the name if valid, raises ArgumentError if not.
21
+ def validate!(name)
22
+ name = name.to_s
23
+ raise ArgumentError, "Queue name cannot be blank" if name.empty?
24
+ if name.length > MAX_QUEUE_NAME_LENGTH
25
+ raise ArgumentError,
26
+ "Queue name too long (#{name.length} chars, max #{MAX_QUEUE_NAME_LENGTH}): #{name.inspect}"
27
+ end
28
+
29
+ unless VALID_QUEUE_NAME_PATTERN.match?(name)
30
+ raise ArgumentError,
31
+ "Invalid queue name: #{name.inspect}. Only alphanumeric characters and underscores are allowed."
32
+ end
33
+
34
+ name
35
+ end
36
+
37
+ # Sanitizes a queue name by removing invalid characters, then validates.
38
+ # Use this for names from untrusted sources (e.g., URL params).
39
+ def sanitize!(name)
40
+ sanitized = name.to_s.gsub(/[^a-zA-Z0-9_]/, "")
41
+ validate!(sanitized)
42
+ sanitized
43
+ end
44
+ end
45
+ end
@@ -24,7 +24,7 @@ module Pgbus
24
24
  end
25
25
 
26
26
  def detect_env
27
- if defined?(Rails)
27
+ if defined?(Rails) && Rails.respond_to?(:env) && Rails.env
28
28
  Rails.env.to_s
29
29
  else
30
30
  ENV.fetch("PGBUS_ENV", "development")
@@ -41,7 +41,7 @@ module Pgbus
41
41
  data = JSON.parse(json_string)
42
42
  payload = data["payload"]
43
43
 
44
- data["payload"] = GlobalID::Locator.locate(payload["_global_id"]) if payload.is_a?(Hash) && payload["_global_id"]
44
+ data["payload"] = locate_global_id(payload["_global_id"]) if payload.is_a?(Hash) && payload["_global_id"]
45
45
 
46
46
  Event.new(
47
47
  event_id: data["event_id"],
@@ -49,5 +49,32 @@ module Pgbus
49
49
  published_at: Time.parse(data["published_at"])
50
50
  )
51
51
  end
52
+
53
+ # Locate a GlobalID with optional type restriction.
54
+ # When allowed_global_id_models is configured, only those model classes
55
+ # can be resolved — prevents loading arbitrary objects from crafted payloads.
56
+ def locate_global_id(gid_string)
57
+ gid = GlobalID.parse(gid_string)
58
+ raise ArgumentError, "Invalid GlobalID: #{gid_string.inspect}" unless gid
59
+
60
+ allowed = Pgbus.configuration.allowed_global_id_models
61
+ if allowed&.empty?
62
+ raise ArgumentError,
63
+ "GlobalID deserialization is disabled (allowed_global_id_models is empty). " \
64
+ "Set to nil to allow all models, or add permitted classes."
65
+ end
66
+ if allowed&.any? { |entry| !entry.is_a?(Class) && !entry.is_a?(Module) }
67
+ raise ArgumentError,
68
+ "allowed_global_id_models must contain Class/Module objects, " \
69
+ "got: #{allowed.map(&:class).uniq.join(", ")}"
70
+ end
71
+ if allowed&.none? { |klass| gid.model_class <= klass }
72
+ raise ArgumentError,
73
+ "GlobalID model #{gid.model_class} is not in allowed_global_id_models. " \
74
+ "Add it to Pgbus.configuration.allowed_global_id_models to permit deserialization."
75
+ end
76
+
77
+ GlobalID::Locator.locate(gid)
78
+ end
52
79
  end
53
80
  end
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.2.3"
4
+ VERSION = "0.2.5"
5
5
  end
@@ -9,16 +9,35 @@ module Pgbus
9
9
  before_action :authenticate_pgbus!
10
10
  end
11
11
 
12
+ class << self
13
+ attr_accessor :auth_warned
14
+ end
15
+
12
16
  private
13
17
 
14
18
  def authenticate_pgbus!
15
19
  auth_block = Pgbus.configuration.web_auth
16
- return if auth_block.nil?
20
+
21
+ if auth_block.nil?
22
+ warn_unauthenticated_dashboard
23
+ return
24
+ end
17
25
 
18
26
  return if auth_block.respond_to?(:call) && auth_block.call(request)
19
27
 
20
28
  head :unauthorized
21
29
  end
30
+
31
+ def warn_unauthenticated_dashboard
32
+ return if Pgbus::Web::Authentication.auth_warned
33
+
34
+ Pgbus.logger.warn do
35
+ "[Pgbus] Dashboard is accessible without authentication. " \
36
+ "Configure Pgbus.configuration.web_auth to restrict access. " \
37
+ "See: https://github.com/mhenrixon/pgbus#dashboard-authentication"
38
+ end
39
+ Pgbus::Web::Authentication.auth_warned = true
40
+ end
22
41
  end
23
42
  end
24
43
  end
@@ -561,7 +561,7 @@ module Pgbus
561
561
  private
562
562
 
563
563
  def connection
564
- Pgbus::ApplicationRecord.connection
564
+ Pgbus::BusRecord.connection
565
565
  end
566
566
 
567
567
  # name is the full PGMQ queue name (already prefixed)
@@ -662,10 +662,7 @@ module Pgbus
662
662
  end
663
663
 
664
664
  def sanitize_name(name)
665
- sanitized = name.gsub(/[^a-zA-Z0-9_]/, "")
666
- raise ArgumentError, "Invalid queue name: #{name.inspect}" if sanitized.empty?
667
-
668
- sanitized
665
+ QueueNameValidator.sanitize!(name)
669
666
  end
670
667
 
671
668
  def compute_throughput(queues)
data/lib/pgbus.rb CHANGED
@@ -13,6 +13,14 @@ module Pgbus
13
13
  class SchemaNotReady < Error; end
14
14
 
15
15
  class << self
16
+ # Process-global flag set by Worker#graceful_shutdown so the adapter
17
+ # can report stopping? to ActiveJob::Continuation (Rails 8.1+).
18
+ attr_writer :stopping
19
+
20
+ def stopping
21
+ @stopping || false
22
+ end
23
+
16
24
  def loader
17
25
  @loader ||= begin
18
26
  loader = Zeitwerk::Loader.for_gem
@@ -75,6 +83,10 @@ module Pgbus
75
83
  def logger
76
84
  configuration.logger
77
85
  end
86
+
87
+ def logger=(value)
88
+ configuration.logger = value
89
+ end
78
90
  end
79
91
 
80
92
  loader.setup
@@ -12,7 +12,7 @@ namespace :pgbus do
12
12
  latest = Pgbus::PgmqSchema.latest_version
13
13
  puts "Vendored version: #{latest}"
14
14
 
15
- conn = Pgbus.configuration.connects_to ? Pgbus::ApplicationRecord.connection : ActiveRecord::Base.connection
15
+ conn = Pgbus.configuration.connects_to ? Pgbus::BusRecord.connection : ActiveRecord::Base.connection
16
16
 
17
17
  if conn.table_exists?("pgbus_pgmq_schema_versions")
18
18
  row = conn.select_one(
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.2.3
4
+ version: 0.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson
@@ -43,6 +43,20 @@ dependencies:
43
43
  - - ">="
44
44
  - !ruby/object:Gem::Version
45
45
  version: 1.11.1
46
+ - !ruby/object:Gem::Dependency
47
+ name: globalid
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - "~>"
51
+ - !ruby/object:Gem::Version
52
+ version: '1.0'
53
+ type: :runtime
54
+ prerelease: false
55
+ version_requirements: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '1.0'
46
60
  - !ruby/object:Gem::Dependency
47
61
  name: pgmq-ruby
48
62
  requirement: !ruby/object:Gem::Requirement
@@ -197,6 +211,7 @@ files:
197
211
  - lib/pgbus/active_job/adapter.rb
198
212
  - lib/pgbus/active_job/executor.rb
199
213
  - lib/pgbus/batch.rb
214
+ - lib/pgbus/bus_record.rb
200
215
  - lib/pgbus/circuit_breaker.rb
201
216
  - lib/pgbus/cli.rb
202
217
  - lib/pgbus/client.rb
@@ -228,6 +243,7 @@ files:
228
243
  - lib/pgbus/process/wake_signal.rb
229
244
  - lib/pgbus/process/worker.rb
230
245
  - lib/pgbus/queue_factory.rb
246
+ - lib/pgbus/queue_name_validator.rb
231
247
  - lib/pgbus/rate_counter.rb
232
248
  - lib/pgbus/recurring/already_recorded.rb
233
249
  - lib/pgbus/recurring/command_job.rb