honcho 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3429cf3e359e2fc30f07682a77b4759205a9da00
4
+ data.tar.gz: 03fa822e64a34857dae9ca770abae29a80a17e05
5
+ SHA512:
6
+ metadata.gz: acb22985e32a0e7dbca8aaa5104b4de987f3b72c4eb9102d3cf8e596217918666fe685d9dbf3400755a2abcd59928ba9425af2b16180a691454b7415986649bd
7
+ data.tar.gz: 046b0e51b48ee9f5a5a8983c7b5bc3f0ff270f1b9c92fce0fe3f3aa105544843051d6d85b4efdd257063e9ccd1340fadf7544a48af0b9624f4f3ddd0db096ab6
data/bin/honcho ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/honcho'
4
+
5
+ Honcho.new.run
@@ -0,0 +1,26 @@
1
+ class Honcho
2
+ module Adapters
3
+ class Base
4
+ def initialize(config:, redis:)
5
+ @redis = redis
6
+ @config = config
7
+ end
8
+
9
+ attr_reader :config, :redis
10
+
11
+ def run?
12
+ work_to_do? || work_being_done?
13
+ end
14
+
15
+ private
16
+
17
+ def work_to_do?
18
+ fail NotImplementedError, "please define #{this.class.name}##{__method__}"
19
+ end
20
+
21
+ def work_being_done?
22
+ fail NotImplementedError, "please define #{this.class.name}##{__method__}"
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,22 @@
1
+ class Honcho
2
+ module Adapters
3
+ class Resque < Base
4
+ private
5
+
6
+ def work_to_do?
7
+ queues = redis.smembers("#{namespace}:queues")
8
+ counts = queues.map { |q| redis.llen("#{namespace}:queue:#{q}") }
9
+ counts.any?(&:nonzero?)
10
+ end
11
+
12
+ def work_being_done?
13
+ # No way to tell via redis if work is being done in resque? Booo.
14
+ false
15
+ end
16
+
17
+ def namespace
18
+ config.fetch('namespace')
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,25 @@
1
+ class Honcho
2
+ module Adapters
3
+ class Sidekiq < Base
4
+ private
5
+
6
+ def work_to_do?
7
+ queues = redis.keys("#{namespace}:queue:*")
8
+ counts = queues.map { |q| redis.llen(q) }
9
+ counts.any?(&:nonzero?)
10
+ end
11
+
12
+ def work_being_done?
13
+ processes = redis.smembers("#{namespace}:processes")
14
+ counts = processes.map do |process|
15
+ redis.hget("#{namespace}:#{process}", 'busy').to_i
16
+ end
17
+ counts.any?(&:nonzero?)
18
+ end
19
+
20
+ def namespace
21
+ config.fetch('namespace')
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,3 @@
1
+ require_relative './adapters/base'
2
+ require_relative './adapters/resque'
3
+ require_relative './adapters/sidekiq'
data/lib/honcho.rb ADDED
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'redis'
4
+ require 'sys/proctable'
5
+ require 'time'
6
+ require 'stringio'
7
+ require 'yaml'
8
+ require_relative './honcho/adapters'
9
+
10
+ Thread.abort_on_exception = true
11
+
12
+ class Honcho
13
+ COLORS = {
14
+ red: 31,
15
+ green: 32,
16
+ yellow: 33,
17
+ blue: 34,
18
+ magenta: 35,
19
+ cyan: 36,
20
+ bright_black: 30,
21
+ bright_red: 31,
22
+ bright_green: 32,
23
+ bright_yellow: 33,
24
+ bright_blue: 34,
25
+ bright_magenta: 35,
26
+ bright_cyan: 36,
27
+ bright_white: 37
28
+ }.freeze
29
+
30
+ def initialize
31
+ @running = {}
32
+ @stopping = {}
33
+ @redis = Redis.new
34
+ @adapters = build_adapters
35
+ @colors = assign_colors
36
+ end
37
+
38
+ attr_reader :adapters, :running, :stopping, :redis, :colors
39
+
40
+ def run
41
+ trap(:INT) { term_all && exit }
42
+ trap(:TERM) { term_all && exit }
43
+ loop do
44
+ check_for_work
45
+ sleep(interval)
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def apps
52
+ config['apps']
53
+ end
54
+
55
+ def command_template
56
+ config['command_template'] || '%s'
57
+ end
58
+
59
+ def stop_delay
60
+ config['stop_delay'] || 30
61
+ end
62
+
63
+ def interval
64
+ config['interval'] || 2
65
+ end
66
+
67
+ def config
68
+ @config ||= YAML.load_file('honcho.yml')
69
+ end
70
+
71
+ def check_for_work
72
+ adapters.each do |adapter|
73
+ if adapter.run?
74
+ start(adapter)
75
+ else
76
+ stop(adapter)
77
+ end
78
+ end
79
+ end
80
+
81
+ def start(adapter)
82
+ stopping.delete(adapter)
83
+ return if running[adapter]
84
+ log(adapter.config['name'], "STARTING\n")
85
+ running[adapter] = start_command(adapter)
86
+ end
87
+
88
+ def start_command(adapter)
89
+ command = adapter.config['command']
90
+ Array(command).map do |cmd|
91
+ rout, wout = IO.pipe
92
+ pid = spawn(adapter.config['path'], cmd, wout)
93
+ Thread.new do
94
+ log(adapter.config['name'], rout.gets) until rout.eof?
95
+ end
96
+ [pid, wout]
97
+ end
98
+ end
99
+
100
+ def spawn(path, cmd, out)
101
+ Process.spawn(
102
+ "cd '#{path}' && " + command_template % cmd,
103
+ pgroup: true,
104
+ err: out,
105
+ out: out
106
+ )
107
+ end
108
+
109
+ def stop(adapter)
110
+ return unless running[adapter]
111
+ if should_stop?(adapter)
112
+ really_stop(adapter)
113
+ else
114
+ stopping[adapter] ||= stop_delay
115
+ stopping[adapter] -= interval
116
+ end
117
+ end
118
+
119
+ def should_stop?(adapter)
120
+ stopping[adapter] && stopping[adapter] <= 0
121
+ end
122
+
123
+ def really_stop(adapter)
124
+ log(adapter.config['name'], "STOPPING\n")
125
+ stopping.delete(adapter)
126
+ return unless running[adapter]
127
+ running[adapter].each do |(pid, wout)|
128
+ Process.kill('-TERM', pid)
129
+ wout.close
130
+ end
131
+ running.delete(adapter)
132
+ end
133
+
134
+ def term_all
135
+ running.values.flatten.each do |(pid)|
136
+ Process.kill('-TERM', pid)
137
+ end
138
+ end
139
+
140
+ def log(name, message)
141
+ color = colors[name]
142
+ $stdout.write("\e[#{color}m#{name.rjust(label_width)}:\e[0m #{message}")
143
+ end
144
+
145
+ def label_width
146
+ @label_width ||= apps.keys.map(&:size).max
147
+ end
148
+
149
+ def assign_colors
150
+ color_values = COLORS.values
151
+ apps.keys.each_with_object({}) do |app, hash|
152
+ hash[app] = color_values.shift
153
+ end
154
+ end
155
+
156
+ def build_adapters
157
+ apps.flat_map do |app, config|
158
+ config.map do |type, worker_config|
159
+ adapter = adapter_from_type(type)
160
+ next if adapter.nil?
161
+ adapter.new(
162
+ config: worker_config.merge('name' => app, 'path' => config['path']),
163
+ redis: redis
164
+ )
165
+ end
166
+ end.compact
167
+ end
168
+
169
+ def adapter_from_type(type)
170
+ case type
171
+ when 'sidekiq'
172
+ Adapters::Sidekiq
173
+ when 'resque'
174
+ Adapters::Resque
175
+ when 'path'
176
+ nil
177
+ else
178
+ fail "Unknown type #{type}"
179
+ end
180
+ end
181
+ end
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: honcho
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Tim Morgn
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-05-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: redis
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description: Sidekiq- and Resque-aware process manager (alternative to Foreman)
28
+ email:
29
+ - tim@timmorgan.org
30
+ executables:
31
+ - honcho
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - bin/honcho
36
+ - lib/honcho.rb
37
+ - lib/honcho/adapters.rb
38
+ - lib/honcho/adapters/base.rb
39
+ - lib/honcho/adapters/resque.rb
40
+ - lib/honcho/adapters/sidekiq.rb
41
+ homepage: https://github.com/seven1m/honcho
42
+ licenses:
43
+ - MIT
44
+ metadata: {}
45
+ post_install_message:
46
+ rdoc_options: []
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ requirements: []
60
+ rubyforge_project:
61
+ rubygems_version: 2.5.1
62
+ signing_key:
63
+ specification_version: 4
64
+ summary: Sidekiq- and Resque-aware process manager (alternative to Foreman)
65
+ test_files: []
66
+ has_rdoc: