process_bot 0.1.14 → 0.1.16

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7faeef8de7bcedd0fb66cc0fcf161de87d94ee3d4e9cceda30086afc12fc09fb
4
- data.tar.gz: 79370a6f54b1f58ee5df361bdf99637e206310d126bbd9da2a8bbeae509561bb
3
+ metadata.gz: 7d79a43f8a6bdf408f637e66fff579115ab45f57f0c8c0314d98c1e84a8f4430
4
+ data.tar.gz: 716c9b8e69f54f1dc60a322d1ddad7f4336b464e338964ffa9f736d7cac11477
5
5
  SHA512:
6
- metadata.gz: 01ab0ae7b02f813fd00313750924b3162167506cc546b49c6d03dc38fd702b3a5d1082aa45cdc7267800575acdb517381b5243937214319b630ee99d765ee6a2
7
- data.tar.gz: 0df83d2af7a0180834e6bfb50d3fcd6e9834abb3a60d23a78cd5d80bc5a6c8441ad0f242da3cccb4b8eb4e49fa9ce12d041b87bd69be7d1e729e151f349d1937
6
+ metadata.gz: 2739cfa0148b5085b45f0ca273d583f609413346e7d7ebed3693fb3715ebeb2ad4c14475b212c82a86e3bc1081585f2edc606c0d2f7ae2159a61722cfc1bc0c1
7
+ data.tar.gz: f59153145d7e36da05d9d9dc98d6e7d2f0a3b8f03fcfb51606b800453685f2fcb45e07eca46395cbdeac095f041749eb2064b9f98bc6dfe1c5e7d3fad5611e35
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- process_bot (0.1.14)
4
+ process_bot (0.1.16)
5
5
  knjrbfw (>= 0.0.116)
6
6
  pry
7
7
  rake
data/README.md CHANGED
@@ -44,6 +44,7 @@ ProcessBot provides these Sidekiq tasks:
44
44
  - `process_bot:sidekiq:stop`
45
45
  - `process_bot:sidekiq:graceful` (stops fetching new jobs and waits for running jobs by default)
46
46
  - `process_bot:sidekiq:graceful_no_wait` (stops fetching new jobs and returns immediately)
47
+ - `process_bot:sidekiq:ensure_running` (starts missing processes, ignoring ones in graceful shutdown)
47
48
  - `process_bot:sidekiq:restart`
48
49
 
49
50
  You can also skip waiting for graceful completion:
@@ -88,7 +89,7 @@ set :process_bot_log, false
88
89
  When running ProcessBot directly, you can control graceful waiting and log file output:
89
90
 
90
91
  ```bash
91
- bundle exec process_bot --command graceful --wait-for-gracefully-stopped false
92
+ bundle exec process_bot --command graceful_no_wait
92
93
  bundle exec process_bot --command start --log-file-path /var/log/process_bot.log
93
94
  ```
94
95
 
@@ -70,6 +70,55 @@ namespace :process_bot do
70
70
  end
71
71
  end
72
72
 
73
+ desc "Ensure the configured number of Sidekiq ProcessBots are running (excluding graceful shutdowns)"
74
+ task :ensure_running do
75
+ on roles fetch(:sidekiq_roles) do |role|
76
+ git_plugin.switch_user(role) do
77
+ desired_processes = fetch(:sidekiq_processes).to_i
78
+ running_processes = git_plugin.running_process_bot_processes
79
+
80
+ graceful_processes = running_processes.select do |process_bot_data|
81
+ git_plugin.sidekiq_process_graceful?(process_bot_data)
82
+ end
83
+
84
+ active_processes = running_processes - graceful_processes
85
+
86
+ active_indexes = active_processes.filter_map do |process_bot_data|
87
+ git_plugin.process_bot_sidekiq_index(process_bot_data)
88
+ end
89
+
90
+ graceful_indexes = graceful_processes.filter_map do |process_bot_data|
91
+ git_plugin.process_bot_sidekiq_index(process_bot_data)
92
+ end
93
+
94
+ puts "ProcessBot Sidekiq in graceful shutdown: #{graceful_indexes.join(", ")}" if graceful_indexes.any?
95
+
96
+ desired_indexes = (0...desired_processes).to_a
97
+ missing_indexes = desired_indexes - active_indexes - graceful_indexes
98
+ missing_count = desired_processes - active_indexes.count
99
+
100
+ if missing_count.negative?
101
+ puts "Found #{active_indexes.count} running ProcessBot Sidekiq processes; desired is #{desired_processes}"
102
+ missing_count = 0
103
+ end
104
+
105
+ if missing_indexes.any?
106
+ missing_indexes.each do |idx|
107
+ puts "Starting Sidekiq with ProcessBot #{idx} (missing)"
108
+ git_plugin.start_sidekiq(idx)
109
+ end
110
+ else
111
+ puts "All ProcessBot Sidekiq processes are running (#{active_indexes.count}/#{desired_processes})"
112
+ end
113
+
114
+ return unless missing_count > missing_indexes.length
115
+
116
+ puts "Skipped starting #{missing_count - missing_indexes.length} processes because " \
117
+ "they are still in graceful shutdown"
118
+ end
119
+ end
120
+ end
121
+
73
122
  desc "Restart Sidekiq and ProcessBot"
74
123
  task :restart do
75
124
  invoke! "process_bot:sidekiq:stop"
@@ -36,14 +36,11 @@ module ProcessBot::Capistrano::SidekiqHelpers # rubocop:disable Metrics/ModuleLe
36
36
  raise "No port in process bot data? #{process_bot_data}" unless process_bot_data["port"]
37
37
 
38
38
  mode = "exec"
39
- wait_for_gracefully_stopped = process_bot_wait_setting(command)
40
39
 
41
40
  if mode == "runner"
42
41
  args = {command: command, port: process_bot_data.fetch("port")}
43
42
  args["log"] = fetch(:process_bot_log) unless fetch(:process_bot_log).nil?
44
43
 
45
- args["wait_for_gracefully_stopped"] = wait_for_gracefully_stopped unless wait_for_gracefully_stopped.nil?
46
-
47
44
  escaped_args = JSON.generate(args).gsub("\"", "\\\"")
48
45
  rails_runner_command = "require 'process_bot'; ProcessBot::Process.new(ProcessBot::Options.from_args(#{escaped_args})).execute!"
49
46
 
@@ -61,7 +58,6 @@ module ProcessBot::Capistrano::SidekiqHelpers # rubocop:disable Metrics/ModuleLe
61
58
  backend_command << "#{key} #{value}"
62
59
  end
63
60
 
64
- backend_command << " --wait-for-gracefully-stopped #{wait_for_gracefully_stopped}" unless wait_for_gracefully_stopped.nil?
65
61
  else
66
62
  raise "Unknown mode: #{mode}"
67
63
  end
@@ -69,13 +65,6 @@ module ProcessBot::Capistrano::SidekiqHelpers # rubocop:disable Metrics/ModuleLe
69
65
  backend.execute backend_command
70
66
  end
71
67
 
72
- def process_bot_wait_setting(command)
73
- return true if command == :graceful
74
- return false if command == :graceful_no_wait
75
-
76
- nil
77
- end
78
-
79
68
  def running_process_bot_processes
80
69
  sidekiq_app_name = fetch(:sidekiq_app_name, fetch(:application))
81
70
  raise "No :sidekiq_app_name was set" unless sidekiq_app_name
@@ -104,6 +93,31 @@ module ProcessBot::Capistrano::SidekiqHelpers # rubocop:disable Metrics/ModuleLe
104
93
  processes
105
94
  end
106
95
 
96
+ def process_bot_sidekiq_index(process_bot_data)
97
+ process_bot_id = process_bot_data["id"].to_s
98
+ match = process_bot_id.match(/-(\d+)\z/)
99
+ return nil unless match
100
+
101
+ match[1].to_i
102
+ end
103
+
104
+ def sidekiq_command_graceful?(command)
105
+ return false unless command
106
+
107
+ normalized_command = command.to_s.downcase
108
+ normalized_command.include?("stopping") || normalized_command.include?("quiet")
109
+ end
110
+
111
+ def sidekiq_process_graceful?(process_bot_data)
112
+ sidekiq_pid = process_bot_data["pid"]
113
+ return false unless sidekiq_pid
114
+
115
+ command = backend.capture(:ps, "-o", "command=", "-p", sidekiq_pid.to_s).strip
116
+ sidekiq_command_graceful?(command)
117
+ rescue SSHKit::Command::Failed
118
+ false
119
+ end
120
+
107
121
  def sidekiq_user(role = nil)
108
122
  if role.nil?
109
123
  fetch(:sidekiq_user)
@@ -1,4 +1,6 @@
1
1
  require "socket"
2
+ require "json"
3
+ require "knjrbfw"
2
4
 
3
5
  class ProcessBot::ControlSocket
4
6
  attr_reader :options, :port, :process, :server
@@ -21,15 +23,25 @@ class ProcessBot::ControlSocket
21
23
  end
22
24
 
23
25
  def start_tcp_server
24
- tries ||= 0
25
- tries += 1
26
- @server = actually_start_tcp_server("localhost", @port)
27
- rescue Errno::EADDRINUSE, Errno::EADDRNOTAVAIL => e
28
- if tries <= 100
29
- @port += 1
30
- retry
31
- else
32
- raise e
26
+ used_ports = used_process_bot_ports
27
+ attempts = 0
28
+
29
+ loop do
30
+ if used_ports.include?(@port)
31
+ @port += 1
32
+ next
33
+ end
34
+
35
+ attempts += 1
36
+ @server = actually_start_tcp_server("localhost", @port)
37
+ break
38
+ rescue Errno::EADDRINUSE, Errno::EADDRNOTAVAIL => e
39
+ if attempts <= 100
40
+ @port += 1
41
+ next
42
+ else
43
+ raise e
44
+ end
33
45
  end
34
46
  end
35
47
 
@@ -100,18 +112,30 @@ class ProcessBot::ControlSocket
100
112
  end
101
113
 
102
114
  def run_command(command_type, command_options, client)
103
- command_type, command_options = normalize_command(command_type, command_options)
104
115
  logger.logs "Command #{command_type} with options #{command_options}"
105
116
 
106
117
  process.__send__(command_type, **command_options)
107
118
  client.puts(JSON.generate(type: "success"))
108
119
  end
109
120
 
110
- def normalize_command(command_type, command_options)
111
- return [command_type, command_options] unless command_type == "graceful_no_wait"
121
+ def used_process_bot_ports
122
+ ports = []
123
+
124
+ Knj::Unix_proc.list("grep" => "ProcessBot") do |process|
125
+ process_command = process.data.fetch("cmd")
126
+ match = process_command.match(/ProcessBot (\{.+\})/)
127
+ next unless match
128
+
129
+ begin
130
+ process_data = JSON.parse(match[1])
131
+ rescue JSON::ParserError
132
+ next
133
+ end
134
+
135
+ port = process_data["port"]
136
+ ports << port.to_i if port
137
+ end
112
138
 
113
- command_type = "graceful"
114
- command_options[:wait_for_gracefully_stopped] = false
115
- [command_type, command_options]
139
+ ports.uniq
116
140
  end
117
141
  end
@@ -21,10 +21,6 @@ class ProcessBot::Process::Handlers::Sidekiq
21
21
  Process.detach(pid) if pid
22
22
  end
23
23
 
24
- def false_value?(value)
25
- !value || value == "false"
26
- end
27
-
28
24
  def process_running?(pid)
29
25
  return false unless pid
30
26
 
@@ -121,20 +117,6 @@ class ProcessBot::Process::Handlers::Sidekiq
121
117
  end
122
118
  end
123
119
 
124
- def handle_graceful_wait(wait_for_gracefully_stopped)
125
- if false_value?(wait_for_gracefully_stopped)
126
- logger.logs "Dont wait for gracefully stopped - doing that in fork..."
127
-
128
- daemonize do
129
- wait_for_no_jobs_and_stop_sidekiq
130
- exit
131
- end
132
- else
133
- logger.logs "Wait for gracefully stopped..."
134
- wait_for_no_jobs_and_stop_sidekiq
135
- end
136
- end
137
-
138
120
  def start_command # rubocop:disable Metrics/AbcSize
139
121
  args = []
140
122
 
@@ -159,15 +141,29 @@ class ProcessBot::Process::Handlers::Sidekiq
159
141
  command
160
142
  end
161
143
 
162
- def graceful(**args)
163
- wait_for_gracefully_stopped = args.fetch(:wait_for_gracefully_stopped, true)
144
+ def graceful(**_args)
145
+ process.set_stopped
146
+
147
+ return unless ensure_current_pid?
148
+
149
+ return unless send_tstp_or_return
150
+
151
+ logger.logs "Wait for gracefully stopped..."
152
+ wait_for_no_jobs_and_stop_sidekiq
153
+ end
154
+
155
+ def graceful_no_wait(**_args)
164
156
  process.set_stopped
165
157
 
166
158
  return unless ensure_current_pid?
167
159
 
168
160
  return unless send_tstp_or_return
169
161
 
170
- handle_graceful_wait(wait_for_gracefully_stopped)
162
+ logger.logs "Dont wait for gracefully stopped - doing that in fork..."
163
+ daemonize do
164
+ wait_for_no_jobs_and_stop_sidekiq
165
+ exit
166
+ end
171
167
  end
172
168
 
173
169
  def stop(**_args)
@@ -219,5 +215,15 @@ class ProcessBot::Process::Handlers::Sidekiq
219
215
  logger.logs "Wait for no jobs and Stop sidekiq"
220
216
  wait_for_no_jobs
221
217
  stop
218
+ wait_for_sidekiq_exit
219
+ end
220
+
221
+ def wait_for_sidekiq_exit
222
+ return unless current_pid
223
+
224
+ while process_running?(current_pid)
225
+ logger.logs "Waiting for Sidekiq PID #{current_pid} to stop"
226
+ sleep 1
227
+ end
222
228
  end
223
229
  end
@@ -6,6 +6,7 @@ class ProcessBot::Process
6
6
  extend Forwardable
7
7
 
8
8
  def_delegator :handler_instance, :graceful
9
+ def_delegator :handler_instance, :graceful_no_wait
9
10
  def_delegator :handler_instance, :stop
10
11
 
11
12
  autoload :Handlers, "#{__dir__}/process/handlers"
@@ -29,10 +30,8 @@ class ProcessBot::Process
29
30
  if command == "start"
30
31
  logger.logs "Starting process"
31
32
  start
32
- elsif command == "graceful" || command == "stop"
33
+ elsif command == "graceful" || command == "graceful_no_wait" || command == "stop"
33
34
  send_control_command(command)
34
- elsif command == "graceful_no_wait"
35
- send_control_command(command, wait_for_gracefully_stopped: false)
36
35
  else
37
36
  raise "Unknown command: #{command}"
38
37
  end
@@ -107,7 +106,8 @@ class ProcessBot::Process
107
106
 
108
107
  def send_control_command(command, **command_options)
109
108
  logger.logs "Sending #{command} command"
110
- client.send_command(command: command, options: options.options.merge(command_options))
109
+ response = client.send_command(command: command, options: options.options.merge(command_options))
110
+ raise "No response from ProcessBot while sending #{command}" if response == :nil
111
111
  rescue Errno::ECONNREFUSED => e
112
112
  raise e unless options[:ignore_no_process_bot]
113
113
  end
@@ -1,3 +1,3 @@
1
1
  module ProcessBot
2
- VERSION = "0.1.14".freeze
2
+ VERSION = "0.1.16".freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: process_bot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.14
4
+ version: 0.1.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - kaspernj
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-01-05 00:00:00.000000000 Z
11
+ date: 2026-01-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: knjrbfw