pgbus 0.9.10 → 0.10.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 +24 -7
- data/README.md +34 -7
- data/Rakefile +22 -1
- data/app/models/pgbus/stream_queue.rb +87 -0
- data/lib/generators/pgbus/add_stream_queues_generator.rb +50 -0
- data/lib/generators/pgbus/install_generator.rb +3 -3
- data/lib/generators/pgbus/templates/add_stream_queues.rb.erb +11 -0
- data/lib/generators/pgbus/templates/initializer.rb.erb +62 -0
- data/lib/generators/pgbus/templates/migration.rb.erb +14 -0
- data/lib/generators/pgbus/update_generator.rb +6 -66
- data/lib/pgbus/client/ensure_stream_queue.rb +15 -1
- data/lib/pgbus/client/read_after.rb +7 -6
- data/lib/pgbus/client/resizable_pool.rb +160 -0
- data/lib/pgbus/client.rb +261 -4
- data/lib/pgbus/configuration.rb +244 -11
- data/lib/pgbus/doctor.rb +73 -2
- data/lib/pgbus/engine.rb +30 -3
- data/lib/pgbus/generators/migration_detector.rb +7 -0
- data/lib/pgbus/process/dispatcher.rb +89 -19
- data/lib/pgbus/process/notify_listener.rb +18 -2
- data/lib/pgbus/process/worker.rb +19 -3
- data/lib/pgbus/streams/pool_autoscaler.rb +202 -0
- data/lib/pgbus/streams/pool_trigger.rb +124 -0
- data/lib/pgbus/streams/turbo_broadcastable.rb +23 -0
- data/lib/pgbus/streams.rb +2 -2
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/streamer/connection.rb +22 -6
- data/lib/pgbus/web/streamer/falcon_connection.rb +17 -5
- data/lib/pgbus/web/streamer/instance.rb +75 -5
- data/lib/pgbus/web/streamer/io_writer.rb +11 -5
- data/lib/pgbus/web/streamer/listener.rb +61 -1
- data/lib/pgbus/web/streamer/outbound_pump.rb +260 -0
- data/lib/pgbus/web/streamer/stream_event_dispatcher.rb +89 -7
- metadata +9 -4
- data/lib/generators/pgbus/templates/pgbus.yml.erb +0 -76
- data/lib/pgbus/config_loader.rb +0 -73
- data/lib/pgbus/generators/config_converter.rb +0 -345
|
@@ -1,345 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "yaml"
|
|
4
|
-
|
|
5
|
-
module Pgbus
|
|
6
|
-
module Generators
|
|
7
|
-
# Converts a +config/pgbus.yml+ file into a Ruby initializer
|
|
8
|
-
# (+config/initializers/pgbus.rb+) using the Ruby DSL added in
|
|
9
|
-
# Pgbus 0.5+ — capsule string DSL, ActiveSupport::Duration coercion,
|
|
10
|
-
# auto-tuned pool size, named capsules, etc.
|
|
11
|
-
#
|
|
12
|
-
# Drops settings that:
|
|
13
|
-
# - match the gem default (no point restating it)
|
|
14
|
-
# - are deprecated (settings that no longer exist in the public API —
|
|
15
|
-
# see DEPRECATED_SETTINGS; pool_size is NOT one of these, it's a
|
|
16
|
-
# live accessor that overrides auto-tuning and is preserved)
|
|
17
|
-
#
|
|
18
|
-
# Converts seconds to durations when they evenly divide into a clean
|
|
19
|
-
# unit (7 days, 30 days, 10 minutes). Falls back to the raw integer
|
|
20
|
-
# otherwise.
|
|
21
|
-
#
|
|
22
|
-
# When the YAML has multiple environments with different values for
|
|
23
|
-
# the same setting, emits Rails.env-aware code:
|
|
24
|
-
#
|
|
25
|
-
# - 2 envs with same value → unconditional line
|
|
26
|
-
# - 2 envs, one differs → `unless Rails.env.X?` modifier
|
|
27
|
-
# - 3+ envs with differences → `case Rails.env when ... end`
|
|
28
|
-
#
|
|
29
|
-
# Backwards compatible: the original YAML file is NOT touched. The
|
|
30
|
-
# generator's CLI wrapper writes the new initializer and tells the
|
|
31
|
-
# user to delete the YAML when ready.
|
|
32
|
-
class ConfigConverter
|
|
33
|
-
class Error < Pgbus::Error; end
|
|
34
|
-
|
|
35
|
-
# Setters that accept ActiveSupport::Duration (PR 5).
|
|
36
|
-
DURATION_SETTINGS = %w[
|
|
37
|
-
visibility_timeout archive_retention idempotency_ttl
|
|
38
|
-
outbox_retention stats_retention recurring_execution_retention
|
|
39
|
-
].freeze
|
|
40
|
-
|
|
41
|
-
# Settings that no longer exist in the public API. The converter
|
|
42
|
-
# silently drops these from the generated initializer so users on
|
|
43
|
-
# legacy YAML get a clean migration. NOTE: pool_size is intentionally
|
|
44
|
-
# NOT here — it's a live Configuration#pool_size accessor users set to
|
|
45
|
-
# override auto-tuning, so it must survive the conversion unchanged.
|
|
46
|
-
#
|
|
47
|
-
# - notify_throttle_ms -> Pgbus::Client::NOTIFY_THROTTLE_MS
|
|
48
|
-
# - circuit_breaker_* -> Pgbus::CircuitBreaker constants
|
|
49
|
-
# - archive_compaction_* -> Pgbus::Process::Dispatcher constants
|
|
50
|
-
# - dead_letter_queue_suffix -> Pgbus::DEAD_LETTER_SUFFIX (frozen)
|
|
51
|
-
DEPRECATED_SETTINGS = %w[
|
|
52
|
-
notify_throttle_ms
|
|
53
|
-
circuit_breaker_threshold circuit_breaker_base_backoff circuit_breaker_max_backoff
|
|
54
|
-
archive_compaction_interval archive_compaction_batch_size
|
|
55
|
-
dead_letter_queue_suffix
|
|
56
|
-
].freeze
|
|
57
|
-
|
|
58
|
-
# Settings whose default we know how to compute by inspecting
|
|
59
|
-
# Pgbus::Configuration.new. Any setting not listed here is emitted
|
|
60
|
-
# as-is (we can't tell if it matches the default).
|
|
61
|
-
KNOWN_SETTINGS = %w[
|
|
62
|
-
queue_prefix default_queue pool_timeout listen_notify
|
|
63
|
-
visibility_timeout max_retries idempotency_ttl
|
|
64
|
-
max_jobs_per_worker max_memory_mb max_worker_lifetime
|
|
65
|
-
dispatch_interval prefetch_limit
|
|
66
|
-
circuit_breaker_enabled
|
|
67
|
-
archive_retention
|
|
68
|
-
outbox_enabled outbox_poll_interval outbox_batch_size outbox_retention
|
|
69
|
-
stats_enabled stats_retention
|
|
70
|
-
recurring_schedule_interval recurring_execution_retention skip_recurring
|
|
71
|
-
polling_interval default_priority priority_levels
|
|
72
|
-
return_to_app_url workers
|
|
73
|
-
].freeze
|
|
74
|
-
|
|
75
|
-
def self.from_yaml(path)
|
|
76
|
-
raise Error, "config file not found: #{path}" unless File.exist?(path)
|
|
77
|
-
|
|
78
|
-
parsed = YAML.safe_load_file(path, aliases: true, permitted_classes: [Symbol])
|
|
79
|
-
from_hash(parsed)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def self.from_hash(envs_hash)
|
|
83
|
-
new(envs_hash).render
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def initialize(envs_hash)
|
|
87
|
-
@envs = (envs_hash || {}).reject { |env, _| env.start_with?("default") }
|
|
88
|
-
@envs = { "production" => envs_hash } if @envs.empty? && envs_hash
|
|
89
|
-
@defaults = build_defaults
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def render
|
|
93
|
-
lines = []
|
|
94
|
-
lines << "# frozen_string_literal: true"
|
|
95
|
-
lines << "#"
|
|
96
|
-
lines << "# Generated by `rails generate pgbus:update` from config/pgbus.yml."
|
|
97
|
-
lines << "# Review and adjust as needed, then delete config/pgbus.yml."
|
|
98
|
-
lines << ""
|
|
99
|
-
lines << "Pgbus.configure do |c|"
|
|
100
|
-
|
|
101
|
-
body = render_body
|
|
102
|
-
body.each { |line| lines << " #{line}" }
|
|
103
|
-
|
|
104
|
-
lines << "end"
|
|
105
|
-
"#{lines.join("\n")}\n"
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
private
|
|
109
|
-
|
|
110
|
-
def build_defaults
|
|
111
|
-
config = Pgbus::Configuration.new
|
|
112
|
-
KNOWN_SETTINGS.each_with_object({}) do |key, h|
|
|
113
|
-
h[key] = config.public_send(key) if config.respond_to?(key)
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def render_body
|
|
118
|
-
all_settings = collect_all_settings
|
|
119
|
-
return [] if all_settings.empty?
|
|
120
|
-
|
|
121
|
-
constant_settings, varying_settings = partition_by_variance(all_settings)
|
|
122
|
-
special_keys = %w[workers]
|
|
123
|
-
|
|
124
|
-
lines = []
|
|
125
|
-
# Special: workers comes first (most user-visible)
|
|
126
|
-
special_keys.each do |key|
|
|
127
|
-
rendered = render_workers(constant_settings[key], varying_settings[key])
|
|
128
|
-
lines.concat(rendered) if rendered
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
constant_settings.each do |key, value|
|
|
132
|
-
next if special_keys.include?(key)
|
|
133
|
-
next if drop?(key, value)
|
|
134
|
-
|
|
135
|
-
lines << render_setting(key, value)
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
varying_settings.each do |key, env_values|
|
|
139
|
-
next if special_keys.include?(key)
|
|
140
|
-
next if env_values.values.all? { |v| drop?(key, v) }
|
|
141
|
-
|
|
142
|
-
lines.concat(render_varying_setting(key, env_values))
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
lines
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
def collect_all_settings
|
|
149
|
-
all_keys = @envs.values.flat_map { |env_settings| env_settings&.keys || [] }.uniq
|
|
150
|
-
all_keys.to_h do |key|
|
|
151
|
-
[key, @envs.transform_values { |env_settings| env_settings&.fetch(key, :__missing__) }]
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
# Returns [constant_settings, varying_settings].
|
|
156
|
-
# constant_settings: { "key" => value } (same value in EVERY env)
|
|
157
|
-
# varying_settings: { "key" => { env => value, ... } }
|
|
158
|
-
#
|
|
159
|
-
# A setting is only "constant" when it is present in every env
|
|
160
|
-
# and all envs agree on the value. If any env is missing the
|
|
161
|
-
# setting entirely (e.g. `polling_interval: 0.01` set only under
|
|
162
|
-
# `test:`), emitting it as an unconditional line would silently
|
|
163
|
-
# apply the value to envs that never asked for it — see #93.
|
|
164
|
-
def partition_by_variance(all_settings)
|
|
165
|
-
constant = {}
|
|
166
|
-
varying = {}
|
|
167
|
-
all_settings.each do |key, env_values|
|
|
168
|
-
present_values = env_values.reject { |_, v| v == :__missing__ }
|
|
169
|
-
unique_values = present_values.values.uniq
|
|
170
|
-
all_envs_present = present_values.size == env_values.size
|
|
171
|
-
|
|
172
|
-
if all_envs_present && unique_values.size <= 1
|
|
173
|
-
constant[key] = unique_values.first
|
|
174
|
-
else
|
|
175
|
-
varying[key] = present_values
|
|
176
|
-
end
|
|
177
|
-
end
|
|
178
|
-
[constant, varying]
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
def drop?(key, value)
|
|
182
|
-
return true if DEPRECATED_SETTINGS.include?(key)
|
|
183
|
-
return true if value == :__missing__
|
|
184
|
-
return true if @defaults.key?(key) && @defaults[key] == value
|
|
185
|
-
|
|
186
|
-
false
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
def render_setting(key, value)
|
|
190
|
-
"c.#{key} = #{render_value(key, value)}"
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
def render_varying_setting(key, env_values)
|
|
194
|
-
envs = env_values.keys
|
|
195
|
-
|
|
196
|
-
# Single-env coverage: the setting exists in exactly one env.
|
|
197
|
-
# Emit an `if Rails.env.X?` modifier rather than a case block
|
|
198
|
-
# so other envs fall back to the gem default. This is the
|
|
199
|
-
# fix for #93 — without it, a `test:`-only `polling_interval`
|
|
200
|
-
# would leak into dev and prod as an unconditional assignment.
|
|
201
|
-
if envs.size == 1
|
|
202
|
-
env = envs.first
|
|
203
|
-
value = env_values[env]
|
|
204
|
-
return ["c.#{key} = #{render_value(key, value)} if Rails.env.#{env}?"]
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
if envs.size == 2 && envs.include?("development")
|
|
208
|
-
# Special case: "everything except dev" — common pattern
|
|
209
|
-
non_dev_value = env_values.except("development").values.first
|
|
210
|
-
dev_value = env_values["development"]
|
|
211
|
-
return ["c.#{key} = #{render_value(key, non_dev_value)} unless Rails.env.development?"] if dev_value.nil? && non_dev_value
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
# Fallback: case Rails.env block — `when` clauses indented one
|
|
215
|
-
# level inside the case, `end` flush with `c.X` (standard Ruby
|
|
216
|
-
# formatting for assigned case expressions).
|
|
217
|
-
lines = ["c.#{key} = case Rails.env"]
|
|
218
|
-
env_values.each do |env, value|
|
|
219
|
-
lines << " when \"#{env}\" then #{render_value(key, value)}"
|
|
220
|
-
end
|
|
221
|
-
lines << "end"
|
|
222
|
-
lines
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
def render_workers(constant_workers, varying_workers)
|
|
226
|
-
if constant_workers && !varying_workers
|
|
227
|
-
rendered = render_workers_value(constant_workers)
|
|
228
|
-
return rendered unless rendered.nil? || rendered.empty?
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
if varying_workers
|
|
232
|
-
# Different worker config per env. when clauses indented one
|
|
233
|
-
# level inside the case, end flush with c.workers (standard
|
|
234
|
-
# Ruby formatting for assigned case expressions).
|
|
235
|
-
lines = ["c.workers = case Rails.env"]
|
|
236
|
-
varying_workers.each do |env, workers|
|
|
237
|
-
string_form = workers_as_string(workers)
|
|
238
|
-
value = string_form ? string_form.inspect : workers.inspect
|
|
239
|
-
lines << " when \"#{env}\" then #{value}"
|
|
240
|
-
end
|
|
241
|
-
lines << "end"
|
|
242
|
-
return lines
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
nil
|
|
246
|
-
end
|
|
247
|
-
|
|
248
|
-
def render_workers_value(workers)
|
|
249
|
-
return nil if workers.nil? || workers == :__missing__
|
|
250
|
-
|
|
251
|
-
# Drop if matches the gem default ([{queues: %w[default], threads: 5}]).
|
|
252
|
-
# Compare via normalized form (string keys both sides) so YAML's
|
|
253
|
-
# string-keyed hashes match the symbol-keyed default.
|
|
254
|
-
return nil if normalize_workers(workers) == normalize_workers(@defaults["workers"])
|
|
255
|
-
|
|
256
|
-
if workers_simple?(workers)
|
|
257
|
-
string_form = workers_as_string(workers)
|
|
258
|
-
["c.workers = #{string_form.inspect}"]
|
|
259
|
-
else
|
|
260
|
-
workers.map.with_index do |capsule, idx|
|
|
261
|
-
render_capsule_call(capsule, idx)
|
|
262
|
-
end
|
|
263
|
-
end
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
# Normalize a workers array to symbol-keyed hashes with array-of-string
|
|
267
|
-
# queues for stable comparison. Doesn't mutate input.
|
|
268
|
-
def normalize_workers(workers)
|
|
269
|
-
return nil if workers.nil?
|
|
270
|
-
|
|
271
|
-
workers.map do |capsule|
|
|
272
|
-
{
|
|
273
|
-
queues: Array(capsule[:queues] || capsule["queues"]).map(&:to_s),
|
|
274
|
-
threads: (capsule[:threads] || capsule["threads"] || 5).to_i
|
|
275
|
-
}
|
|
276
|
-
end
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
def workers_simple?(workers)
|
|
280
|
-
workers.all? do |capsule|
|
|
281
|
-
extras = capsule.keys.map(&:to_s) - %w[queues threads]
|
|
282
|
-
extras.empty?
|
|
283
|
-
end
|
|
284
|
-
end
|
|
285
|
-
|
|
286
|
-
def workers_as_string(workers)
|
|
287
|
-
workers.map do |capsule|
|
|
288
|
-
queues = (capsule["queues"] || capsule[:queues]).join(", ")
|
|
289
|
-
threads = capsule["threads"] || capsule[:threads] || 5
|
|
290
|
-
"#{queues}: #{threads}"
|
|
291
|
-
end.join("; ")
|
|
292
|
-
end
|
|
293
|
-
|
|
294
|
-
def render_capsule_call(capsule, _idx)
|
|
295
|
-
queues = capsule["queues"] || capsule[:queues]
|
|
296
|
-
threads = capsule["threads"] || capsule[:threads] || 5
|
|
297
|
-
name = capsule["name"] || capsule[:name] || queues.first
|
|
298
|
-
opts = capsule.reject { |k, _| %w[queues threads name].include?(k.to_s) }
|
|
299
|
-
|
|
300
|
-
parts = ["queues: #{queues.inspect}", "threads: #{threads}"]
|
|
301
|
-
opts.each { |k, v| parts << "#{k}: #{v.inspect}" }
|
|
302
|
-
|
|
303
|
-
"c.capsule :#{name}, #{parts.join(", ")}"
|
|
304
|
-
end
|
|
305
|
-
|
|
306
|
-
def render_value(key, value)
|
|
307
|
-
return "nil" if value.nil?
|
|
308
|
-
|
|
309
|
-
if DURATION_SETTINGS.include?(key) && value.is_a?(Integer) && value.positive?
|
|
310
|
-
duration_form = format_duration(value)
|
|
311
|
-
return duration_form if duration_form
|
|
312
|
-
end
|
|
313
|
-
|
|
314
|
-
if value.is_a?(Integer) && value >= 1000
|
|
315
|
-
format_integer_with_underscores(value)
|
|
316
|
-
else
|
|
317
|
-
value.inspect
|
|
318
|
-
end
|
|
319
|
-
end
|
|
320
|
-
|
|
321
|
-
def format_duration(seconds)
|
|
322
|
-
units = [
|
|
323
|
-
[86_400, "day", "days"],
|
|
324
|
-
[3600, "hour", "hours"],
|
|
325
|
-
[60, "minute", "minutes"],
|
|
326
|
-
[1, "second", "seconds"]
|
|
327
|
-
]
|
|
328
|
-
|
|
329
|
-
units.each do |unit_seconds, singular, plural|
|
|
330
|
-
next unless (seconds % unit_seconds).zero?
|
|
331
|
-
|
|
332
|
-
count = seconds / unit_seconds
|
|
333
|
-
unit_name = count == 1 ? singular : plural
|
|
334
|
-
return "#{count}.#{unit_name}"
|
|
335
|
-
end
|
|
336
|
-
|
|
337
|
-
nil
|
|
338
|
-
end
|
|
339
|
-
|
|
340
|
-
def format_integer_with_underscores(int)
|
|
341
|
-
int.to_s.reverse.scan(/\d{1,3}/).join("_").reverse
|
|
342
|
-
end
|
|
343
|
-
end
|
|
344
|
-
end
|
|
345
|
-
end
|