solid_queue 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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: []