process_bot 0.1.2 → 0.1.3

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: 918adb2b2275f38d3b22eb926a5da3b021f667d4a7290a3b0b1d9ea7bb56a2c2
4
- data.tar.gz: 03c557694d0a91f78f25dc4689f2b99c46b626ebeed5816f8e2e0346e29dd482
3
+ metadata.gz: b719a497be535319bf027f1669321e58466f845657bdebb82f94d78f6d6e984f
4
+ data.tar.gz: cbbbc984e75edacbcdf6e9f8579633e6ebac87cf4e0690b944a7e32b4424c4f1
5
5
  SHA512:
6
- metadata.gz: 2053d076158df45b723855cee991d47852efb18b34ace65e24005798c7d9d1f1ffff126d1fc7409ba3554e4bfa3bf9855c13dfec5bcfa14ac79aac57d9409acf
7
- data.tar.gz: 5622bc3069e029485876c7329bd0658b3bf7b3c4b265a40e51421bab04e0b898367f91c864dbe7caf1de93b317e9ba1fcd4787971fd4dc9e28528bf4f14a8df8
6
+ metadata.gz: fdbd9fd4a6196cf6d9183289ec7c9ee984d25e5e2af0af5c8213a5fe649865687aa70f0dcf5a49f91100c7ceae72298e9bb9bdcb9dac9861c86b50209528d1a6
7
+ data.tar.gz: 97942320a0a9354770aa8f4e155a620a1c232c5fd712d2269f525d76b1a00adc2480c6c8e51c79c8eb2047a391224c6597e4c9bc74bf4b3369aa0664b7418e1d
data/.rubocop.yml CHANGED
@@ -2,7 +2,7 @@ AllCops:
2
2
  DisplayCopNames: true
3
3
  DisplayStyleGuide: true
4
4
  NewCops: enable
5
- TargetRubyVersion: 2.6
5
+ TargetRubyVersion: 2.7
6
6
 
7
7
  require:
8
8
  - rubocop-performance
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.7.8
data/Gemfile CHANGED
@@ -5,9 +5,14 @@ source "https://rubygems.org"
5
5
  # Specify your gem's dependencies in process_bot.gemspec
6
6
  gemspec
7
7
 
8
+ gem "pry"
8
9
  gem "rake"
9
10
  gem "rspec"
10
- gem "rubocop"
11
11
  gem "string-cases"
12
12
 
13
- gem "pry"
13
+ group :development do
14
+ gem "rubocop"
15
+ gem "rubocop-performance"
16
+ gem "rubocop-rake"
17
+ gem "rubocop-rspec"
18
+ end
data/Gemfile.lock CHANGED
@@ -1,63 +1,89 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- process_bot (0.1.2)
4
+ process_bot (0.1.3)
5
+ knjrbfw (>= 0.0.116)
5
6
 
6
7
  GEM
7
8
  remote: https://rubygems.org/
8
9
  specs:
9
10
  ast (2.4.2)
10
11
  coderay (1.1.3)
12
+ datet (0.0.25)
11
13
  diff-lcs (1.5.0)
12
- json (2.6.2)
14
+ http2 (0.0.36)
15
+ string-cases (~> 0)
16
+ json (2.6.3)
17
+ knjrbfw (0.0.116)
18
+ datet
19
+ http2
20
+ php4r
21
+ ruby_process
22
+ tsafe
23
+ wref (>= 0.0.8)
13
24
  method_source (1.0.0)
14
- parallel (1.22.1)
15
- parser (3.1.2.1)
25
+ parallel (1.23.0)
26
+ parser (3.2.2.0)
16
27
  ast (~> 2.4.1)
17
- pry (0.14.1)
28
+ php4r (0.0.4)
29
+ datet
30
+ http2
31
+ string-strtr
32
+ pry (0.14.2)
18
33
  coderay (~> 1.1)
19
34
  method_source (~> 1.0)
20
35
  rainbow (3.1.1)
21
36
  rake (13.0.6)
22
- regexp_parser (2.6.0)
37
+ regexp_parser (2.8.0)
23
38
  rexml (3.2.5)
24
- rspec (3.11.0)
25
- rspec-core (~> 3.11.0)
26
- rspec-expectations (~> 3.11.0)
27
- rspec-mocks (~> 3.11.0)
28
- rspec-core (3.11.0)
29
- rspec-support (~> 3.11.0)
30
- rspec-expectations (3.11.0)
39
+ rspec (3.12.0)
40
+ rspec-core (~> 3.12.0)
41
+ rspec-expectations (~> 3.12.0)
42
+ rspec-mocks (~> 3.12.0)
43
+ rspec-core (3.12.0)
44
+ rspec-support (~> 3.12.0)
45
+ rspec-expectations (3.12.0)
31
46
  diff-lcs (>= 1.2.0, < 2.0)
32
- rspec-support (~> 3.11.0)
33
- rspec-mocks (3.11.1)
47
+ rspec-support (~> 3.12.0)
48
+ rspec-mocks (3.12.0)
34
49
  diff-lcs (>= 1.2.0, < 2.0)
35
- rspec-support (~> 3.11.0)
36
- rspec-support (3.11.0)
37
- rubocop (1.36.0)
50
+ rspec-support (~> 3.12.0)
51
+ rspec-support (3.12.0)
52
+ rubocop (1.50.2)
38
53
  json (~> 2.3)
39
54
  parallel (~> 1.10)
40
- parser (>= 3.1.2.1)
55
+ parser (>= 3.2.0.0)
41
56
  rainbow (>= 2.2.2, < 4.0)
42
57
  regexp_parser (>= 1.8, < 3.0)
43
58
  rexml (>= 3.2.5, < 4.0)
44
- rubocop-ast (>= 1.20.1, < 2.0)
59
+ rubocop-ast (>= 1.28.0, < 2.0)
45
60
  ruby-progressbar (~> 1.7)
46
- unicode-display_width (>= 1.4.0, < 3.0)
47
- rubocop-ast (1.21.0)
48
- parser (>= 3.1.1.0)
49
- rubocop-performance (1.15.0)
61
+ unicode-display_width (>= 2.4.0, < 3.0)
62
+ rubocop-ast (1.28.0)
63
+ parser (>= 3.2.1.0)
64
+ rubocop-capybara (2.18.0)
65
+ rubocop (~> 1.41)
66
+ rubocop-performance (1.17.1)
50
67
  rubocop (>= 1.7.0, < 2.0)
51
68
  rubocop-ast (>= 0.4.0)
52
69
  rubocop-rake (0.6.0)
53
70
  rubocop (~> 1.0)
54
- rubocop-rspec (2.11.1)
55
- rubocop (~> 1.19)
56
- ruby-progressbar (1.11.0)
71
+ rubocop-rspec (2.20.0)
72
+ rubocop (~> 1.33)
73
+ rubocop-capybara (~> 2.17)
74
+ ruby-progressbar (1.13.0)
75
+ ruby_process (0.0.13)
76
+ string-cases
77
+ tsafe
78
+ wref
57
79
  string-cases (0.0.4)
58
- unicode-display_width (2.3.0)
80
+ string-strtr (0.0.3)
81
+ tsafe (0.0.12)
82
+ unicode-display_width (2.4.2)
83
+ wref (0.0.8)
59
84
 
60
85
  PLATFORMS
86
+ ruby
61
87
  x86_64-linux
62
88
 
63
89
  DEPENDENCIES
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # ProcessBot
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/process_bot`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ Run your app through ProcessBot for automatic restart if crashing, but still support normal deployment through Capistrano.
4
4
 
5
- TODO: Delete this and the text above, and describe your gem
5
+ In the future ProcessBot will also watch memory usage and restart processes if leaking memory automatically and gracefully.
6
6
 
7
7
  ## Installation
8
8
 
@@ -12,17 +12,27 @@ Add this line to your application's Gemfile:
12
12
  gem 'process_bot'
13
13
  ```
14
14
 
15
- And then execute:
16
-
17
- $ bundle install
18
-
19
- Or install it yourself as:
15
+ Add to your `Capfile`:
16
+ ```ruby
17
+ require "process_bot"
18
+ install_plugin ProcessBot::Capistrano::Sidekiq
19
+ install_plugin ProcessBot::Capistrano::Puma
20
+ ```
20
21
 
21
- $ gem install process_bot
22
+ Add to your `deploy.rb`:
23
+ ```ruby
24
+ after "deploy:starting", "process_bot:sidekiq:graceful"
25
+ after "deploy:published", "process_bot:sidekiq:start"
26
+ after "deploy:failed", "process_bot:sidekiq:start"
27
+ ```
22
28
 
23
29
  ## Usage
24
30
 
25
- TODO: Write usage instructions here
31
+ Run commands in the command line like this:
32
+
33
+ ```bash
34
+ cap production process_bot:sidekiq:graceful
35
+ ```
26
36
 
27
37
  ## Development
28
38
 
data/exe/process_bot CHANGED
@@ -29,8 +29,6 @@ while argv_i < ARGV.length
29
29
  argv_i += 1
30
30
  end
31
31
 
32
- pp options.options
33
-
34
32
  ProcessBot::Process
35
33
  .new(options)
36
34
  .execute!
@@ -14,62 +14,51 @@ namespace :load do
14
14
  set :sidekiq_options_per_process, nil
15
15
  set :sidekiq_user, nil
16
16
  # Rbenv, Chruby, and RVM integration
17
- set :rbenv_map_bins, fetch(:rbenv_map_bins).to_a.concat(%w[sidekiq sidekiqctl])
18
- set :rvm_map_bins, fetch(:rvm_map_bins).to_a.concat(%w[sidekiq sidekiqctl])
19
- set :chruby_map_bins, fetch(:chruby_map_bins).to_a.concat(%w[sidekiq sidekiqctl])
17
+ set :rbenv_map_bins, fetch(:rbenv_map_bins).to_a + ["sidekiq", "sidekiqctl"]
18
+ set :rvm_map_bins, fetch(:rvm_map_bins).to_a + ["sidekiq", "sidekiqctl"]
19
+ set :chruby_map_bins, fetch(:chruby_map_bins).to_a + ["sidekiq", "sidekiqctl"]
20
20
  # Bundler integration
21
- set :bundle_bins, fetch(:bundle_bins).to_a.concat(%w[sidekiq sidekiqctl])
21
+ set :bundle_bins, fetch(:bundle_bins).to_a + ["sidekiq", "sidekiqctl"]
22
22
  end
23
23
  end
24
24
 
25
25
  namespace :process_bot do
26
26
  namespace :sidekiq do
27
- desc "Quiet sidekiq (stop fetching new tasks from Redis)"
28
- task :quiet do
27
+ desc "Stop Sidekiq and ProcessBot gracefully (stop fetching new tasks from Redis and then quit when nothing is running)"
28
+ task :graceful do
29
29
  on roles fetch(:sidekiq_roles) do |role|
30
30
  git_plugin.switch_user(role) do
31
- git_plugin.running_sidekiq_processes.each do |sidekiq_process|
32
- git_plugin.stop_sidekiq(pid: sidekiq_process.fetch(:pid), signal: "TSTP")
31
+ git_plugin.running_process_bot_processes.each do |process_bot_process|
32
+ git_plugin.process_bot_command(process_bot_process, :graceful)
33
33
  end
34
34
  end
35
35
  end
36
36
  end
37
37
 
38
- desc "Stop Sidekiq (graceful shutdown within timeout, put unfinished tasks back to Redis)"
38
+ desc "Stop Sidekiq and ProcessBot (graceful shutdown within timeout, put unfinished tasks back to Redis)"
39
39
  task :stop do
40
40
  on roles fetch(:sidekiq_roles) do |role|
41
41
  git_plugin.switch_user(role) do
42
- git_plugin.running_sidekiq_processes.each do |sidekiq_process|
43
- git_plugin.stop_sidekiq(pid: sidekiq_process.fetch(:pid), signal: "TERM")
42
+ git_plugin.running_process_bot_processes.each do |process_bot_data|
43
+ git_plugin.process_bot_command(process_bot_data, :stop)
44
44
  end
45
45
  end
46
46
  end
47
47
  end
48
48
 
49
- desc "Stops Sidekiq after a set amount of time"
50
- task :stop_after_time do
51
- on roles fetch(:sidekiq_roles) do |role|
52
- git_plugin.switch_user(role) do
53
- git_plugin.running_sidekiq_processes.each do |sidekiq_process|
54
- git_plugin.stop_sidekiq_after_time(pid: sidekiq_process.fetch(:pid), signal: "TERM")
55
- end
56
- end
57
- end
58
- end
59
-
60
- desc "Start sidekiq"
49
+ desc "Start Sidekiq and ProcessBot"
61
50
  task :start do
62
51
  on roles fetch(:sidekiq_roles) do |role|
63
52
  git_plugin.switch_user(role) do
64
53
  fetch(:sidekiq_processes).times do |idx|
65
- puts "Starting Sidekiq #{idx}"
54
+ puts "Starting Sidekiq with ProcessBot #{idx}"
66
55
  git_plugin.start_sidekiq(idx)
67
56
  end
68
57
  end
69
58
  end
70
59
  end
71
60
 
72
- desc "Restart sidekiq"
61
+ desc "Restart Sidekiq and ProcessBot"
73
62
  task :restart do
74
63
  invoke! "process_bot:sidekiq:stop"
75
64
  invoke! "process_bot:sidekiq:start"
@@ -1,5 +1,3 @@
1
- require_relative "sidekiq_helpers"
2
-
3
1
  class ProcessBot::Capistrano::Sidekiq < Capistrano::Plugin
4
2
  include ProcessBot::Capistrano::SidekiqHelpers
5
3
 
@@ -1,4 +1,6 @@
1
- module ProcessBot::Capistrano::SidekiqHelpers
1
+ require "json"
2
+
3
+ module ProcessBot::Capistrano::SidekiqHelpers # rubocop:disable Metrics/ModuleLength
2
4
  def sidekiq_require
3
5
  "--require #{fetch(:sidekiq_require)}" if fetch(:sidekiq_require)
4
6
  end
@@ -30,41 +32,44 @@ module ProcessBot::Capistrano::SidekiqHelpers
30
32
  end
31
33
  end
32
34
 
33
- VALID_SIGNALS = ["TERM", "TSTP"].freeze
34
- def stop_sidekiq(pid:, signal:)
35
- raise "Invalid PID: #{pid}" unless pid.to_s.match?(/\A\d+\Z/)
36
- raise "Invalid signal: #{signal}" unless VALID_SIGNALS.include?(signal)
37
-
38
- backend.execute "kill -#{signal} #{pid}"
39
- end
35
+ def process_bot_command(process_bot_data, command)
36
+ raise "No port in process bot data? #{process_bot_data}" unless process_bot_data["port"]
40
37
 
41
- def stop_sidekiq_after_time(pid:, signal:)
42
- raise "Invalid PID: #{pid}" unless pid.to_s.match?(/\A\d+\Z/)
43
- raise "Invalid signal: #{signal}" unless VALID_SIGNALS.include?(signal)
38
+ backend_command = "cd #{release_path} && " \
39
+ "#{SSHKit.config.command_map.prefix[:bundle].join(" ")} bundle exec process_bot " \
40
+ "--command #{command} " \
41
+ "--port #{process_bot_data.fetch("port")}"
44
42
 
45
- time = ENV["STOP_AFTER_TIME"] || fetch(:sidekiq_stop_after_time)
46
- raise "Invalid time: #{time}" unless time.to_s.match?(/\A\d+\Z/)
43
+ if command == :graceful && !fetch(:process_bot_wait_for_gracefully_stopped).nil?
44
+ backend_command << " --wait-for-gracefully-stopped #{fetch(:process_bot_wait_for_gracefully_stopped)}"
45
+ end
47
46
 
48
- backend.execute "screen -dmS stopsidekiq#{pid} bash -c \"sleep #{time} && kill -#{signal} #{pid}\""
47
+ backend.execute backend_command
49
48
  end
50
49
 
51
- def running_sidekiq_processes
50
+ def running_process_bot_processes
52
51
  sidekiq_app_name = fetch(:sidekiq_app_name, fetch(:application))
53
52
  raise "No :sidekiq_app_name was set" unless sidekiq_app_name
54
53
 
55
54
  begin
56
- processes_output = backend.capture("ps a | egrep 'sidekiq ([0-9]+\.[0-9]+\.[0-9]+) #{Regexp.escape(sidekiq_app_name)}'")
55
+ processes_output = backend.capture("ps a | grep ProcessBot | grep sidekiq | grep -v '/usr/bin/SCREEN' | grep '#{Regexp.escape(sidekiq_app_name)}'")
57
56
  rescue SSHKit::Command::Failed
58
57
  # Fails when output is empty (when no processes found through grep)
59
- puts "No Sidekiq processes found"
58
+ puts "No ProcessBot Sidekiq processes found"
60
59
  return []
61
60
  end
62
61
 
62
+ parse_process_bot_process_from_ps(processes_output)
63
+ end
64
+
65
+ def parse_process_bot_process_from_ps(processes_output)
63
66
  processes = []
64
- processes_output.scan(/^\s*(\d+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.+)$/).each do |process_output|
65
- sidekiq_pid = process_output[0]
67
+ processes_output.scan(/^\s*(\d+)\s+(\S+)\s+(\S+)\s+(\S+)\s+ProcessBot (\{([^\n]+?)\})$/).each do |process_output|
68
+ process_bot_data = JSON.parse(process_output[4])
69
+ process_bot_pid = process_output[0]
70
+ process_bot_data["process_bot_pid"] = process_bot_pid
66
71
 
67
- processes << {pid: sidekiq_pid}
72
+ processes << process_bot_data
68
73
  end
69
74
 
70
75
  processes
@@ -86,7 +91,7 @@ module ProcessBot::Capistrano::SidekiqHelpers
86
91
  backend.capture(:echo, SSHKit.config.command_map[:bundle]).strip
87
92
  end
88
93
 
89
- def start_sidekiq(idx = 0) # rubocop:disable Metrics/AbcSize
94
+ def start_sidekiq(idx = 0) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
90
95
  releases = backend.capture(:ls, "-x", releases_path).split
91
96
  releases << release_timestamp.to_s if release_timestamp
92
97
  releases.uniq
@@ -95,13 +100,19 @@ module ProcessBot::Capistrano::SidekiqHelpers
95
100
  raise "Invalid release timestamp: #{release_timestamp}" unless latest_release_version
96
101
 
97
102
  args = [
103
+ "--command", "start",
98
104
  "--id", "sidekiq-#{latest_release_version}-#{idx}",
105
+ "--application", fetch(:sidekiq_app_name, fetch(:application)),
99
106
  "--handler", "sidekiq",
100
107
  "--bundle-prefix", SSHKit.config.command_map.prefix[:bundle].join(" "),
101
108
  "--sidekiq-environment", fetch(:sidekiq_env),
102
- "--port", 7050 + idx
109
+ "--port", idx + 7050,
110
+ "--release-path", release_path
103
111
  ]
104
- args += ["--log-file-path", fetch(:sidekiq_log)] if fetch(:sidekiq_log)
112
+
113
+ # Use screen for logging everything which is why this is disabled
114
+ # args += ["--log-file-path", fetch(:sidekiq_log)] if fetch(:sidekiq_log)
115
+
105
116
  args += ["--sidekiq-require", fetch(:sidekiq_require)] if fetch(:sidekiq_require)
106
117
  args += ["--sidekiq-tag", fetch(:sidekiq_tag)] if fetch(:sidekiq_tag)
107
118
  args += ["--sidekiq-queues", Array(fetch(:sidekiq_queue)).join(",")] if fetch(:sidekiq_queue)
@@ -112,13 +123,18 @@ module ProcessBot::Capistrano::SidekiqHelpers
112
123
  end
113
124
  args += fetch(:sidekiq_options) if fetch(:sidekiq_options)
114
125
 
115
- screen_args = ["-dmS sidekiq-#{idx}-#{latest_release_version}"]
116
- screen_args << "-L -Logfile #{fetch(:sidekiq_log)}" if fetch(:sidekiq_log)
126
+ screen_args = ["-dmS process-bot--sidekiq--#{idx}-#{latest_release_version}"]
127
+
128
+ if (process_bot_sidekiq_log = fetch(:process_bot_sidekig_log))
129
+ screen_args << "-L -Logfile #{process_bot_sidekiq_log}"
130
+ elsif fetch(:sidekiq_log)
131
+ screen_args << "-L -Logfile #{fetch(:sidekiq_log)}"
132
+ end
117
133
 
118
134
  process_bot_args = args.compact.map { |arg| "\"#{arg}\"" }
119
135
 
120
136
  command = "/usr/bin/screen #{screen_args.join(" ")} " \
121
- "bash -c 'cd #{release_path} && #{SSHKit.config.command_map.prefix[:bundle].join(" ")} bundle exec process_bot #{process_bot_args.join(" ")}'"
137
+ "bash -c 'cd #{release_path} && exec #{SSHKit.config.command_map.prefix[:bundle].join(" ")} bundle exec process_bot #{process_bot_args.join(" ")}'"
122
138
 
123
139
  puts "WARNING: A known bug prevents Sidekiq from starting when pty is set (which it is)" if fetch(:pty)
124
140
  puts "ProcessBot Sidekiq command: #{command}"
@@ -1,4 +1,5 @@
1
1
  class ProcessBot::Capistrano
2
2
  autoload :Puma, "#{__dir__}/capistrano/puma"
3
3
  autoload :Sidekiq, "#{__dir__}/capistrano/sidekiq"
4
+ autoload :SidekiqHelpers, "#{__dir__}/capistrano/sidekiq_helpers"
4
5
  end
@@ -0,0 +1,31 @@
1
+ require "socket"
2
+
3
+ class ProcessBot::ClientSocket
4
+ attr_reader :options
5
+
6
+ def initialize(options:)
7
+ @options = options
8
+ end
9
+
10
+ def client
11
+ @client ||= TCPSocket.new("localhost", options.fetch(:port).to_i)
12
+ end
13
+
14
+ def close
15
+ client.close
16
+ end
17
+
18
+ def send_command(data)
19
+ client.puts(JSON.generate(data))
20
+ response_raw = client.gets
21
+
22
+ # Happens if process is interrupted
23
+ return :nil if response_raw.nil?
24
+
25
+ response = JSON.parse(response_raw)
26
+
27
+ return :success if response.fetch("type") == "success"
28
+
29
+ raise "Command raised an error: #{response.fetch("message")}" if response.fetch("type") == "error"
30
+ end
31
+ end
@@ -1,3 +1,5 @@
1
+ require "socket"
2
+
1
3
  class ProcessBot::ControlSocket
2
4
  attr_reader :options, :process, :server
3
5
 
@@ -11,9 +13,16 @@ class ProcessBot::ControlSocket
11
13
  end
12
14
 
13
15
  def start
14
- require "socket"
16
+ @server = TCPServer.new("localhost", port)
17
+ run_client_loop
18
+
19
+ puts "TCPServer started"
20
+
21
+ options.events.call(:on_socket_opened, port: port)
22
+ end
15
23
 
16
- @server = TCPServer.new(port)
24
+ def stop
25
+ server.close
17
26
  end
18
27
 
19
28
  def run_client_loop
@@ -26,14 +35,26 @@ class ProcessBot::ControlSocket
26
35
  end
27
36
  end
28
37
 
29
- def handle_client(client)
30
- command = JSON.parse(client.gets)
31
- type = command.fetch("type")
32
-
33
- if type == "stop"
34
- process.stop
35
- else
36
- client.puts(JSON.generate(type: "error", message: "Unknown type: #{type}"))
38
+ def handle_client(client) # rubocop:disable Metrics/AbcSize
39
+ loop do
40
+ data = client.gets
41
+ break if data.nil? # Client disconnected
42
+
43
+ command = JSON.parse(data)
44
+ command_type = command.fetch("command")
45
+
46
+ if command_type == "graceful" || command_type == "stop"
47
+ begin
48
+ process.__send__(command_type)
49
+ client.puts(JSON.generate(type: "success"))
50
+ rescue => e # rubocop:disable Style/RescueStandardError
51
+ client.puts(JSON.generate(type: "error", message: e.message))
52
+
53
+ raise e
54
+ end
55
+ else
56
+ client.puts(JSON.generate(type: "error", message: "Unknown command: #{command_type}"))
57
+ end
37
58
  end
38
59
  end
39
60
  end
@@ -1,22 +1,34 @@
1
1
  class ProcessBot::Logger
2
- attr_reader :fp_log, :options
2
+ attr_reader :options
3
3
 
4
4
  def initialize(options:)
5
5
  @options = options
6
-
7
- open_file
8
6
  end
9
7
 
10
- def log(output)
11
- fp_log&.write(output)
12
- fp_log&.flush
8
+ def log(output, type: :stdout)
9
+ if type == :stdout
10
+ $stdout.print output
11
+ elsif type == :stderr
12
+ $stderr.print output
13
+ else
14
+ raise "Unknown type: #{type}"
15
+ end
16
+
17
+ return unless log_to_file?
18
+
19
+ fp_log.write(output)
20
+ fp_log.flush
13
21
  end
14
22
 
15
23
  def log_file_path
16
24
  options.fetch(:log_file_path)
17
25
  end
18
26
 
19
- def open_file
20
- @fp_log = File.open(log_file_path, "a")
27
+ def log_to_file?
28
+ options.present?(:log_file_path)
29
+ end
30
+
31
+ def fp_log
32
+ @fp_log ||= File.open(log_file_path, "a") if log_to_file?
21
33
  end
22
34
  end
@@ -5,8 +5,59 @@ class ProcessBot::Options
5
5
  @options = options
6
6
  end
7
7
 
8
- def fetch(*args, **opts, &blk)
9
- options.fetch(*args, **opts, &blk)
8
+ def [](key)
9
+ options[key]
10
+ end
11
+
12
+ def events
13
+ @events ||= begin
14
+ require "knjrbfw"
15
+
16
+ event_handler = ::Knj::Event_handler.new
17
+ event_handler.add_event(name: :on_process_started)
18
+ event_handler.add_event(name: :on_socket_opened)
19
+ event_handler
20
+ end
21
+ end
22
+
23
+ def fetch(...)
24
+ options.fetch(...)
25
+ end
26
+
27
+ def application_basename
28
+ @application_basename ||= begin
29
+ app_path_parts = release_path.split("/")
30
+
31
+ if release_path.include?("/releases/")
32
+ app_path_parts.pop(2)
33
+ elsif release_path.end_with?("/current")
34
+ app_path_parts.pop
35
+ end
36
+
37
+ app_path_parts.last
38
+ end
39
+ end
40
+
41
+ def possible_process_titles
42
+ possible_names = []
43
+
44
+ # Sidekiq name can by current Rails root base name
45
+ possible_names << application_basename
46
+
47
+ # Sidekiq name can be set tag name (but we wrongly read application for some reason?)
48
+ possible_names << options.fetch(:application)
49
+
50
+ possible_names
51
+ end
52
+
53
+ def possible_process_titles_joined_regex
54
+ possible_process_titles_joined_regex = ""
55
+ possible_process_titles.each_with_index do |possible_name, index|
56
+ possible_process_titles_joined_regex << "|" if index >= 1
57
+ possible_process_titles_joined_regex << Regexp.escape(possible_name)
58
+ end
59
+
60
+ possible_process_titles_joined_regex
10
61
  end
11
62
 
12
63
  def present?(key)
@@ -15,6 +66,10 @@ class ProcessBot::Options
15
66
  false
16
67
  end
17
68
 
69
+ def release_path
70
+ @release_path ||= fetch(:release_path)
71
+ end
72
+
18
73
  def set(key, value)
19
74
  options[key] = value
20
75
  end
@@ -3,8 +3,6 @@ class ProcessBot::Process::Handlers::Sidekiq
3
3
 
4
4
  def initialize(options)
5
5
  @options = options
6
-
7
- set_defaults
8
6
  end
9
7
 
10
8
  def fetch(*args, **opts)
@@ -21,35 +19,27 @@ class ProcessBot::Process::Handlers::Sidekiq
21
19
  options.set(*args, **opts)
22
20
  end
23
21
 
24
- def set_defaults
25
- set :sidekiq_default_hooks, true
26
- set :sidekiq_pid, -> { File.join(shared_path, "tmp", "pids", "sidekiq.pid") }
27
- set :sidekiq_timeout, 10
28
- set :sidekiq_roles, fetch(:sidekiq_role, :app)
29
- set :sidekiq_processes, 1
30
- set :sidekiq_options_per_process, nil
31
- end
32
-
33
- def command # rubocop:disable Metrics/AbcSize
22
+ def start_command # rubocop:disable Metrics/AbcSize
34
23
  args = []
35
24
 
36
25
  options.options.each do |key, value|
37
- if (match = key.to_s.match(/\Asidekiq-(.+)\Z/))
38
- sidekiq_key = match[1]
39
-
40
- if sidekiq_key == "queue"
41
- value.split(",").each do |queue|
42
- args.push "--queue #{value}"
43
- end
44
- else
45
- args.push "--#{sidekiq_key} #{value}"
26
+ next unless (match = key.to_s.match(/\Asidekiq_(.+)\Z/))
27
+
28
+ sidekiq_key = match[1]
29
+
30
+ if sidekiq_key == "queue"
31
+ value.split(",").each do |queue|
32
+ args.push "--queue #{queue}"
46
33
  end
34
+ else
35
+ args.push "--#{sidekiq_key} #{value}"
47
36
  end
48
37
  end
49
38
 
50
- command = ""
39
+ command = "bash -c 'cd #{options.fetch(:release_path)} && exec "
51
40
  command << "#{options.fetch(:bundle_prefix)} " if options.present?(:bundle_prefix)
52
41
  command << "bundle exec sidekiq #{args.compact.join(' ')}"
42
+ command << "'"
53
43
  command
54
44
  end
55
45
  end
@@ -1,5 +1,7 @@
1
+ require "knjrbfw"
2
+
1
3
  class ProcessBot::Process::Runner
2
- attr_reader :command, :exit_status, :logger, :monitor, :options, :stop_time
4
+ attr_reader :command, :exit_status, :logger, :monitor, :options, :pid, :stop_time, :subprocess_pid
3
5
 
4
6
  def initialize(command:, logger:, options:)
5
7
  @command = command
@@ -9,8 +11,12 @@ class ProcessBot::Process::Runner
9
11
  @output = []
10
12
  end
11
13
 
12
- def output(output:, type:) # rubocop:disable Lint/UnusedMethodArgument
13
- logger.log(output)
14
+ def output(output:, type:)
15
+ logger.log(output, type: type)
16
+ end
17
+
18
+ def running?
19
+ !stop_time
14
20
  end
15
21
 
16
22
  def run # rubocop:disable Metrics/AbcSize
@@ -20,8 +26,8 @@ class ProcessBot::Process::Runner
20
26
  require "pty"
21
27
 
22
28
  PTY.spawn(command, err: stderr_writer.fileno) do |stdout, _stdin, pid|
23
- @pid = pid
24
- logger.log "Command running with PID #{pid}: #{command}"
29
+ @subprocess_pid = pid
30
+ logger.log "Command running with PID #{pid}: #{command}\n"
25
31
 
26
32
  stdout_reader_thread = Thread.new do
27
33
  stdout.each_char do |chunk|
@@ -32,7 +38,7 @@ class ProcessBot::Process::Runner
32
38
  rescue Errno::EIO
33
39
  # Process done
34
40
  ensure
35
- status = Process::Status.wait(@pid, 0)
41
+ status = Process::Status.wait(subprocess_pid, 0)
36
42
 
37
43
  @exit_status = status.exitstatus
38
44
  stderr_writer.close
@@ -46,10 +52,56 @@ class ProcessBot::Process::Runner
46
52
  end
47
53
  end
48
54
 
55
+ find_sidekiq_pid
56
+
49
57
  stdout_reader_thread.join
50
58
  stderr_reader_thread.join
51
59
 
52
60
  @stop_time = Time.new
53
61
  end
54
62
  end
63
+
64
+ def subprocess_pgid
65
+ @subprocess_pgid ||= Process.getpgid(subprocess_pid)
66
+ end
67
+
68
+ def sidekiq_app_name
69
+ options.fetch(:application)
70
+ end
71
+
72
+ def find_sidekiq_pid # rubocop:disable Metrics/AbcSize
73
+ Thread.new do
74
+ while running? && !pid
75
+ Knj::Unix_proc.list("grep" => "sidekiq") do |process|
76
+ cmd = process.data.fetch("cmd")
77
+
78
+ if /sidekiq ([0-9]+\.[0-9]+\.[0-9]+) (#{options.possible_process_titles_joined_regex})/.match?(cmd)
79
+ sidekiq_pid = process.data.fetch("pid").to_i
80
+
81
+ begin
82
+ sidekiq_pgid = Process.getpgid(sidekiq_pid)
83
+ rescue Errno::ESRCH
84
+ # Process no longer running
85
+ end
86
+
87
+ if subprocess_pgid == sidekiq_pgid
88
+ puts "FOUND PID: #{sidekiq_pid}"
89
+
90
+ @pid = sidekiq_pid
91
+ options.events.call(:on_process_started, pid: pid)
92
+
93
+ break
94
+ else
95
+ puts "PGID didn't match - Sidekiq: #{sidekiq_pgid} Own: #{subprocess_pgid}"
96
+ end
97
+ end
98
+ end
99
+
100
+ unless pid
101
+ puts "Waiting 1 second before trying to find Sidekiq PID again"
102
+ sleep 1
103
+ end
104
+ end
105
+ end
106
+ end
55
107
  end
@@ -1,35 +1,68 @@
1
+ require "json"
2
+
1
3
  class ProcessBot::Process
2
4
  autoload :Handlers, "#{__dir__}/process/handlers"
3
5
  autoload :Runner, "#{__dir__}/process/runner"
4
6
 
5
- attr_reader :options, :stopped
7
+ attr_reader :current_pid, :current_process_title, :options, :port, :stopped
6
8
 
7
9
  def initialize(options)
8
10
  @options = options
9
11
  @stopped = false
10
- end
11
12
 
12
- def logger
13
- @logger ||= ProcessBot::Logger.new(options: options)
13
+ options.events.connect(:on_process_started, &method(:on_process_started)) # rubocop:disable Performance/MethodObjectAsBlock
14
+ options.events.connect(:on_socket_opened, &method(:on_socket_opened)) # rubocop:disable Performance/MethodObjectAsBlock
15
+
16
+ logger.log("Options: #{options.options}")
14
17
  end
15
18
 
16
- def start_control_socket
17
- @control_socket = ProcessBot::ControlSocket.new(options: options, process: self)
18
- @control_socket.start
19
+ def execute!
20
+ command = options.fetch(:command)
21
+
22
+ if command == "start"
23
+ start
24
+ elsif command == "graceful" || command == "stop"
25
+ client.send_command(command: command)
26
+ else
27
+ raise "Unknown command: #{command}"
28
+ end
19
29
  end
20
30
 
21
- def stop
22
- @stopped = true
31
+ def client
32
+ @client ||= ProcessBot::ClientSocket.new(options: options)
23
33
  end
24
34
 
25
35
  def handler_class
26
36
  @handler_class ||= begin
27
- require_relative "process/handlers/#{options.fetch(:handler)}"
28
- ProcessBot::Process::Handlers.const_get(StringCases.snake_to_camel(options.fetch(:handler)))
37
+ require_relative "process/handlers/#{handler_name}"
38
+ ProcessBot::Process::Handlers.const_get(StringCases.snake_to_camel(handler_name))
29
39
  end
30
40
  end
31
41
 
32
- def execute!
42
+ def handler_name
43
+ @handler_name ||= options.fetch(:handler)
44
+ end
45
+
46
+ def logger
47
+ @logger ||= ProcessBot::Logger.new(options: options)
48
+ end
49
+
50
+ def on_process_started(_event_name, pid:)
51
+ @current_pid = pid
52
+ update_process_title
53
+ end
54
+
55
+ def on_socket_opened(_event_name, port:)
56
+ @port = port
57
+ update_process_title
58
+ end
59
+
60
+ def start_control_socket
61
+ @control_socket = ProcessBot::ControlSocket.new(options: options, process: self)
62
+ @control_socket.start
63
+ end
64
+
65
+ def start
33
66
  start_control_socket
34
67
 
35
68
  loop do
@@ -44,9 +77,77 @@ class ProcessBot::Process
44
77
  end
45
78
  end
46
79
 
80
+ def graceful
81
+ @stopped = true
82
+
83
+ unless current_pid
84
+ warn "#{handler_name} not running with a PID"
85
+ return
86
+ end
87
+
88
+ Process.kill("TSTP", current_pid)
89
+
90
+ if options[:wait_for_gracefully_stopped] == "false"
91
+ Thread.new { wait_for_no_jobs_and_stop_sidekiq }
92
+ else
93
+ wait_for_no_jobs_and_stop_sidekiq
94
+ end
95
+ end
96
+
97
+ def stop
98
+ @stopped = true
99
+
100
+ unless current_pid
101
+ warn "#{handler_name} not running with a PID"
102
+ return
103
+ end
104
+
105
+ Process.kill("TERM", current_pid)
106
+ end
107
+
47
108
  def run
48
109
  handler_instance = handler_class.new(options)
49
- runner = ProcessBot::Process::Runner.new(command: handler_instance.command, logger: logger, options: options)
110
+ runner = ProcessBot::Process::Runner.new(command: handler_instance.start_command, logger: logger, options: options)
50
111
  runner.run
51
112
  end
113
+
114
+ def update_process_title
115
+ process_args = {application: options[:application], handler: handler_name, id: options[:id], pid: current_pid, port: port}
116
+ @current_process_title = "ProcessBot #{JSON.generate(process_args)}"
117
+ Process.setproctitle(current_process_title)
118
+ end
119
+
120
+ def wait_for_no_jobs # rubocop:disable Metrics/AbcSize
121
+ loop do
122
+ found_process = false
123
+
124
+ Knj::Unix_proc.list("grep" => current_pid) do |process|
125
+ process_command = process.data.fetch("cmd")
126
+ process_pid = process.data.fetch("pid").to_i
127
+ next unless process_pid == current_pid
128
+
129
+ found_process = true
130
+ sidekiq_regex = /\Asidekiq (\d+).(\d+).(\d+) (#{options.possible_process_titles_joined_regex}) \[(\d+) of (\d+)(\]|) (.+?)(\]|)\Z/
131
+ match = process_command.match(sidekiq_regex)
132
+ raise "Couldnt match Sidekiq command: #{process_command} with Sidekiq regex: #{sidekiq_regex}" unless match
133
+
134
+ running_jobs = match[5].to_i
135
+
136
+ puts "running_jobs: #{running_jobs}"
137
+
138
+ return if running_jobs.zero? # rubocop:disable Lint/NonLocalExitFromIterator
139
+ end
140
+
141
+ raise "Couldn't find running process with PID #{current_pid}" unless found_process
142
+
143
+ sleep 1
144
+ end
145
+ end
146
+
147
+ def wait_for_no_jobs_and_stop_sidekiq
148
+ puts "Wait for no jobs and Stop sidekiq"
149
+
150
+ wait_for_no_jobs
151
+ stop
152
+ end
52
153
  end
@@ -1,3 +1,3 @@
1
1
  module ProcessBot
2
- VERSION = "0.1.2"
2
+ VERSION = "0.1.3".freeze
3
3
  end
data/lib/process_bot.rb CHANGED
@@ -4,6 +4,7 @@ module ProcessBot
4
4
  class Error < StandardError; end
5
5
 
6
6
  autoload :Capistrano, "#{__dir__}/process_bot/capistrano"
7
+ autoload :ClientSocket, "#{__dir__}/process_bot/client_socket"
7
8
  autoload :ControlSocket, "#{__dir__}/process_bot/control_socket"
8
9
  autoload :Logger, "#{__dir__}/process_bot/logger"
9
10
  autoload :Options, "#{__dir__}/process_bot/options"
data/peak_flow.yml CHANGED
@@ -1,4 +1,20 @@
1
1
  rvm: true
2
- script:
3
- - bundle exec rspec
4
- - bundle exec rubocop
2
+ builds:
3
+ build_1:
4
+ environment:
5
+ RUBY_VERSION: 2.7.8
6
+ name: Ruby 2.7.8
7
+ script:
8
+ - bundle exec rspec
9
+ build_2:
10
+ environment:
11
+ RUBY_VERSION: 3.2.2
12
+ name: Ruby 3.2.2
13
+ script:
14
+ - bundle exec rspec
15
+ build_3:
16
+ environment:
17
+ RUBY_VERSION: 2.7.8
18
+ name: Rubocop
19
+ script:
20
+ - bundle exec rubocop
data/process_bot.gemspec CHANGED
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
12
12
  spec.description = "Run and control processes."
13
13
  spec.homepage = "https://github.com/kaspernj/process_bot"
14
14
  spec.license = "MIT"
15
- spec.required_ruby_version = ">= 2.6.0"
15
+ spec.required_ruby_version = ">= 2.7.0"
16
16
 
17
17
  spec.metadata["allowed_push_host"] = "https://rubygems.org"
18
18
 
@@ -31,15 +31,7 @@ Gem::Specification.new do |spec|
31
31
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
32
32
  spec.require_paths = ["lib"]
33
33
 
34
- # Uncomment to register a new dependency of your gem
35
- # spec.add_dependency "example-gem", "~> 1.0"
34
+ spec.add_dependency "knjrbfw", ">= 0.0.116"
36
35
 
37
- spec.add_development_dependency "rubocop"
38
- spec.add_development_dependency "rubocop-performance"
39
- spec.add_development_dependency "rubocop-rake"
40
- spec.add_development_dependency "rubocop-rspec"
41
-
42
- # For more information and examples about making a new gem, check out our
43
- # guide at: https://bundler.io/guides/creating_gem.html
44
36
  spec.metadata["rubygems_mfa_required"] = "true"
45
37
  end
metadata CHANGED
@@ -1,71 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: process_bot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - kaspernj
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-10-18 00:00:00.000000000 Z
11
+ date: 2023-07-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: rubocop
14
+ name: knjrbfw
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :development
19
+ version: 0.0.116
20
+ type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
27
- - !ruby/object:Gem::Dependency
28
- name: rubocop-performance
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: rubocop-rake
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
- - !ruby/object:Gem::Dependency
56
- name: rubocop-rspec
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: '0'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: '0'
26
+ version: 0.0.116
69
27
  description: Run and control processes.
70
28
  email:
71
29
  - k@spernj.org
@@ -76,6 +34,7 @@ extra_rdoc_files: []
76
34
  files:
77
35
  - ".rspec"
78
36
  - ".rubocop.yml"
37
+ - ".ruby-version"
79
38
  - CHANGELOG.md
80
39
  - Gemfile
81
40
  - Gemfile.lock
@@ -91,6 +50,7 @@ files:
91
50
  - lib/process_bot/capistrano/sidekiq.rake
92
51
  - lib/process_bot/capistrano/sidekiq.rb
93
52
  - lib/process_bot/capistrano/sidekiq_helpers.rb
53
+ - lib/process_bot/client_socket.rb
94
54
  - lib/process_bot/control_socket.rb
95
55
  - lib/process_bot/logger.rb
96
56
  - lib/process_bot/options.rb
@@ -119,14 +79,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
119
79
  requirements:
120
80
  - - ">="
121
81
  - !ruby/object:Gem::Version
122
- version: 2.6.0
82
+ version: 2.7.0
123
83
  required_rubygems_version: !ruby/object:Gem::Requirement
124
84
  requirements:
125
85
  - - ">="
126
86
  - !ruby/object:Gem::Version
127
87
  version: '0'
128
88
  requirements: []
129
- rubygems_version: 3.3.7
89
+ rubygems_version: 3.1.6
130
90
  signing_key:
131
91
  specification_version: 4
132
92
  summary: Run and control processes.