solid_queue 0.1.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +230 -0
- data/Rakefile +8 -0
- data/app/models/solid_queue/blocked_execution.rb +68 -0
- data/app/models/solid_queue/claimed_execution.rb +73 -0
- data/app/models/solid_queue/execution/job_attributes.rb +24 -0
- data/app/models/solid_queue/execution.rb +15 -0
- data/app/models/solid_queue/failed_execution.rb +31 -0
- data/app/models/solid_queue/job/clearable.rb +19 -0
- data/app/models/solid_queue/job/concurrency_controls.rb +50 -0
- data/app/models/solid_queue/job/executable.rb +87 -0
- data/app/models/solid_queue/job.rb +38 -0
- data/app/models/solid_queue/pause.rb +6 -0
- data/app/models/solid_queue/process/prunable.rb +20 -0
- data/app/models/solid_queue/process.rb +28 -0
- data/app/models/solid_queue/queue.rb +52 -0
- data/app/models/solid_queue/queue_selector.rb +68 -0
- data/app/models/solid_queue/ready_execution.rb +41 -0
- data/app/models/solid_queue/record.rb +19 -0
- data/app/models/solid_queue/scheduled_execution.rb +65 -0
- data/app/models/solid_queue/semaphore.rb +65 -0
- data/config/routes.rb +2 -0
- data/db/migrate/20231211200639_create_solid_queue_tables.rb +100 -0
- data/lib/active_job/concurrency_controls.rb +53 -0
- data/lib/active_job/queue_adapters/solid_queue_adapter.rb +24 -0
- data/lib/generators/solid_queue/install/USAGE +9 -0
- data/lib/generators/solid_queue/install/install_generator.rb +19 -0
- data/lib/puma/plugin/solid_queue.rb +63 -0
- data/lib/solid_queue/app_executor.rb +21 -0
- data/lib/solid_queue/configuration.rb +102 -0
- data/lib/solid_queue/dispatcher.rb +73 -0
- data/lib/solid_queue/engine.rb +39 -0
- data/lib/solid_queue/pool.rb +58 -0
- data/lib/solid_queue/processes/base.rb +27 -0
- data/lib/solid_queue/processes/interruptible.rb +37 -0
- data/lib/solid_queue/processes/pidfile.rb +58 -0
- data/lib/solid_queue/processes/poller.rb +24 -0
- data/lib/solid_queue/processes/procline.rb +11 -0
- data/lib/solid_queue/processes/registrable.rb +69 -0
- data/lib/solid_queue/processes/runnable.rb +77 -0
- data/lib/solid_queue/processes/signals.rb +69 -0
- data/lib/solid_queue/processes/supervised.rb +38 -0
- data/lib/solid_queue/supervisor.rb +182 -0
- data/lib/solid_queue/tasks.rb +16 -0
- data/lib/solid_queue/version.rb +3 -0
- data/lib/solid_queue/worker.rb +54 -0
- data/lib/solid_queue.rb +52 -0
- metadata +134 -0
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidQueue::Processes
|
4
|
+
module Supervised
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
attr_reader :supervisor
|
9
|
+
end
|
10
|
+
|
11
|
+
def supervised_by(process)
|
12
|
+
self.mode = :supervised
|
13
|
+
@supervisor = process
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
def supervisor_went_away?
|
18
|
+
supervised? && supervisor&.pid != ::Process.ppid
|
19
|
+
end
|
20
|
+
|
21
|
+
def supervised?
|
22
|
+
mode.supervised?
|
23
|
+
end
|
24
|
+
|
25
|
+
def register_signal_handlers
|
26
|
+
%w[ INT TERM ].each do |signal|
|
27
|
+
trap(signal) do
|
28
|
+
stop
|
29
|
+
interrupt
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
trap(:QUIT) do
|
34
|
+
exit!
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,182 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidQueue
|
4
|
+
class Supervisor < Processes::Base
|
5
|
+
include Processes::Signals
|
6
|
+
|
7
|
+
set_callback :boot, :after, :launch_process_prune
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def start(mode: :work, load_configuration_from: nil)
|
11
|
+
SolidQueue.supervisor = true
|
12
|
+
configuration = Configuration.new(mode: mode, load_from: load_configuration_from)
|
13
|
+
|
14
|
+
new(*configuration.processes).start
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(*configured_processes)
|
19
|
+
@configured_processes = Array(configured_processes)
|
20
|
+
@forks = {}
|
21
|
+
end
|
22
|
+
|
23
|
+
def start
|
24
|
+
run_callbacks(:boot) { boot }
|
25
|
+
|
26
|
+
supervise
|
27
|
+
rescue Processes::GracefulTerminationRequested
|
28
|
+
graceful_termination
|
29
|
+
rescue Processes::ImmediateTerminationRequested
|
30
|
+
immediate_termination
|
31
|
+
ensure
|
32
|
+
run_callbacks(:shutdown) { shutdown }
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
attr_reader :configured_processes, :forks
|
37
|
+
|
38
|
+
def boot
|
39
|
+
sync_std_streams
|
40
|
+
setup_pidfile
|
41
|
+
register_signal_handlers
|
42
|
+
end
|
43
|
+
|
44
|
+
def supervise
|
45
|
+
start_forks
|
46
|
+
|
47
|
+
loop do
|
48
|
+
procline "supervising #{forks.keys.join(", ")}"
|
49
|
+
|
50
|
+
process_signal_queue
|
51
|
+
reap_and_replace_terminated_forks
|
52
|
+
interruptible_sleep(1.second)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def sync_std_streams
|
57
|
+
STDOUT.sync = STDERR.sync = true
|
58
|
+
end
|
59
|
+
|
60
|
+
def setup_pidfile
|
61
|
+
@pidfile = if SolidQueue.supervisor_pidfile
|
62
|
+
Processes::Pidfile.new(SolidQueue.supervisor_pidfile).tap(&:setup)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def launch_process_prune
|
67
|
+
@prune_task = Concurrent::TimerTask.new(run_now: true, execution_interval: SolidQueue.process_alive_threshold) { prune_dead_processes }
|
68
|
+
@prune_task.execute
|
69
|
+
end
|
70
|
+
|
71
|
+
def start_forks
|
72
|
+
configured_processes.each { |configured_process| start_fork(configured_process) }
|
73
|
+
end
|
74
|
+
|
75
|
+
def shutdown
|
76
|
+
stop_process_prune
|
77
|
+
restore_default_signal_handlers
|
78
|
+
delete_pidfile
|
79
|
+
end
|
80
|
+
|
81
|
+
def graceful_termination
|
82
|
+
procline "terminating gracefully"
|
83
|
+
term_forks
|
84
|
+
|
85
|
+
wait_until(SolidQueue.shutdown_timeout, -> { all_forks_terminated? }) do
|
86
|
+
reap_terminated_forks
|
87
|
+
end
|
88
|
+
|
89
|
+
immediate_termination unless all_forks_terminated?
|
90
|
+
end
|
91
|
+
|
92
|
+
def immediate_termination
|
93
|
+
procline "terminating immediately"
|
94
|
+
quit_forks
|
95
|
+
end
|
96
|
+
|
97
|
+
def term_forks
|
98
|
+
signal_processes(forks.keys, :TERM)
|
99
|
+
end
|
100
|
+
|
101
|
+
def quit_forks
|
102
|
+
signal_processes(forks.keys, :QUIT)
|
103
|
+
end
|
104
|
+
|
105
|
+
def stop_process_prune
|
106
|
+
@prune_task&.shutdown
|
107
|
+
end
|
108
|
+
|
109
|
+
def delete_pidfile
|
110
|
+
@pidfile&.delete
|
111
|
+
end
|
112
|
+
|
113
|
+
def prune_dead_processes
|
114
|
+
wrap_in_app_executor do
|
115
|
+
SolidQueue::Process.prune
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def start_fork(configured_process)
|
120
|
+
configured_process.supervised_by process
|
121
|
+
|
122
|
+
pid = fork do
|
123
|
+
configured_process.start
|
124
|
+
end
|
125
|
+
|
126
|
+
forks[pid] = configured_process
|
127
|
+
end
|
128
|
+
|
129
|
+
def reap_and_replace_terminated_forks
|
130
|
+
loop do
|
131
|
+
pid, status = ::Process.waitpid2(-1, ::Process::WNOHANG)
|
132
|
+
break unless pid
|
133
|
+
|
134
|
+
replace_fork(pid, status)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def reap_terminated_forks
|
139
|
+
loop do
|
140
|
+
pid, status = ::Process.waitpid2(-1, ::Process::WNOHANG)
|
141
|
+
break unless pid
|
142
|
+
|
143
|
+
forks.delete(pid)
|
144
|
+
end
|
145
|
+
rescue SystemCallError
|
146
|
+
# All children already reaped
|
147
|
+
end
|
148
|
+
|
149
|
+
def replace_fork(pid, status)
|
150
|
+
if supervised_fork = forks.delete(pid)
|
151
|
+
SolidQueue.logger.info "[SolidQueue] Restarting fork[#{status.pid}] (status: #{status.exitstatus})"
|
152
|
+
start_fork(supervised_fork)
|
153
|
+
else
|
154
|
+
SolidQueue.logger.info "[SolidQueue] Tried to replace fork[#{pid}] (status: #{status.exitstatus}, fork[#{status.pid}]), but it had already died (status: #{status.exitstatus})"
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def all_forks_terminated?
|
159
|
+
forks.empty?
|
160
|
+
end
|
161
|
+
|
162
|
+
def wait_until(timeout, condition, &block)
|
163
|
+
if timeout > 0
|
164
|
+
deadline = monotonic_time_now + timeout
|
165
|
+
|
166
|
+
while monotonic_time_now < deadline && !condition.call
|
167
|
+
sleep 0.1
|
168
|
+
block.call
|
169
|
+
end
|
170
|
+
else
|
171
|
+
while !condition.call
|
172
|
+
sleep 0.5
|
173
|
+
block.call
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def monotonic_time_now
|
179
|
+
::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
namespace :solid_queue do
|
2
|
+
desc "start solid_queue supervisor to dispatch and process jobs"
|
3
|
+
task start: :environment do
|
4
|
+
SolidQueue::Supervisor.start(mode: :all)
|
5
|
+
end
|
6
|
+
|
7
|
+
desc "start solid_queue supervisor to process jobs"
|
8
|
+
task work: :environment do
|
9
|
+
SolidQueue::Supervisor.start(mode: :work)
|
10
|
+
end
|
11
|
+
|
12
|
+
desc "start solid_queue dispatcher to enqueue scheduled jobs"
|
13
|
+
task dispatch: :environment do
|
14
|
+
SolidQueue::Supervisor.start(mode: :dispatch)
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidQueue
|
4
|
+
class Worker < Processes::Base
|
5
|
+
include Processes::Runnable, Processes::Poller
|
6
|
+
|
7
|
+
attr_accessor :queues, :pool
|
8
|
+
|
9
|
+
def initialize(**options)
|
10
|
+
options = options.dup.with_defaults(SolidQueue::Configuration::WORKER_DEFAULTS)
|
11
|
+
|
12
|
+
@polling_interval = options[:polling_interval]
|
13
|
+
@queues = Array(options[:queues])
|
14
|
+
@pool = Pool.new(options[:threads], on_idle: -> { wake_up })
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
def run
|
19
|
+
polled_executions = poll
|
20
|
+
|
21
|
+
if polled_executions.size > 0
|
22
|
+
procline "performing #{polled_executions.count} jobs"
|
23
|
+
|
24
|
+
polled_executions.each do |execution|
|
25
|
+
pool.post(execution)
|
26
|
+
end
|
27
|
+
else
|
28
|
+
procline "waiting for jobs in #{queues.join(",")}"
|
29
|
+
interruptible_sleep(polling_interval)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def poll
|
34
|
+
with_polling_volume do
|
35
|
+
SolidQueue::ReadyExecution.claim(queues, pool.idle_threads, process.id)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def shutdown
|
40
|
+
super
|
41
|
+
|
42
|
+
pool.shutdown
|
43
|
+
pool.wait_for_termination(SolidQueue.shutdown_timeout)
|
44
|
+
end
|
45
|
+
|
46
|
+
def all_work_completed?
|
47
|
+
SolidQueue::ReadyExecution.queued_as(queues).empty?
|
48
|
+
end
|
49
|
+
|
50
|
+
def metadata
|
51
|
+
super.merge(queues: queues.join(","), thread_pool_size: pool.size)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/solid_queue.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "solid_queue/version"
|
4
|
+
require "solid_queue/engine"
|
5
|
+
|
6
|
+
require "active_job/queue_adapters/solid_queue_adapter"
|
7
|
+
require "active_job/concurrency_controls"
|
8
|
+
|
9
|
+
require "solid_queue/app_executor"
|
10
|
+
require "solid_queue/processes/supervised"
|
11
|
+
require "solid_queue/processes/registrable"
|
12
|
+
require "solid_queue/processes/interruptible"
|
13
|
+
require "solid_queue/processes/pidfile"
|
14
|
+
require "solid_queue/processes/procline"
|
15
|
+
require "solid_queue/processes/poller"
|
16
|
+
require "solid_queue/processes/base"
|
17
|
+
require "solid_queue/processes/runnable"
|
18
|
+
require "solid_queue/processes/signals"
|
19
|
+
require "solid_queue/configuration"
|
20
|
+
require "solid_queue/pool"
|
21
|
+
require "solid_queue/worker"
|
22
|
+
require "solid_queue/dispatcher"
|
23
|
+
require "solid_queue/supervisor"
|
24
|
+
|
25
|
+
module SolidQueue
|
26
|
+
mattr_accessor :logger, default: ActiveSupport::Logger.new($stdout)
|
27
|
+
mattr_accessor :app_executor, :on_thread_error, :connects_to
|
28
|
+
|
29
|
+
mattr_accessor :use_skip_locked, default: true
|
30
|
+
|
31
|
+
mattr_accessor :process_heartbeat_interval, default: 60.seconds
|
32
|
+
mattr_accessor :process_alive_threshold, default: 5.minutes
|
33
|
+
|
34
|
+
mattr_accessor :shutdown_timeout, default: 5.seconds
|
35
|
+
|
36
|
+
mattr_accessor :silence_polling, default: false
|
37
|
+
|
38
|
+
mattr_accessor :supervisor_pidfile
|
39
|
+
mattr_accessor :supervisor, default: false
|
40
|
+
|
41
|
+
mattr_accessor :preserve_finished_jobs, default: true
|
42
|
+
mattr_accessor :clear_finished_jobs_after, default: 1.day
|
43
|
+
mattr_accessor :default_concurrency_control_period, default: 3.minutes
|
44
|
+
|
45
|
+
def self.supervisor?
|
46
|
+
supervisor
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.silence_polling?
|
50
|
+
silence_polling
|
51
|
+
end
|
52
|
+
end
|
metadata
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: solid_queue
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Rosa Gutierrez
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-12-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 7.0.3.1
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 7.0.3.1
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: debug
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: mocha
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description: Database-backed Active Job backend.
|
56
|
+
email:
|
57
|
+
- rosa@37signals.com
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- MIT-LICENSE
|
63
|
+
- README.md
|
64
|
+
- Rakefile
|
65
|
+
- app/models/solid_queue/blocked_execution.rb
|
66
|
+
- app/models/solid_queue/claimed_execution.rb
|
67
|
+
- app/models/solid_queue/execution.rb
|
68
|
+
- app/models/solid_queue/execution/job_attributes.rb
|
69
|
+
- app/models/solid_queue/failed_execution.rb
|
70
|
+
- app/models/solid_queue/job.rb
|
71
|
+
- app/models/solid_queue/job/clearable.rb
|
72
|
+
- app/models/solid_queue/job/concurrency_controls.rb
|
73
|
+
- app/models/solid_queue/job/executable.rb
|
74
|
+
- app/models/solid_queue/pause.rb
|
75
|
+
- app/models/solid_queue/process.rb
|
76
|
+
- app/models/solid_queue/process/prunable.rb
|
77
|
+
- app/models/solid_queue/queue.rb
|
78
|
+
- app/models/solid_queue/queue_selector.rb
|
79
|
+
- app/models/solid_queue/ready_execution.rb
|
80
|
+
- app/models/solid_queue/record.rb
|
81
|
+
- app/models/solid_queue/scheduled_execution.rb
|
82
|
+
- app/models/solid_queue/semaphore.rb
|
83
|
+
- config/routes.rb
|
84
|
+
- db/migrate/20231211200639_create_solid_queue_tables.rb
|
85
|
+
- lib/active_job/concurrency_controls.rb
|
86
|
+
- lib/active_job/queue_adapters/solid_queue_adapter.rb
|
87
|
+
- lib/generators/solid_queue/install/USAGE
|
88
|
+
- lib/generators/solid_queue/install/install_generator.rb
|
89
|
+
- lib/puma/plugin/solid_queue.rb
|
90
|
+
- lib/solid_queue.rb
|
91
|
+
- lib/solid_queue/app_executor.rb
|
92
|
+
- lib/solid_queue/configuration.rb
|
93
|
+
- lib/solid_queue/dispatcher.rb
|
94
|
+
- lib/solid_queue/engine.rb
|
95
|
+
- lib/solid_queue/pool.rb
|
96
|
+
- lib/solid_queue/processes/base.rb
|
97
|
+
- lib/solid_queue/processes/interruptible.rb
|
98
|
+
- lib/solid_queue/processes/pidfile.rb
|
99
|
+
- lib/solid_queue/processes/poller.rb
|
100
|
+
- lib/solid_queue/processes/procline.rb
|
101
|
+
- lib/solid_queue/processes/registrable.rb
|
102
|
+
- lib/solid_queue/processes/runnable.rb
|
103
|
+
- lib/solid_queue/processes/signals.rb
|
104
|
+
- lib/solid_queue/processes/supervised.rb
|
105
|
+
- lib/solid_queue/supervisor.rb
|
106
|
+
- lib/solid_queue/tasks.rb
|
107
|
+
- lib/solid_queue/version.rb
|
108
|
+
- lib/solid_queue/worker.rb
|
109
|
+
homepage: http://github.com/basecamp/solid_queue
|
110
|
+
licenses:
|
111
|
+
- MIT
|
112
|
+
metadata:
|
113
|
+
homepage_uri: http://github.com/basecamp/solid_queue
|
114
|
+
source_code_uri: http://github.com/basecamp/solid_queue
|
115
|
+
post_install_message:
|
116
|
+
rdoc_options: []
|
117
|
+
require_paths:
|
118
|
+
- lib
|
119
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
125
|
+
requirements:
|
126
|
+
- - ">="
|
127
|
+
- !ruby/object:Gem::Version
|
128
|
+
version: '0'
|
129
|
+
requirements: []
|
130
|
+
rubygems_version: 3.4.20
|
131
|
+
signing_key:
|
132
|
+
specification_version: 4
|
133
|
+
summary: Database-backed Active Job backend.
|
134
|
+
test_files: []
|