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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +230 -0
  4. data/Rakefile +8 -0
  5. data/app/models/solid_queue/blocked_execution.rb +68 -0
  6. data/app/models/solid_queue/claimed_execution.rb +73 -0
  7. data/app/models/solid_queue/execution/job_attributes.rb +24 -0
  8. data/app/models/solid_queue/execution.rb +15 -0
  9. data/app/models/solid_queue/failed_execution.rb +31 -0
  10. data/app/models/solid_queue/job/clearable.rb +19 -0
  11. data/app/models/solid_queue/job/concurrency_controls.rb +50 -0
  12. data/app/models/solid_queue/job/executable.rb +87 -0
  13. data/app/models/solid_queue/job.rb +38 -0
  14. data/app/models/solid_queue/pause.rb +6 -0
  15. data/app/models/solid_queue/process/prunable.rb +20 -0
  16. data/app/models/solid_queue/process.rb +28 -0
  17. data/app/models/solid_queue/queue.rb +52 -0
  18. data/app/models/solid_queue/queue_selector.rb +68 -0
  19. data/app/models/solid_queue/ready_execution.rb +41 -0
  20. data/app/models/solid_queue/record.rb +19 -0
  21. data/app/models/solid_queue/scheduled_execution.rb +65 -0
  22. data/app/models/solid_queue/semaphore.rb +65 -0
  23. data/config/routes.rb +2 -0
  24. data/db/migrate/20231211200639_create_solid_queue_tables.rb +100 -0
  25. data/lib/active_job/concurrency_controls.rb +53 -0
  26. data/lib/active_job/queue_adapters/solid_queue_adapter.rb +24 -0
  27. data/lib/generators/solid_queue/install/USAGE +9 -0
  28. data/lib/generators/solid_queue/install/install_generator.rb +19 -0
  29. data/lib/puma/plugin/solid_queue.rb +63 -0
  30. data/lib/solid_queue/app_executor.rb +21 -0
  31. data/lib/solid_queue/configuration.rb +102 -0
  32. data/lib/solid_queue/dispatcher.rb +73 -0
  33. data/lib/solid_queue/engine.rb +39 -0
  34. data/lib/solid_queue/pool.rb +58 -0
  35. data/lib/solid_queue/processes/base.rb +27 -0
  36. data/lib/solid_queue/processes/interruptible.rb +37 -0
  37. data/lib/solid_queue/processes/pidfile.rb +58 -0
  38. data/lib/solid_queue/processes/poller.rb +24 -0
  39. data/lib/solid_queue/processes/procline.rb +11 -0
  40. data/lib/solid_queue/processes/registrable.rb +69 -0
  41. data/lib/solid_queue/processes/runnable.rb +77 -0
  42. data/lib/solid_queue/processes/signals.rb +69 -0
  43. data/lib/solid_queue/processes/supervised.rb +38 -0
  44. data/lib/solid_queue/supervisor.rb +182 -0
  45. data/lib/solid_queue/tasks.rb +16 -0
  46. data/lib/solid_queue/version.rb +3 -0
  47. data/lib/solid_queue/worker.rb +54 -0
  48. data/lib/solid_queue.rb +52 -0
  49. 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,3 @@
1
+ module SolidQueue
2
+ VERSION = "0.1.1"
3
+ 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
@@ -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: []