process_bot 0.1.21 → 0.1.22

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: f9caad47714b9ab413bc33e85beb54b250455b6831ace86d058e6e63e84939bb
4
- data.tar.gz: f5d294e5d78cf4bafa60d4ddca38cea9887310ed3cc517d0fbe91ff95673c5b4
3
+ metadata.gz: 393279b999e338c87046703fca4e6e1c22adc46ffe085a95bef758df831d3a47
4
+ data.tar.gz: 852bd1ad1529685294bcc54be632c444f8748490519d41fd32bd963217098ec8
5
5
  SHA512:
6
- metadata.gz: ec5cdb7e07944681062b07f44408c0eb17962f4667a8215eead66a5b62ba6d0c78ffb521c431c94199fcff597bee5e5755df540687e79396cbb53df1daca5034
7
- data.tar.gz: 3e21dba2ec87ef787ce3c4bf3b1d6f81273896862a04807ee48f81cc86b8e769712c1de043106c9e64cb16b6ac47f52f0a99d025614c0861ba60f4728fa1fb97
6
+ metadata.gz: 8e166f7a08badb085df25e770028432f4a40b5e024f7c4b363426388145e2fbce4c5b91a67e348d504c9c040b24c674ce03e1ba2c5d097d3fffa54a949549e3f
7
+ data.tar.gz: 1fed86f6ca3b6c6dcc579f9cbe44f2d9796b6cdc18b08227a9d03f4a3814b6b806dfa0e154a5f63e17c533496a0720ffb32b336c3e98b3f9cd84958027e97563
data/CHANGELOG.md CHANGED
@@ -5,6 +5,7 @@
5
5
  - Bump version to 0.1.20.
6
6
  - Flush log output immediately so Capistrano can stream it.
7
7
  - Bump version to 0.1.21.
8
+ - Add optional Sidekiq restart overlap and a new ProcessBot restart command.
8
9
 
9
10
  ## [0.1.0] - 2022-04-03
10
11
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- process_bot (0.1.21)
4
+ process_bot (0.1.22)
5
5
  knjrbfw (>= 0.0.116)
6
6
  pry
7
7
  rake
data/README.md CHANGED
@@ -75,6 +75,22 @@ bundle exec process_bot --command start --log true --log-file-path /var/log/proc
75
75
  Use `process_bot:sidekiq:graceful` to wait for running jobs, and
76
76
  `process_bot:sidekiq:graceful_no_wait` to return immediately while Sidekiq drains.
77
77
 
78
+ ### Overlapping restarts
79
+
80
+ You can restart Sidekiq while the old process drains by enabling overlap on the ProcessBot instance:
81
+
82
+ ```ruby
83
+ set :sidekiq_restart_overlap, true
84
+ ```
85
+
86
+ When enabled, `process_bot:sidekiq:restart` will use the overlap behavior.
87
+
88
+ Or when running ProcessBot directly:
89
+
90
+ ```bash
91
+ bundle exec process_bot --command restart --sidekiq-restart-overlap true
92
+ ```
93
+
78
94
  ### Capistrano logging
79
95
 
80
96
  ProcessBot logging is enabled by default in the Capistrano integration.
@@ -13,6 +13,7 @@ namespace :load do
13
13
  set :sidekiq_processes, 1
14
14
  set :sidekiq_options_per_process, nil
15
15
  set :sidekiq_user, nil
16
+ set :sidekiq_restart_overlap, nil
16
17
  set :process_bot_log, true
17
18
  # Rbenv, Chruby, and RVM integration
18
19
  set :rbenv_map_bins, fetch(:rbenv_map_bins).to_a + ["sidekiq", "sidekiqctl"]
@@ -118,8 +119,27 @@ namespace :process_bot do
118
119
 
119
120
  desc "Restart Sidekiq and ProcessBot"
120
121
  task :restart do
121
- invoke! "process_bot:sidekiq:stop"
122
- invoke! "process_bot:sidekiq:start"
122
+ if fetch(:sidekiq_restart_overlap, nil)
123
+ on roles fetch(:sidekiq_roles) do |role|
124
+ git_plugin.switch_user(role) do
125
+ running_processes = git_plugin.running_process_bot_processes
126
+
127
+ if running_processes.any?
128
+ running_processes.each do |process_bot_process|
129
+ git_plugin.process_bot_command(process_bot_process, :restart)
130
+ end
131
+ else
132
+ fetch(:sidekiq_processes).times do |idx|
133
+ puts "Starting Sidekiq with ProcessBot #{idx}"
134
+ git_plugin.start_sidekiq(idx)
135
+ end
136
+ end
137
+ end
138
+ end
139
+ else
140
+ invoke! "process_bot:sidekiq:stop"
141
+ invoke! "process_bot:sidekiq:start"
142
+ end
123
143
  end
124
144
  end
125
145
  end
@@ -167,6 +167,7 @@ module ProcessBot::Capistrano::SidekiqHelpers # rubocop:disable Metrics/ModuleLe
167
167
  args += ["--sidekiq-queues", Array(fetch(:sidekiq_queue)).join(",")] if fetch(:sidekiq_queue)
168
168
  args += ["--sidekiq-config", fetch(:sidekiq_config)] if fetch(:sidekiq_config)
169
169
  args += ["--sidekiq-concurrency", fetch(:sidekiq_concurrency)] if fetch(:sidekiq_concurrency)
170
+ args += ["--sidekiq-restart-overlap", fetch(:sidekiq_restart_overlap)] unless fetch(:sidekiq_restart_overlap, nil).nil?
170
171
  if (process_options = fetch(:sidekiq_options_per_process))
171
172
  args += process_options[idx]
172
173
  end
@@ -84,7 +84,7 @@ class ProcessBot::ControlSocket
84
84
  command = JSON.parse(data)
85
85
  command_type = command.fetch("command")
86
86
 
87
- if command_type == "graceful" || command_type == "graceful_no_wait" || command_type == "stop"
87
+ if command_type == "graceful" || command_type == "graceful_no_wait" || command_type == "restart" || command_type == "stop"
88
88
  begin
89
89
  unless process.accept_control_commands?
90
90
  client.puts(JSON.generate(type: "error", message: "ProcessBot is shutting down", backtrace: Thread.current.backtrace))
@@ -141,8 +141,8 @@ class ProcessBot::Process::Handlers::Sidekiq
141
141
  command
142
142
  end
143
143
 
144
- def graceful(**_args)
145
- process.set_stopped
144
+ def graceful(stop_process_bot: true, **_args)
145
+ process.set_stopped if stop_process_bot
146
146
 
147
147
  return unless ensure_current_pid?
148
148
 
@@ -152,8 +152,8 @@ class ProcessBot::Process::Handlers::Sidekiq
152
152
  wait_for_no_jobs_and_stop_sidekiq
153
153
  end
154
154
 
155
- def graceful_no_wait(**_args)
156
- process.set_stopped
155
+ def graceful_no_wait(stop_process_bot: true, **_args)
156
+ process.set_stopped if stop_process_bot
157
157
 
158
158
  return unless ensure_current_pid?
159
159
 
@@ -0,0 +1,28 @@
1
+ class ProcessBot::Process::RunnerInstance
2
+ attr_reader :runner, :thread
3
+
4
+ def initialize(runner:, event_queue:, logger:)
5
+ @runner = runner
6
+ @event_queue = event_queue
7
+ @logger = logger
8
+ end
9
+
10
+ def start
11
+ @thread = Thread.new do
12
+ runner.run
13
+ event_queue << {type: :stopped, runner_instance: self}
14
+ rescue => e # rubocop:disable Style/RescueStandardError
15
+ logger.error e.message
16
+ logger.error e.backtrace
17
+ event_queue << {type: :error, runner_instance: self, error: e}
18
+ end
19
+ end
20
+
21
+ def running?
22
+ runner.running?
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :event_queue, :logger
28
+ end
@@ -12,6 +12,7 @@ class ProcessBot::Process
12
12
 
13
13
  autoload :Handlers, "#{__dir__}/process/handlers"
14
14
  autoload :Runner, "#{__dir__}/process/runner"
15
+ autoload :RunnerInstance, "#{__dir__}/process/runner_instance"
15
16
 
16
17
  attr_reader :control_command_monitor, :current_pid, :current_process_title, :options, :port, :stopped
17
18
 
@@ -21,6 +22,9 @@ class ProcessBot::Process
21
22
  @accept_control_commands = true
22
23
  @control_command_monitor = Monitor.new
23
24
  @control_commands_in_flight = 0
25
+ @runner_events = Queue.new
26
+ @runner_instances = []
27
+ @runner_monitor = Monitor.new
24
28
 
25
29
  options.events.connect(:on_process_started, &method(:on_process_started)) # rubocop:disable Performance/MethodObjectAsBlock
26
30
  options.events.connect(:on_socket_opened, &method(:on_socket_opened)) # rubocop:disable Performance/MethodObjectAsBlock
@@ -34,7 +38,7 @@ class ProcessBot::Process
34
38
  if command == "start"
35
39
  logger.logs "Starting process"
36
40
  start
37
- elsif command == "graceful" || command == "graceful_no_wait" || command == "stop"
41
+ elsif command == "graceful" || command == "graceful_no_wait" || command == "restart" || command == "stop"
38
42
  send_control_command(command)
39
43
  else
40
44
  raise "Unknown command: #{command}"
@@ -85,18 +89,32 @@ class ProcessBot::Process
85
89
 
86
90
  def start
87
91
  start_control_socket
92
+ start_runner_instance
88
93
 
89
94
  loop do
90
- run
95
+ runner_event = runner_events.pop
96
+ handle_runner_event(runner_event)
91
97
 
92
- if stopped
93
- stop_accepting_control_commands
94
- wait_for_control_commands
95
- break
98
+ next unless stopped && runner_instances.empty?
99
+
100
+ stop_accepting_control_commands
101
+ wait_for_control_commands
102
+ break
103
+ end
104
+ end
105
+
106
+ def restart(**args)
107
+ logger.logs "Restart process"
108
+
109
+ if handler_name == "sidekiq"
110
+ if restart_overlap?(args)
111
+ handler_instance.graceful_no_wait(stop_process_bot: false)
112
+ start_runner_instance
96
113
  else
97
- logger.logs "Process stopped - starting again after 1 sec"
98
- sleep 1
114
+ handler_instance.graceful(stop_process_bot: false)
99
115
  end
116
+ else
117
+ handler_instance.stop
100
118
  end
101
119
  end
102
120
 
@@ -107,7 +125,7 @@ class ProcessBot::Process
107
125
  end
108
126
 
109
127
  def run
110
- runner.run
128
+ start_runner_instance
111
129
  end
112
130
 
113
131
  def send_control_command(command, **command_options)
@@ -119,13 +137,7 @@ class ProcessBot::Process
119
137
  end
120
138
 
121
139
  def runner
122
- @runner ||= ProcessBot::Process::Runner.new(
123
- command: handler_instance.start_command,
124
- handler_name: handler_name,
125
- handler_instance: handler_instance,
126
- logger: logger,
127
- options: options
128
- )
140
+ current_runner_instance&.runner || @runner ||= build_runner
129
141
  end
130
142
 
131
143
  def update_process_title
@@ -164,4 +176,94 @@ class ProcessBot::Process
164
176
  @control_commands_in_flight
165
177
  end
166
178
  end
179
+
180
+ def build_runner
181
+ ProcessBot::Process::Runner.new(
182
+ command: handler_instance.start_command,
183
+ handler_name: handler_name,
184
+ handler_instance: handler_instance,
185
+ logger: logger,
186
+ options: options
187
+ )
188
+ end
189
+
190
+ def start_runner_instance
191
+ runner_instance = ProcessBot::Process::RunnerInstance.new(
192
+ runner: build_runner,
193
+ event_queue: runner_events,
194
+ logger: logger
195
+ )
196
+
197
+ track_runner_instance(runner_instance)
198
+ @current_runner_instance = runner_instance
199
+ @runner = runner_instance.runner
200
+ runner_instance.start
201
+ end
202
+
203
+ def handle_runner_event(runner_event)
204
+ runner_instance = runner_event.fetch(:runner_instance)
205
+ remove_runner_instance(runner_instance)
206
+ log_runner_event_error(runner_event)
207
+ clear_current_runner(runner_instance)
208
+ restart_runner_if_needed(runner_instance)
209
+ end
210
+
211
+ def runner_instances
212
+ runner_monitor.synchronize do
213
+ @runner_instances.dup
214
+ end
215
+ end
216
+
217
+ def track_runner_instance(runner_instance)
218
+ runner_monitor.synchronize do
219
+ @runner_instances << runner_instance
220
+ end
221
+ end
222
+
223
+ def remove_runner_instance(runner_instance)
224
+ runner_monitor.synchronize do
225
+ @runner_instances.delete(runner_instance)
226
+ end
227
+ end
228
+
229
+ def restart_overlap?(command_options = {})
230
+ value = if command_options.key?(:sidekiq_restart_overlap)
231
+ command_options[:sidekiq_restart_overlap]
232
+ else
233
+ options[:sidekiq_restart_overlap]
234
+ end
235
+ return false if value.nil?
236
+ return value if value == true || value == false
237
+
238
+ normalized = value.to_s.strip.downcase
239
+ return false if normalized == "false" || normalized == "0" || normalized == ""
240
+
241
+ true
242
+ end
243
+
244
+ def log_runner_event_error(runner_event)
245
+ return unless runner_event.fetch(:type) == :error
246
+
247
+ logger.error "Process runner crashed: #{runner_event.fetch(:error)}"
248
+ end
249
+
250
+ def clear_current_runner(runner_instance)
251
+ return unless runner_instance == current_runner_instance
252
+
253
+ @current_runner_instance = nil
254
+ @runner = nil
255
+ end
256
+
257
+ def restart_runner_if_needed(runner_instance)
258
+ return if stopped
259
+ return unless runner_instance == current_runner_instance || current_runner_instance.nil?
260
+
261
+ logger.logs "Process stopped - starting again after 1 sec"
262
+ sleep 1
263
+ start_runner_instance
264
+ end
265
+
266
+ private
267
+
268
+ attr_reader :current_runner_instance, :runner_events, :runner_monitor
167
269
  end
@@ -1,3 +1,3 @@
1
1
  module ProcessBot
2
- VERSION = "0.1.21".freeze
2
+ VERSION = "0.1.22".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.21
4
+ version: 0.1.22
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-13 00:00:00.000000000 Z
11
+ date: 2026-01-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: knjrbfw
@@ -173,6 +173,7 @@ files:
173
173
  - lib/process_bot/process/handlers/custom.rb
174
174
  - lib/process_bot/process/handlers/sidekiq.rb
175
175
  - lib/process_bot/process/runner.rb
176
+ - lib/process_bot/process/runner_instance.rb
176
177
  - lib/process_bot/version.rb
177
178
  - peak_flow.yml
178
179
  - process_bot.gemspec