pgbus 0.6.4 → 0.6.6
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/README.md +44 -1
- data/lib/pgbus/cli.rb +7 -0
- data/lib/pgbus/configuration.rb +31 -2
- data/lib/pgbus/engine.rb +9 -0
- data/lib/pgbus/execution_pools/async_pool.rb +175 -0
- data/lib/pgbus/execution_pools/thread_pool.rb +61 -0
- data/lib/pgbus/execution_pools.rb +27 -0
- data/lib/pgbus/process/consumer.rb +5 -4
- data/lib/pgbus/process/supervisor.rb +4 -2
- data/lib/pgbus/process/worker.rb +19 -9
- data/lib/pgbus/streams/turbo_stream_override.rb +40 -0
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/stream_app.rb +39 -3
- data/lib/pgbus/web/streamer/connection.rb +4 -0
- data/lib/pgbus/web/streamer/falcon_connection.rb +114 -0
- data/lib/pgbus/web/streamer/instance.rb +1 -6
- data/lib/pgbus/web/streamer/listener.rb +8 -0
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1a4669f7150717428ced89a13b5c8e7f9cf5d61d6eae6b19919d6408fefad06a
|
|
4
|
+
data.tar.gz: 4dc1c6db2f50ce6e0de5c845cd2813cbb299ad096dab2dcfa5169db368dfa0af
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3184e872126937ec8660530143c610a64a0649bd60ab7c9bca6663e6883af3ec16144cfb3d7b74713ff0a5487b7f1edacba973e387a52f802237f9ff3a2c7b2c
|
|
7
|
+
data.tar.gz: bc0f72ace6413ad27899d7968b3fe6df822fd35f3b495bdbf24ccd95c9add029f5ebb3b11e462c0a30937bfca3082460e3c655533464becf73d6cb328ded8621
|
data/README.md
CHANGED
|
@@ -463,6 +463,36 @@ end
|
|
|
463
463
|
|
|
464
464
|
When a limit is hit, the worker drains its thread pool, exits, and the supervisor forks a fresh process. RSS memory is sampled from `/proc/self/statm` (Linux) or `ps -o rss` (macOS).
|
|
465
465
|
|
|
466
|
+
### Async execution mode (fibers)
|
|
467
|
+
|
|
468
|
+
Workers can optionally execute jobs as fibers instead of threads. This is ideal for I/O-bound workloads (HTTP calls, email delivery, LLM API calls) where jobs spend most of their time waiting on network I/O.
|
|
469
|
+
|
|
470
|
+
```ruby
|
|
471
|
+
Pgbus.configure do |config|
|
|
472
|
+
# Global: all workers use async mode
|
|
473
|
+
config.execution_mode = :async
|
|
474
|
+
|
|
475
|
+
# Or per-worker: mix thread and async workers
|
|
476
|
+
config.workers = [
|
|
477
|
+
{ queues: %w[webhooks emails], threads: 100, execution_mode: :async },
|
|
478
|
+
{ queues: %w[default], threads: 5 } # stays thread-based
|
|
479
|
+
]
|
|
480
|
+
end
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
**Prerequisites:**
|
|
484
|
+
|
|
485
|
+
1. Add `gem "async"` to your Gemfile
|
|
486
|
+
2. Set `config.active_support.isolation_level = :fiber` in your Rails app
|
|
487
|
+
|
|
488
|
+
**Why it reduces connections:** In thread mode, each thread holds a database connection while waiting on I/O. With 50 threads, that's 50 connections. In async mode, 50 fibers share 3-5 connections because fibers yield during I/O and only one runs at a time.
|
|
489
|
+
|
|
490
|
+
**CLI flag:** `pgbus start --execution-mode async`
|
|
491
|
+
|
|
492
|
+
**Safety:** Messages stay in PGMQ with visibility timeout protection regardless of execution mode. If a fiber or worker crashes, the visibility timeout expires and messages become available for re-read. No data loss risk.
|
|
493
|
+
|
|
494
|
+
**Not recommended for:** CPU-bound jobs (image processing, heavy computation). These block the single reactor thread and should use thread mode.
|
|
495
|
+
|
|
466
496
|
## Routing and ordering
|
|
467
497
|
|
|
468
498
|
How messages flow between producers and the workers that handle them: priority sub-queues, consumer priority for active/standby workers, and single-active-consumer for strict ordering.
|
|
@@ -675,7 +705,7 @@ Without the plugin, Puma closes hijacked SSE sockets abruptly during graceful re
|
|
|
675
705
|
|
|
676
706
|
### Requirements
|
|
677
707
|
|
|
678
|
-
- **Puma 6.1+ or Falcon.** Streams use `rack.hijack
|
|
708
|
+
- **Puma 6.1+ or Falcon.** Streams use `rack.hijack` by default. Puma 6.1+ supports it via partial hijack (thread-releasing, see [puma/puma#1009](https://github.com/puma/puma/issues/1009)). Falcon supports both the hijack path (via [protocol-rack](https://github.com/socketry/protocol-rack)'s emulation) and a native streaming body path (`streams_falcon_streaming_body = true`) that integrates with Falcon's fiber scheduler for better backpressure and connection lifecycle management. Unicorn, Pitchfork, and Passenger return HTTP 501 from the streams endpoint.
|
|
679
709
|
- **PostgreSQL LISTEN/NOTIFY.** `config.listen_notify = true` (the default). Stream queues override PGMQ's 250ms NOTIFY throttle to 0 so every broadcast fires individually.
|
|
680
710
|
- **HTTP/2 or HTTP/3 in production.** SSE has a 6-connection-per-origin limit on HTTP/1.1; HTTP/2 lifts it. Falcon supports HTTP/2 natively without a reverse proxy.
|
|
681
711
|
|
|
@@ -695,9 +725,22 @@ Pgbus.configure do |c|
|
|
|
695
725
|
c.streams_idle_timeout = 3_600 # close idle connections after 1h
|
|
696
726
|
c.streams_listen_health_check_ms = 250 # PG LISTEN keepalive + ensure_listening ack budget
|
|
697
727
|
c.streams_write_deadline_ms = 5_000 # write_nonblock deadline
|
|
728
|
+
c.streams_falcon_streaming_body = false # opt-in: Falcon-native streaming body
|
|
698
729
|
end
|
|
699
730
|
```
|
|
700
731
|
|
|
732
|
+
#### Falcon-native streaming body (opt-in)
|
|
733
|
+
|
|
734
|
+
When running on Falcon, enable native streaming body support for better integration with Falcon's fiber scheduler:
|
|
735
|
+
|
|
736
|
+
```ruby
|
|
737
|
+
c.streams_falcon_streaming_body = true
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
With this flag, `StreamApp` returns `[200, headers, Writable]` instead of hijacking the socket. Falcon drives the response lifecycle with proper backpressure, connection cleanup, and fiber-scheduled IO. SSE writes go through `Protocol::HTTP::Body::Writable` which is fiber-safe and yields to other fibers when blocked.
|
|
741
|
+
|
|
742
|
+
Without the flag (default), Falcon uses the same `rack.hijack` path as Puma via protocol-rack's emulation. Both paths are tested and work correctly — the streaming body path is an optimization for Falcon deployments that want tighter scheduler integration.
|
|
743
|
+
|
|
701
744
|
### How it works
|
|
702
745
|
|
|
703
746
|
Stream broadcasts are stored in PGMQ queues prefixed `pgbus_stream_*`. Each broadcast is assigned a monotonic `msg_id` by PGMQ. The `pgbus_stream_from` helper captures the current `MAX(msg_id)` at render time and embeds it in the HTML as `since-id`. When the SSE client connects, it sends that cursor as `?since=` on the first request and as `Last-Event-ID` on reconnects. The streamer replays from `pgmq.q_*` (live) UNION `pgmq.a_*` (archive) for any `msg_id > cursor`, then switches to LISTEN/NOTIFY for the live path. There is no message identity gap between the render and the subscribe — the cursor model guarantees every broadcast is delivered exactly once, in order, even across reconnects.
|
data/lib/pgbus/cli.rb
CHANGED
|
@@ -42,6 +42,7 @@ module Pgbus
|
|
|
42
42
|
options = parse_start_options(args)
|
|
43
43
|
|
|
44
44
|
Pgbus.configuration.workers = options[:queues] if options[:queues]
|
|
45
|
+
Pgbus.configuration.execution_mode = options[:execution_mode].to_sym if options[:execution_mode]
|
|
45
46
|
apply_capsule_filter(options[:capsule]) if options[:capsule]
|
|
46
47
|
apply_role_filter(options)
|
|
47
48
|
end
|
|
@@ -70,6 +71,10 @@ module Pgbus
|
|
|
70
71
|
opts.on("--dispatcher-only", "Run only the dispatcher (the maintenance pod pattern)") do
|
|
71
72
|
options[:dispatcher_only] = true
|
|
72
73
|
end
|
|
74
|
+
|
|
75
|
+
opts.on("--execution-mode MODE", "Execution mode: threads (default) or async") do |v|
|
|
76
|
+
options[:execution_mode] = v
|
|
77
|
+
end
|
|
73
78
|
end.parse!(args.dup)
|
|
74
79
|
options
|
|
75
80
|
end
|
|
@@ -165,6 +170,8 @@ module Pgbus
|
|
|
165
170
|
pattern — exactly one of these per deployment)
|
|
166
171
|
--dispatcher-only Run only the dispatcher (the maintenance pod
|
|
167
172
|
pattern)
|
|
173
|
+
--execution-mode Execution mode: threads (default) or async
|
|
174
|
+
(fiber-based, lower connection usage)
|
|
168
175
|
HELP
|
|
169
176
|
end
|
|
170
177
|
end
|
data/lib/pgbus/configuration.rb
CHANGED
|
@@ -11,7 +11,7 @@ module Pgbus
|
|
|
11
11
|
attr_accessor :default_queue, :queue_prefix
|
|
12
12
|
|
|
13
13
|
# Worker settings
|
|
14
|
-
attr_accessor :polling_interval, :prefetch_limit
|
|
14
|
+
attr_accessor :polling_interval, :prefetch_limit, :execution_mode
|
|
15
15
|
attr_reader :workers, :visibility_timeout # rubocop:disable Style/AccessorGrouping
|
|
16
16
|
|
|
17
17
|
# Supervisor role selection.
|
|
@@ -111,6 +111,7 @@ module Pgbus
|
|
|
111
111
|
@visibility_timeout = 30
|
|
112
112
|
|
|
113
113
|
@prefetch_limit = nil
|
|
114
|
+
@execution_mode = :threads
|
|
114
115
|
|
|
115
116
|
@max_jobs_per_worker = nil
|
|
116
117
|
@max_memory_mb = nil
|
|
@@ -216,6 +217,13 @@ module Pgbus
|
|
|
216
217
|
(0...priority_levels).map { |p| priority_queue_name(name, p) }
|
|
217
218
|
end
|
|
218
219
|
|
|
220
|
+
# Returns the execution mode for a specific worker config hash,
|
|
221
|
+
# falling back to the global execution_mode setting.
|
|
222
|
+
def execution_mode_for(worker_config)
|
|
223
|
+
mode = worker_config[:execution_mode] || worker_config["execution_mode"] || execution_mode
|
|
224
|
+
ExecutionPools.normalize_mode(mode)
|
|
225
|
+
end
|
|
226
|
+
|
|
219
227
|
VALID_PGMQ_SCHEMA_MODES = %i[auto extension embedded].freeze
|
|
220
228
|
|
|
221
229
|
def pgmq_schema_mode=(mode)
|
|
@@ -242,9 +250,16 @@ module Pgbus
|
|
|
242
250
|
raise ArgumentError, "retry_backoff_jitter must be between 0 and 1"
|
|
243
251
|
end
|
|
244
252
|
|
|
253
|
+
# Validate global execution_mode
|
|
254
|
+
ExecutionPools.normalize_mode(execution_mode)
|
|
255
|
+
|
|
245
256
|
Array(workers).each do |w|
|
|
246
257
|
threads = w[:threads] || w["threads"] || 5
|
|
247
258
|
raise ArgumentError, "worker threads must be > 0" unless threads.is_a?(Integer) && threads.positive?
|
|
259
|
+
|
|
260
|
+
# Validate per-worker execution_mode override if present
|
|
261
|
+
mode = w[:execution_mode] || w["execution_mode"]
|
|
262
|
+
ExecutionPools.normalize_mode(mode) if mode
|
|
248
263
|
end
|
|
249
264
|
|
|
250
265
|
raise ArgumentError, "prefetch_limit must be > 0" if prefetch_limit && !(prefetch_limit.is_a?(Integer) && prefetch_limit.positive?)
|
|
@@ -470,6 +485,11 @@ module Pgbus
|
|
|
470
485
|
# default formula provides.
|
|
471
486
|
POOL_SIZE_WARN_THRESHOLD = 50
|
|
472
487
|
|
|
488
|
+
# Connections needed per async worker: one for the reactor's serial
|
|
489
|
+
# execution, one for polling, one for headroom. Fibers share connections
|
|
490
|
+
# because only one runs at a time per reactor thread.
|
|
491
|
+
ASYNC_POOL_CONNECTIONS = 3
|
|
492
|
+
|
|
473
493
|
def resolved_pool_size
|
|
474
494
|
return pool_size if pool_size
|
|
475
495
|
|
|
@@ -609,10 +629,19 @@ module Pgbus
|
|
|
609
629
|
raise ArgumentError,
|
|
610
630
|
"#{group} threads must be a positive integer, got #{threads.inspect}"
|
|
611
631
|
end
|
|
612
|
-
|
|
632
|
+
|
|
633
|
+
if async_execution_mode?(entry)
|
|
634
|
+
ASYNC_POOL_CONNECTIONS
|
|
635
|
+
else
|
|
636
|
+
threads
|
|
637
|
+
end
|
|
613
638
|
end
|
|
614
639
|
end
|
|
615
640
|
|
|
641
|
+
def async_execution_mode?(entry)
|
|
642
|
+
execution_mode_for(entry) == :async
|
|
643
|
+
end
|
|
644
|
+
|
|
616
645
|
def warn_if_oversized(size)
|
|
617
646
|
return unless size > POOL_SIZE_WARN_THRESHOLD
|
|
618
647
|
|
data/lib/pgbus/engine.rb
CHANGED
|
@@ -84,6 +84,15 @@ module Pgbus
|
|
|
84
84
|
# `_` keeps RuboCop's Lint/Void from deleting the line.
|
|
85
85
|
_autoload_trigger = Pgbus::Streams::TurboBroadcastable
|
|
86
86
|
Pgbus::Streams.install_turbo_broadcastable_patch!
|
|
87
|
+
|
|
88
|
+
# Subscribe-side patch: override turbo_stream_from to render
|
|
89
|
+
# <pgbus-stream-source> (SSE) instead of <turbo-cable-stream-source>
|
|
90
|
+
# (ActionCable). Without this, third-party gems like
|
|
91
|
+
# hotwire-livereload that call turbo_stream_from in their views
|
|
92
|
+
# subscribe via ActionCable while the broadcast (patched above)
|
|
93
|
+
# goes through PGMQ — the message never arrives.
|
|
94
|
+
_autoload_trigger_override = Pgbus::Streams::TurboStreamOverride
|
|
95
|
+
Pgbus::Streams.install_turbo_stream_override!
|
|
87
96
|
end
|
|
88
97
|
end
|
|
89
98
|
end
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
module ExecutionPools
|
|
5
|
+
class AsyncPool
|
|
6
|
+
attr_reader :capacity
|
|
7
|
+
|
|
8
|
+
IDLE_WAIT_INTERVAL = 0.01
|
|
9
|
+
|
|
10
|
+
def initialize(capacity:, on_state_change: nil)
|
|
11
|
+
@capacity = capacity
|
|
12
|
+
@on_state_change = on_state_change
|
|
13
|
+
@available_capacity = capacity
|
|
14
|
+
@mutex = Mutex.new
|
|
15
|
+
@state_mutex = Mutex.new
|
|
16
|
+
@shutdown_flag = false
|
|
17
|
+
@fatal_error = nil
|
|
18
|
+
@boot_queue = Thread::Queue.new
|
|
19
|
+
@pending = Thread::Queue.new
|
|
20
|
+
|
|
21
|
+
validate_dependencies!
|
|
22
|
+
@reactor_thread = start_reactor
|
|
23
|
+
result = @boot_queue.pop
|
|
24
|
+
raise result if result.is_a?(Exception)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def post(&block)
|
|
28
|
+
raise_if_fatal!
|
|
29
|
+
raise "Execution pool is shutting down" if shutdown?
|
|
30
|
+
|
|
31
|
+
reserved = false
|
|
32
|
+
reserve_capacity!
|
|
33
|
+
reserved = true
|
|
34
|
+
@pending << block
|
|
35
|
+
rescue StandardError
|
|
36
|
+
restore_capacity if reserved
|
|
37
|
+
raise
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def available_capacity
|
|
41
|
+
raise_if_fatal!
|
|
42
|
+
@mutex.synchronize { @available_capacity }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def idle?
|
|
46
|
+
available_capacity.positive?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def shutdown
|
|
50
|
+
@state_mutex.synchronize do
|
|
51
|
+
return false if @shutdown_flag
|
|
52
|
+
|
|
53
|
+
@shutdown_flag = true
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def shutdown?
|
|
58
|
+
@state_mutex.synchronize { @shutdown_flag }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def wait_for_termination(timeout)
|
|
62
|
+
@reactor_thread&.join(timeout)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def kill
|
|
66
|
+
shutdown
|
|
67
|
+
@reactor_thread&.kill
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def metadata
|
|
71
|
+
inflight = @mutex.synchronize { @available_capacity }
|
|
72
|
+
{
|
|
73
|
+
mode: :async,
|
|
74
|
+
capacity: @capacity,
|
|
75
|
+
busy: @capacity - inflight
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def validate_dependencies!
|
|
82
|
+
require "async"
|
|
83
|
+
require "async/semaphore"
|
|
84
|
+
rescue LoadError => e
|
|
85
|
+
raise LoadError,
|
|
86
|
+
"Async execution mode requires the `async` gem. " \
|
|
87
|
+
"Add `gem \"async\"` to your Gemfile. Original error: #{e.message}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# rubocop:disable Lint/RescueException
|
|
91
|
+
def start_reactor
|
|
92
|
+
Thread.new do
|
|
93
|
+
Thread.current.name = "pgbus-async-reactor-#{object_id}"
|
|
94
|
+
Async do |task|
|
|
95
|
+
semaphore = Async::Semaphore.new(@capacity, parent: task)
|
|
96
|
+
@boot_queue << :ready
|
|
97
|
+
|
|
98
|
+
wait_for_executions(semaphore)
|
|
99
|
+
wait_for_inflight
|
|
100
|
+
end
|
|
101
|
+
rescue Exception => e
|
|
102
|
+
register_fatal_error(e)
|
|
103
|
+
raise
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
# rubocop:enable Lint/RescueException
|
|
107
|
+
|
|
108
|
+
def wait_for_executions(semaphore)
|
|
109
|
+
loop do
|
|
110
|
+
schedule_pending(semaphore)
|
|
111
|
+
break if shutdown? && @pending.empty?
|
|
112
|
+
|
|
113
|
+
sleep(IDLE_WAIT_INTERVAL) if @pending.empty?
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def schedule_pending(semaphore)
|
|
118
|
+
while (block = next_pending)
|
|
119
|
+
semaphore.async do
|
|
120
|
+
perform(block)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def next_pending
|
|
126
|
+
@pending.pop(true)
|
|
127
|
+
rescue ThreadError
|
|
128
|
+
nil
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def perform(block)
|
|
132
|
+
block.call
|
|
133
|
+
rescue StandardError => e
|
|
134
|
+
Pgbus.logger.error { "[Pgbus] Async pool fiber error: #{e.class}: #{e.message}" }
|
|
135
|
+
ensure
|
|
136
|
+
restore_capacity
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def reserve_capacity!
|
|
140
|
+
@mutex.synchronize do
|
|
141
|
+
raise "Execution pool is at capacity" if @available_capacity <= 0
|
|
142
|
+
|
|
143
|
+
@available_capacity -= 1
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def restore_capacity
|
|
148
|
+
should_notify = @mutex.synchronize do
|
|
149
|
+
@available_capacity += 1
|
|
150
|
+
@available_capacity.positive?
|
|
151
|
+
end
|
|
152
|
+
@on_state_change&.call if should_notify
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def register_fatal_error(error)
|
|
156
|
+
@state_mutex.synchronize { @fatal_error ||= error }
|
|
157
|
+
@boot_queue << error if @boot_queue.empty?
|
|
158
|
+
@on_state_change&.call
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def raise_if_fatal!
|
|
162
|
+
error = @state_mutex.synchronize { @fatal_error }
|
|
163
|
+
raise error if error
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def wait_for_inflight
|
|
167
|
+
sleep(IDLE_WAIT_INTERVAL) while inflight?
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def inflight?
|
|
171
|
+
@mutex.synchronize { @available_capacity < @capacity }
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "concurrent"
|
|
4
|
+
|
|
5
|
+
module Pgbus
|
|
6
|
+
module ExecutionPools
|
|
7
|
+
class ThreadPool
|
|
8
|
+
attr_reader :capacity
|
|
9
|
+
|
|
10
|
+
def initialize(capacity:, on_state_change: nil)
|
|
11
|
+
@capacity = capacity
|
|
12
|
+
@on_state_change = on_state_change
|
|
13
|
+
@available_capacity = Concurrent::AtomicFixnum.new(capacity)
|
|
14
|
+
@pool = Concurrent::FixedThreadPool.new(capacity)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def post(&block)
|
|
18
|
+
@available_capacity.decrement
|
|
19
|
+
begin
|
|
20
|
+
@pool.post do
|
|
21
|
+
block.call
|
|
22
|
+
ensure
|
|
23
|
+
@available_capacity.increment
|
|
24
|
+
@on_state_change&.call
|
|
25
|
+
end
|
|
26
|
+
rescue StandardError
|
|
27
|
+
@available_capacity.increment
|
|
28
|
+
raise
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def available_capacity
|
|
33
|
+
@available_capacity.value
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def idle?
|
|
37
|
+
available_capacity.positive?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def shutdown
|
|
41
|
+
@pool.shutdown
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def wait_for_termination(timeout)
|
|
45
|
+
@pool.wait_for_termination(timeout)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def kill
|
|
49
|
+
@pool.kill
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def metadata
|
|
53
|
+
{
|
|
54
|
+
mode: :threads,
|
|
55
|
+
capacity: @capacity,
|
|
56
|
+
busy: @capacity - available_capacity
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
module ExecutionPools
|
|
5
|
+
class << self
|
|
6
|
+
def build(mode:, capacity:, on_state_change: nil)
|
|
7
|
+
case normalize_mode(mode)
|
|
8
|
+
when :threads
|
|
9
|
+
ThreadPool.new(capacity: capacity, on_state_change: on_state_change)
|
|
10
|
+
when :async
|
|
11
|
+
AsyncPool.new(capacity: capacity, on_state_change: on_state_change)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def normalize_mode(mode)
|
|
16
|
+
case mode.to_s
|
|
17
|
+
when "", "threads"
|
|
18
|
+
:threads
|
|
19
|
+
when "async", "fiber"
|
|
20
|
+
:async
|
|
21
|
+
else
|
|
22
|
+
raise ArgumentError, "Unknown execution_mode: #{mode.inspect}. Expected one of: :threads, :async, :fiber"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -7,14 +7,15 @@ module Pgbus
|
|
|
7
7
|
class Consumer
|
|
8
8
|
include SignalHandler
|
|
9
9
|
|
|
10
|
-
attr_reader :topics, :threads, :config
|
|
10
|
+
attr_reader :topics, :threads, :config, :execution_mode
|
|
11
11
|
|
|
12
|
-
def initialize(topics:, threads: 3, config: Pgbus.configuration)
|
|
12
|
+
def initialize(topics:, threads: 3, config: Pgbus.configuration, execution_mode: :threads)
|
|
13
13
|
@topics = Array(topics)
|
|
14
14
|
@threads = threads
|
|
15
15
|
@config = config
|
|
16
|
+
@execution_mode = ExecutionPools.normalize_mode(execution_mode)
|
|
16
17
|
@shutting_down = false
|
|
17
|
-
@pool =
|
|
18
|
+
@pool = ExecutionPools.build(mode: @execution_mode, capacity: threads)
|
|
18
19
|
@registry = EventBus::Registry.instance
|
|
19
20
|
end
|
|
20
21
|
|
|
@@ -53,7 +54,7 @@ module Pgbus
|
|
|
53
54
|
end
|
|
54
55
|
|
|
55
56
|
def consume
|
|
56
|
-
idle = @pool.
|
|
57
|
+
idle = @pool.available_capacity
|
|
57
58
|
return interruptible_sleep(config.polling_interval) if idle <= 0
|
|
58
59
|
|
|
59
60
|
tagged_messages = if @queue_names.size == 1
|
|
@@ -60,6 +60,7 @@ module Pgbus
|
|
|
60
60
|
threads = worker_config[:threads] || worker_config["threads"] || 5
|
|
61
61
|
single_active = worker_config[:single_active_consumer] || worker_config["single_active_consumer"] || false
|
|
62
62
|
priority = worker_config[:consumer_priority] || worker_config["consumer_priority"] || 0
|
|
63
|
+
exec_mode = config.execution_mode_for(worker_config)
|
|
63
64
|
|
|
64
65
|
pid = fork do
|
|
65
66
|
restore_signals
|
|
@@ -68,7 +69,8 @@ module Pgbus
|
|
|
68
69
|
bootstrap_queues
|
|
69
70
|
worker = Worker.new(
|
|
70
71
|
queues: queues, threads: threads, config: config,
|
|
71
|
-
single_active_consumer: single_active, consumer_priority: priority
|
|
72
|
+
single_active_consumer: single_active, consumer_priority: priority,
|
|
73
|
+
execution_mode: exec_mode
|
|
72
74
|
)
|
|
73
75
|
worker.run
|
|
74
76
|
end
|
|
@@ -79,7 +81,7 @@ module Pgbus
|
|
|
79
81
|
end
|
|
80
82
|
|
|
81
83
|
@forks[pid] = { type: :worker, config: worker_config }
|
|
82
|
-
Pgbus.logger.info { "[Pgbus] Forked worker pid=#{pid} queues=#{queues.join(",")}" }
|
|
84
|
+
Pgbus.logger.info { "[Pgbus] Forked worker pid=#{pid} queues=#{queues.join(",")} mode=#{exec_mode}" }
|
|
83
85
|
rescue Errno::EAGAIN, Errno::ENOMEM => e
|
|
84
86
|
Pgbus.logger.error { "[Pgbus] Fork failed for worker: #{e.message}" }
|
|
85
87
|
end
|
data/lib/pgbus/process/worker.rb
CHANGED
|
@@ -7,14 +7,16 @@ module Pgbus
|
|
|
7
7
|
class Worker
|
|
8
8
|
include SignalHandler
|
|
9
9
|
|
|
10
|
-
attr_reader :queues, :threads, :config
|
|
10
|
+
attr_reader :queues, :threads, :config, :execution_mode
|
|
11
11
|
|
|
12
12
|
def initialize(queues:, threads: 5, config: Pgbus.configuration,
|
|
13
|
-
single_active_consumer: false, consumer_priority: 0
|
|
13
|
+
single_active_consumer: false, consumer_priority: 0,
|
|
14
|
+
execution_mode: :threads)
|
|
14
15
|
@queues = Array(queues)
|
|
15
16
|
@wildcard = @queues.include?("*")
|
|
16
17
|
@threads = threads
|
|
17
18
|
@config = config
|
|
19
|
+
@execution_mode = ExecutionPools.normalize_mode(execution_mode)
|
|
18
20
|
@single_active_consumer = single_active_consumer
|
|
19
21
|
@consumer_priority = consumer_priority
|
|
20
22
|
@lifecycle = Lifecycle.new
|
|
@@ -27,10 +29,14 @@ module Pgbus
|
|
|
27
29
|
@started_at_monotonic = monotonic_now
|
|
28
30
|
@stat_buffer = config.stats_enabled ? Pgbus::StatBuffer.new : nil
|
|
29
31
|
@executor = Pgbus::ActiveJob::Executor.new(stat_buffer: @stat_buffer)
|
|
30
|
-
@
|
|
32
|
+
@wake_signal = WakeSignal.new
|
|
33
|
+
@pool = ExecutionPools.build(
|
|
34
|
+
mode: @execution_mode,
|
|
35
|
+
capacity: threads,
|
|
36
|
+
on_state_change: -> { @wake_signal.notify! }
|
|
37
|
+
)
|
|
31
38
|
@circuit_breaker = Pgbus::CircuitBreaker.new(config: config)
|
|
32
39
|
@queue_lock = QueueLock.new if @single_active_consumer
|
|
33
|
-
@wake_signal = WakeSignal.new
|
|
34
40
|
end
|
|
35
41
|
|
|
36
42
|
def stats
|
|
@@ -39,12 +45,13 @@ module Pgbus
|
|
|
39
45
|
jobs_failed: @jobs_failed.value,
|
|
40
46
|
in_flight: @in_flight.value,
|
|
41
47
|
state: @lifecycle.state,
|
|
48
|
+
execution_mode: @execution_mode,
|
|
42
49
|
consumer_priority: @consumer_priority,
|
|
43
50
|
single_active_consumer: @single_active_consumer,
|
|
44
51
|
locked_queues: @queue_lock&.held_queues || [],
|
|
45
52
|
rates: @rate_counter.rates,
|
|
46
53
|
started_at: @started_at
|
|
47
|
-
}
|
|
54
|
+
}.merge(@pool.metadata)
|
|
48
55
|
end
|
|
49
56
|
|
|
50
57
|
def run
|
|
@@ -52,7 +59,10 @@ module Pgbus
|
|
|
52
59
|
start_heartbeat
|
|
53
60
|
resolve_wildcard_queues
|
|
54
61
|
@lifecycle.transition_to!(:running)
|
|
55
|
-
Pgbus.logger.info
|
|
62
|
+
Pgbus.logger.info do
|
|
63
|
+
"[Pgbus] Worker started: queues=#{queues.join(",")} threads=#{threads} " \
|
|
64
|
+
"mode=#{@execution_mode} pid=#{::Process.pid}"
|
|
65
|
+
end
|
|
56
66
|
|
|
57
67
|
loop do
|
|
58
68
|
process_signals
|
|
@@ -60,7 +70,7 @@ module Pgbus
|
|
|
60
70
|
refresh_wildcard_queues
|
|
61
71
|
|
|
62
72
|
break if @lifecycle.stopped?
|
|
63
|
-
break if @lifecycle.draining? && @pool.
|
|
73
|
+
break if @lifecycle.draining? && @pool.idle?
|
|
64
74
|
|
|
65
75
|
claim_and_execute if @lifecycle.can_process?
|
|
66
76
|
@stat_buffer&.flush_if_due
|
|
@@ -97,7 +107,7 @@ module Pgbus
|
|
|
97
107
|
def claim_and_execute
|
|
98
108
|
poll_interval = effective_polling_interval
|
|
99
109
|
|
|
100
|
-
idle = @pool.
|
|
110
|
+
idle = @pool.available_capacity
|
|
101
111
|
return @wake_signal.wait(timeout: poll_interval) if idle <= 0
|
|
102
112
|
|
|
103
113
|
if config.prefetch_limit
|
|
@@ -332,7 +342,7 @@ module Pgbus
|
|
|
332
342
|
kind: "worker",
|
|
333
343
|
metadata: {
|
|
334
344
|
queues: queues, threads: threads, pid: ::Process.pid,
|
|
335
|
-
consumer_priority: @consumer_priority
|
|
345
|
+
execution_mode: @execution_mode, consumer_priority: @consumer_priority
|
|
336
346
|
},
|
|
337
347
|
on_beat: -> { @rate_counter.snapshot! }
|
|
338
348
|
)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
module Streams
|
|
5
|
+
# Runtime patch that redirects `turbo_stream_from` (the view helper)
|
|
6
|
+
# through `pgbus_stream_from` when pgbus streams are enabled. This is
|
|
7
|
+
# the subscribe-side counterpart to `TurboBroadcastable` (which patches
|
|
8
|
+
# the publish-side `broadcast_stream_to`).
|
|
9
|
+
#
|
|
10
|
+
# Without this patch, third-party gems like hotwire-livereload call
|
|
11
|
+
# `turbo_stream_from "hotwire-livereload"` in their views, which
|
|
12
|
+
# renders a `<turbo-cable-stream-source>` element connected to
|
|
13
|
+
# ActionCable. Meanwhile, the `TurboBroadcastable` patch routes the
|
|
14
|
+
# broadcast through PGMQ/SSE. Publisher and subscriber end up on
|
|
15
|
+
# different transports — the message never arrives.
|
|
16
|
+
#
|
|
17
|
+
# After this patch, `turbo_stream_from` renders a `<pgbus-stream-source>`
|
|
18
|
+
# element instead, so both sides use PGMQ/SSE. When `streams_enabled`
|
|
19
|
+
# is false, the original turbo-rails behavior is preserved via `super`.
|
|
20
|
+
module TurboStreamOverride
|
|
21
|
+
def turbo_stream_from(*streamables, **attributes)
|
|
22
|
+
if Pgbus.configuration.streams_enabled
|
|
23
|
+
pgbus_stream_from(*streamables, **attributes)
|
|
24
|
+
else
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Apply the patch to Turbo::StreamsHelper. Idempotent: prepending the
|
|
31
|
+
# same module twice is a no-op. Called from Pgbus::Engine's initializer
|
|
32
|
+
# when both turbo-rails and pgbus streams are enabled.
|
|
33
|
+
def self.install_turbo_stream_override!
|
|
34
|
+
return unless defined?(::Turbo::StreamsHelper)
|
|
35
|
+
return if ::Turbo::StreamsHelper.ancestors.include?(TurboStreamOverride)
|
|
36
|
+
|
|
37
|
+
::Turbo::StreamsHelper.prepend(TurboStreamOverride)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/pgbus/version.rb
CHANGED
data/lib/pgbus/web/stream_app.rb
CHANGED
|
@@ -62,11 +62,15 @@ module Pgbus
|
|
|
62
62
|
cursor = parse_cursor(env, request)
|
|
63
63
|
return bad_request("invalid cursor: #{cursor}") if cursor.is_a?(String)
|
|
64
64
|
|
|
65
|
-
return unsupported_server unless env["rack.hijack?"]
|
|
66
|
-
|
|
67
65
|
return over_capacity if streamer.registry.size >= config.streams_max_connections
|
|
68
66
|
|
|
69
|
-
|
|
67
|
+
if config.streams_falcon_streaming_body
|
|
68
|
+
streaming_body_response(stream_name: stream_name, since_id: cursor, context: context)
|
|
69
|
+
elsif env["rack.hijack?"]
|
|
70
|
+
hijack_and_register(env, stream_name: stream_name, since_id: cursor, context: context)
|
|
71
|
+
else
|
|
72
|
+
unsupported_server
|
|
73
|
+
end
|
|
70
74
|
rescue StandardError => e
|
|
71
75
|
logger.error { "[Pgbus::StreamApp] #{e.class}: #{e.message}" }
|
|
72
76
|
server_error
|
|
@@ -127,6 +131,38 @@ module Pgbus
|
|
|
127
131
|
[-1, {}, []]
|
|
128
132
|
end
|
|
129
133
|
|
|
134
|
+
def streaming_body_response(stream_name:, since_id:, context: nil)
|
|
135
|
+
require "protocol/http/body/writable" unless defined?(::Protocol::HTTP::Body::Writable)
|
|
136
|
+
require_relative "streamer/falcon_connection" unless defined?(Pgbus::Web::Streamer::FalconConnection)
|
|
137
|
+
|
|
138
|
+
body = ::Protocol::HTTP::Body::Writable.new
|
|
139
|
+
|
|
140
|
+
body.write(Pgbus::Streams::Envelope.retry_directive(2_000))
|
|
141
|
+
body.write(Pgbus::Streams::Envelope.comment(
|
|
142
|
+
"pgbus stream open since_id=#{since_id} stream=#{stream_name}"
|
|
143
|
+
))
|
|
144
|
+
|
|
145
|
+
connection = Pgbus::Web::Streamer::FalconConnection.new(
|
|
146
|
+
id: SecureRandom.hex(8),
|
|
147
|
+
stream_name: stream_name,
|
|
148
|
+
body: body,
|
|
149
|
+
since_id: since_id,
|
|
150
|
+
write_deadline_ms: config.streams_write_deadline_ms,
|
|
151
|
+
context: context
|
|
152
|
+
)
|
|
153
|
+
streamer.register(connection)
|
|
154
|
+
|
|
155
|
+
[200, sse_headers, body]
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def sse_headers
|
|
159
|
+
{
|
|
160
|
+
"content-type" => "text/event-stream",
|
|
161
|
+
"cache-control" => "no-cache, no-transform",
|
|
162
|
+
"x-accel-buffering" => "no"
|
|
163
|
+
}
|
|
164
|
+
end
|
|
165
|
+
|
|
130
166
|
def write_headers(io, stream_name:, since_id:)
|
|
131
167
|
io.write(Pgbus::Streams::Envelope.http_response_headers)
|
|
132
168
|
io.write(Pgbus::Streams::Envelope.retry_directive(2_000))
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
module Web
|
|
5
|
+
module Streamer
|
|
6
|
+
# Connection adapter for Falcon's native streaming body path.
|
|
7
|
+
# Wraps a Protocol::HTTP::Body::Writable instead of a raw IO socket.
|
|
8
|
+
#
|
|
9
|
+
# Satisfies the same duck-type interface as Connection so the
|
|
10
|
+
# Dispatcher, Heartbeat, and Instance can use either type
|
|
11
|
+
# interchangeably. Key difference: no IoWriter — writes go
|
|
12
|
+
# directly to body.write which is backed by Thread::Queue and
|
|
13
|
+
# is fiber-safe under Falcon's scheduler.
|
|
14
|
+
class FalconConnection
|
|
15
|
+
attr_reader :id, :stream_name, :io, :mutex, :last_msg_id_sent, :context
|
|
16
|
+
|
|
17
|
+
def initialize(id:, stream_name:, body:, since_id:, write_deadline_ms:, context: nil)
|
|
18
|
+
@id = id
|
|
19
|
+
@stream_name = stream_name
|
|
20
|
+
@body = body
|
|
21
|
+
@io = body
|
|
22
|
+
@last_msg_id_sent = since_id.to_i
|
|
23
|
+
@write_deadline_ms = write_deadline_ms
|
|
24
|
+
@mutex = Mutex.new
|
|
25
|
+
@dead = false
|
|
26
|
+
@closed = false
|
|
27
|
+
@created_at = monotonic
|
|
28
|
+
@last_write_at = @created_at
|
|
29
|
+
@context = context
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def enqueue(envelopes)
|
|
33
|
+
written = []
|
|
34
|
+
envelopes.each do |envelope|
|
|
35
|
+
next if envelope.msg_id <= @last_msg_id_sent
|
|
36
|
+
|
|
37
|
+
bytes = Pgbus::Streams::Envelope.message(
|
|
38
|
+
id: envelope.msg_id,
|
|
39
|
+
event: "turbo-stream",
|
|
40
|
+
data: envelope.payload
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
result = write_to_body(bytes)
|
|
44
|
+
if result == :ok
|
|
45
|
+
@last_msg_id_sent = envelope.msg_id
|
|
46
|
+
@last_write_at = monotonic
|
|
47
|
+
written << envelope
|
|
48
|
+
else
|
|
49
|
+
mark_dead!
|
|
50
|
+
break
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
written
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def write_comment(text)
|
|
57
|
+
bytes = Pgbus::Streams::Envelope.comment(text)
|
|
58
|
+
result = write_to_body(bytes)
|
|
59
|
+
if result == :ok
|
|
60
|
+
@last_write_at = monotonic
|
|
61
|
+
else
|
|
62
|
+
mark_dead!
|
|
63
|
+
end
|
|
64
|
+
result
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def write_sentinel(bytes)
|
|
68
|
+
write_to_body(bytes)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def close
|
|
72
|
+
@mutex.synchronize do
|
|
73
|
+
return if @closed
|
|
74
|
+
|
|
75
|
+
@closed = true
|
|
76
|
+
mark_dead!
|
|
77
|
+
@body.close_write
|
|
78
|
+
end
|
|
79
|
+
rescue StandardError => e
|
|
80
|
+
Pgbus.logger&.debug { "[Pgbus::Streamer::FalconConnection] close failed: #{e.class}: #{e.message}" }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def idle_for
|
|
84
|
+
monotonic - @last_write_at
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def dead?
|
|
88
|
+
@dead || @body.closed?
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def mark_dead!
|
|
92
|
+
@dead = true
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def write_to_body(bytes)
|
|
98
|
+
@mutex.synchronize do
|
|
99
|
+
return :closed if @dead || @body.closed?
|
|
100
|
+
|
|
101
|
+
@body.write(bytes)
|
|
102
|
+
:ok
|
|
103
|
+
end
|
|
104
|
+
rescue Protocol::HTTP::Body::Writable::Closed
|
|
105
|
+
:closed
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def monotonic
|
|
109
|
+
::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -134,14 +134,9 @@ module Pgbus
|
|
|
134
134
|
event: "pgbus:shutdown",
|
|
135
135
|
data: '{"reason":"worker_restart"}'
|
|
136
136
|
)
|
|
137
|
-
deadline_ms = @config.streams_write_deadline_ms
|
|
138
137
|
|
|
139
138
|
@registry.each_connection do |connection|
|
|
140
|
-
|
|
141
|
-
# serialised against any write the dispatcher/heartbeat
|
|
142
|
-
# might still be performing if their stop hadn't fully
|
|
143
|
-
# returned yet.
|
|
144
|
-
safely { IoWriter.write(connection, sentinel_bytes, deadline_ms: deadline_ms) }
|
|
139
|
+
safely { connection.write_sentinel(sentinel_bytes) }
|
|
145
140
|
safely { connection.close }
|
|
146
141
|
end
|
|
147
142
|
end
|
|
@@ -113,6 +113,14 @@ module Pgbus
|
|
|
113
113
|
@conn.wait_for_notify(timeout_s) do |channel, _pid, _payload|
|
|
114
114
|
handle_notify(channel)
|
|
115
115
|
end || run_health_check
|
|
116
|
+
rescue IOError => e
|
|
117
|
+
# #stop closes the PG connection to interrupt
|
|
118
|
+
# wait_for_notify, which raises IOError ("stream closed
|
|
119
|
+
# in another thread"). This is expected — exit cleanly.
|
|
120
|
+
break unless @running
|
|
121
|
+
|
|
122
|
+
@logger.warn { "[Pgbus::Streamer::Listener] IO error (#{e.class}: #{e.message}) — reconnecting" }
|
|
123
|
+
reconnect!
|
|
116
124
|
rescue PG::Error => e
|
|
117
125
|
break unless @running
|
|
118
126
|
|
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.6.
|
|
4
|
+
version: 0.6.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mikael Henriksson
|
|
@@ -256,6 +256,9 @@ files:
|
|
|
256
256
|
- lib/pgbus/event_bus/publisher.rb
|
|
257
257
|
- lib/pgbus/event_bus/registry.rb
|
|
258
258
|
- lib/pgbus/event_bus/subscriber.rb
|
|
259
|
+
- lib/pgbus/execution_pools.rb
|
|
260
|
+
- lib/pgbus/execution_pools/async_pool.rb
|
|
261
|
+
- lib/pgbus/execution_pools/thread_pool.rb
|
|
259
262
|
- lib/pgbus/failed_event_recorder.rb
|
|
260
263
|
- lib/pgbus/generators/config_converter.rb
|
|
261
264
|
- lib/pgbus/generators/database_target_detector.rb
|
|
@@ -294,6 +297,7 @@ files:
|
|
|
294
297
|
- lib/pgbus/streams/presence.rb
|
|
295
298
|
- lib/pgbus/streams/signed_name.rb
|
|
296
299
|
- lib/pgbus/streams/turbo_broadcastable.rb
|
|
300
|
+
- lib/pgbus/streams/turbo_stream_override.rb
|
|
297
301
|
- lib/pgbus/streams/watermark_cache_middleware.rb
|
|
298
302
|
- lib/pgbus/uniqueness.rb
|
|
299
303
|
- lib/pgbus/version.rb
|
|
@@ -303,6 +307,7 @@ files:
|
|
|
303
307
|
- lib/pgbus/web/stream_app.rb
|
|
304
308
|
- lib/pgbus/web/streamer.rb
|
|
305
309
|
- lib/pgbus/web/streamer/connection.rb
|
|
310
|
+
- lib/pgbus/web/streamer/falcon_connection.rb
|
|
306
311
|
- lib/pgbus/web/streamer/heartbeat.rb
|
|
307
312
|
- lib/pgbus/web/streamer/instance.rb
|
|
308
313
|
- lib/pgbus/web/streamer/io_writer.rb
|