process_bot 0.1.2 → 0.1.3

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