captainu-tincan 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/.rspec +2 -0
- data/.rubocop.yml +8 -0
- data/.ruby-version +1 -0
- data/.travis.yml +22 -0
- data/Gemfile +4 -0
- data/Rakefile +10 -0
- data/bin/tincan +14 -0
- data/bin/tincanctl +91 -0
- data/lib/tincan.rb +8 -0
- data/lib/tincan/cli.rb +305 -0
- data/lib/tincan/failure.rb +67 -0
- data/lib/tincan/message.rb +79 -0
- data/lib/tincan/receiver.rb +167 -0
- data/lib/tincan/sender.rb +101 -0
- data/lib/tincan/version.rb +3 -0
- data/license.markdown +22 -0
- data/readme.markdown +120 -0
- data/spec/failure_spec.rb +58 -0
- data/spec/fixtures/failure.json +1 -0
- data/spec/fixtures/message.json +1 -0
- data/spec/message_spec.rb +68 -0
- data/spec/receiver_spec.rb +202 -0
- data/spec/sender_spec.rb +121 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/support/dummy.rb +8 -0
- data/spec/support/futuristic.rb +61 -0
- data/spec/tincan_spec.rb +7 -0
- data/tincan.gemspec +35 -0
- metadata +213 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 54d85276c1f0b834e9bfde2089bf93af0df86168
|
4
|
+
data.tar.gz: 4571dace4673d9e6c01b74fec3e490840e9d4069
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7303de924c1ee265ef03500ee6386fabb2d3b5b9b27dd37d965c371a462b23e57b9f1c45a857b6b0d20df9951fb2e3983582813c054648d66093d9264abe47d6
|
7
|
+
data.tar.gz: 4eea6964d2a1ed9d8aae2a3f4c0caceb5f765e0505a0da16770b29a1dd179824c838d808c19f12dd8c358e4836a9bc1da201ac416e53df7d3acb17d156af48bd
|
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.0.0-p353
|
data/.travis.yml
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
language: ruby
|
2
|
+
cache: bundler
|
3
|
+
|
4
|
+
rvm:
|
5
|
+
- 2.0.0
|
6
|
+
- 2.1.0
|
7
|
+
|
8
|
+
services:
|
9
|
+
- redis-server
|
10
|
+
|
11
|
+
script: 'bundle exec rake'
|
12
|
+
|
13
|
+
notifications:
|
14
|
+
email:
|
15
|
+
recipients:
|
16
|
+
- ben@captainu.com
|
17
|
+
on_failure: change
|
18
|
+
on_success: never
|
19
|
+
|
20
|
+
addons:
|
21
|
+
code_climate:
|
22
|
+
repo_token: cba73555a55853fa1641eb318bc937205ed57e318af6ce10dff3b224f8d01ef7
|
data/Gemfile
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'rspec/core/rake_task'
|
2
|
+
require 'bundler/gem_tasks'
|
3
|
+
|
4
|
+
# Default directory to look in is `/spec`
|
5
|
+
# Run with `rake spec`
|
6
|
+
RSpec::Core::RakeTask.new(:spec) do |task|
|
7
|
+
task.rspec_opts = %w(--color --format documentation)
|
8
|
+
end
|
9
|
+
|
10
|
+
task default: :spec
|
data/bin/tincan
ADDED
data/bin/tincanctl
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
# A control bin for Tincan's CLI, shamefully replicated from Sidekiq's.
|
6
|
+
class Tincanctl
|
7
|
+
DEFAULT_TIMEOUT = 10
|
8
|
+
|
9
|
+
attr_reader :stage, :pidfile, :timeout
|
10
|
+
|
11
|
+
def self.print_usage
|
12
|
+
puts
|
13
|
+
puts "Usage: #{File.basename($PROGRAM_NAME)} <command> <pidfile> <timeout>"
|
14
|
+
puts " where <command> is either 'quiet', 'stop' or 'shutdown'"
|
15
|
+
puts ' <pidfile> is path to a pidfile'
|
16
|
+
puts " <timeout> is number of seconds to wait till Tincan exits (default: #{Tincanctl::DEFAULT_TIMEOUT})"
|
17
|
+
puts
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(stage, pidfile, timeout)
|
21
|
+
@stage = stage
|
22
|
+
@pidfile = pidfile
|
23
|
+
@timeout = timeout
|
24
|
+
|
25
|
+
done('No pidfile given', :error) unless pidfile
|
26
|
+
done("Pidfile #{pidfile} does not exist", :warn) unless File.exist?(pidfile)
|
27
|
+
done('Invalid pidfile content', :error) if pid == 0
|
28
|
+
|
29
|
+
fetch_process
|
30
|
+
|
31
|
+
begin
|
32
|
+
send(stage)
|
33
|
+
rescue NoMethodError
|
34
|
+
done 'Invalid control command', :error
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def fetch_process
|
39
|
+
Process.getpgid(pid)
|
40
|
+
rescue Errno::ESRCH
|
41
|
+
done "Process doesn't exist", :error
|
42
|
+
end
|
43
|
+
|
44
|
+
def done(msg, error = nil)
|
45
|
+
puts msg
|
46
|
+
exit(exit_signal(error))
|
47
|
+
end
|
48
|
+
|
49
|
+
def exit_signal(error)
|
50
|
+
(error == :error) ? 1 : 0
|
51
|
+
end
|
52
|
+
|
53
|
+
def pid
|
54
|
+
@pid ||= File.read(pidfile).to_i
|
55
|
+
end
|
56
|
+
|
57
|
+
def quiet
|
58
|
+
`kill -USR1 #{pid}`
|
59
|
+
end
|
60
|
+
|
61
|
+
def stop
|
62
|
+
`kill -TERM #{pid}`
|
63
|
+
timeout.times do
|
64
|
+
begin
|
65
|
+
Process.getpgid(pid)
|
66
|
+
rescue Errno::ESRCH
|
67
|
+
FileUtils.rm_f pidfile
|
68
|
+
done 'Tincan shut down gracefully.'
|
69
|
+
end
|
70
|
+
sleep 1
|
71
|
+
end
|
72
|
+
`kill -9 #{pid}`
|
73
|
+
FileUtils.rm_f pidfile
|
74
|
+
done 'Tincan shut down forcefully.'
|
75
|
+
end
|
76
|
+
|
77
|
+
def shutdown
|
78
|
+
quiet
|
79
|
+
stop
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
if ARGV.length < 2
|
84
|
+
Tincanctl.print_usage
|
85
|
+
else
|
86
|
+
stage = ARGV[0]
|
87
|
+
pidfile = ARGV[1]
|
88
|
+
timeout = ARGV[2].to_i
|
89
|
+
timeout = Tincanctl::DEFAULT_TIMEOUT if timeout == 0
|
90
|
+
Tincanctl.new(stage, pidfile, timeout)
|
91
|
+
end
|
data/lib/tincan.rb
ADDED
data/lib/tincan/cli.rb
ADDED
@@ -0,0 +1,305 @@
|
|
1
|
+
$stdout.sync = true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
require 'erb'
|
5
|
+
require 'singleton'
|
6
|
+
require 'optparse'
|
7
|
+
require 'fileutils'
|
8
|
+
require 'logger'
|
9
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
10
|
+
|
11
|
+
require 'tincan/receiver'
|
12
|
+
require 'tincan/version'
|
13
|
+
|
14
|
+
module Tincan
|
15
|
+
class Shutdown < Interrupt; end
|
16
|
+
|
17
|
+
class CLI
|
18
|
+
include Singleton
|
19
|
+
|
20
|
+
# Used for CLI testing
|
21
|
+
attr_accessor :code
|
22
|
+
attr_accessor :receiver
|
23
|
+
attr_accessor :environment
|
24
|
+
attr_accessor :logger
|
25
|
+
attr_accessor :config
|
26
|
+
attr_accessor :thread
|
27
|
+
|
28
|
+
def initialize
|
29
|
+
@code = nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def parse(args = ARGV)
|
33
|
+
@code = nil
|
34
|
+
|
35
|
+
setup_config(args)
|
36
|
+
initialize_logger
|
37
|
+
check_required_keys!
|
38
|
+
validate!
|
39
|
+
daemonize
|
40
|
+
write_pid
|
41
|
+
end
|
42
|
+
|
43
|
+
def run
|
44
|
+
boot_system
|
45
|
+
print_banner
|
46
|
+
|
47
|
+
self_read, self_write = IO.pipe
|
48
|
+
|
49
|
+
%w(INT TERM USR1 USR2 TTIN).each do |sig|
|
50
|
+
begin
|
51
|
+
trap sig do
|
52
|
+
self_write.puts(sig)
|
53
|
+
end
|
54
|
+
rescue ArgumentError
|
55
|
+
puts "Signal #{sig} not supported."
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
logger.info "Running in #{RUBY_DESCRIPTION}."
|
60
|
+
|
61
|
+
unless config[:daemon]
|
62
|
+
logger.info 'Now listening for notifications. Hit Ctrl-C to stop.'
|
63
|
+
end
|
64
|
+
|
65
|
+
## TODO: FIX THIS
|
66
|
+
|
67
|
+
@receiver = Tincan::Receiver.new do |r|
|
68
|
+
r.logger = @logger
|
69
|
+
r.redis_host = config[:redis_host]
|
70
|
+
r.client_name = config[:client_name]
|
71
|
+
r.namespace = config[:namespace]
|
72
|
+
r.listen_to = Hash[config[:listen_to].map do |object, runners|
|
73
|
+
[object.to_sym, runners.map do |runner|
|
74
|
+
klass, method_name = runner.split('.')
|
75
|
+
klass = klass.constantize
|
76
|
+
->(data) { klass.send(method_name.to_sym, data) }
|
77
|
+
end]
|
78
|
+
end]
|
79
|
+
r.on_exception = lambda do |ex, context|
|
80
|
+
@logger.error ex
|
81
|
+
@logger.error ex.backtrace
|
82
|
+
Airbrake.notify_or_ignore(ex, parameters: context)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
begin
|
87
|
+
@thread = Thread.new { @receiver.listen }
|
88
|
+
|
89
|
+
while readable_io = IO.select([self_read])
|
90
|
+
signal = readable_io.first[0].gets.strip
|
91
|
+
handle_signal(signal)
|
92
|
+
end
|
93
|
+
rescue Interrupt
|
94
|
+
logger.info 'Shutting down.'
|
95
|
+
# @thread.stop
|
96
|
+
exit(0)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
def print_banner
|
103
|
+
# Print logo and banner for development
|
104
|
+
return unless environment == 'development' && $stdout.tty?
|
105
|
+
puts "\e[#{31}m"
|
106
|
+
puts 'Welcome to tincan!'
|
107
|
+
puts "\e[0m"
|
108
|
+
end
|
109
|
+
|
110
|
+
def handle_signal(sig)
|
111
|
+
@logger.debug "Got #{sig} signal"
|
112
|
+
case sig
|
113
|
+
when 'INT'
|
114
|
+
fail Interrupt
|
115
|
+
when 'TERM'
|
116
|
+
fail Interrupt
|
117
|
+
when 'USR1'
|
118
|
+
@logger.info 'Received USR1, no longer accepting new work'
|
119
|
+
@thread.stop
|
120
|
+
# when 'USR2'
|
121
|
+
# if config[:logfile]
|
122
|
+
# @logger.info 'Received USR2, reopening log file'
|
123
|
+
# Tincan::Logging.reopen_logs
|
124
|
+
# end
|
125
|
+
when 'TTIN'
|
126
|
+
Thread.list.each do |thread|
|
127
|
+
label = thread['label']
|
128
|
+
@logger.info "Thread TID-#{thread.object_id.to_s(36)} #{label}"
|
129
|
+
if thread.backtrace
|
130
|
+
@logger.info thread.backtrace.join("\n")
|
131
|
+
else
|
132
|
+
@logger.info '<no backtrace available>'
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def daemonize
|
139
|
+
return unless config[:daemon]
|
140
|
+
|
141
|
+
unless config[:logfile]
|
142
|
+
fail ArgumentError,
|
143
|
+
"You really should set a logfile if you're going to daemonize"
|
144
|
+
end
|
145
|
+
files_to_reopen = []
|
146
|
+
ObjectSpace.each_object(File) do |file|
|
147
|
+
files_to_reopen << file unless file.closed?
|
148
|
+
end
|
149
|
+
|
150
|
+
::Process.daemon(true, true)
|
151
|
+
|
152
|
+
files_to_reopen.each do |file|
|
153
|
+
begin
|
154
|
+
file.reopen file.path, 'a+'
|
155
|
+
file.sync = true
|
156
|
+
rescue ::Exception
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
[$stdout, $stderr].each do |io|
|
161
|
+
File.open(config[:logfile], 'ab') do |f|
|
162
|
+
io.reopen(f)
|
163
|
+
end
|
164
|
+
io.sync = true
|
165
|
+
end
|
166
|
+
$stdin.reopen('/dev/null')
|
167
|
+
|
168
|
+
initialize_logger
|
169
|
+
end
|
170
|
+
|
171
|
+
def set_environment(cli_env)
|
172
|
+
@environment = cli_env || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
|
173
|
+
end
|
174
|
+
|
175
|
+
def setup_config(args)
|
176
|
+
opts = parse_options(args)
|
177
|
+
set_environment opts[:environment]
|
178
|
+
opts = parse_config(opts[:config_file]).merge(opts) if opts[:config_file]
|
179
|
+
opts[:strict] = true if opts[:strict].nil?
|
180
|
+
@config = opts
|
181
|
+
end
|
182
|
+
|
183
|
+
def boot_system
|
184
|
+
ENV['RACK_ENV'] = ENV['RAILS_ENV'] = environment
|
185
|
+
|
186
|
+
unless File.exist?(config[:require])
|
187
|
+
fail ArgumentError, "#{config[:require]} does not exist"
|
188
|
+
end
|
189
|
+
|
190
|
+
if File.directory?(config[:require])
|
191
|
+
require 'rails'
|
192
|
+
require File.expand_path("#{config[:require]}/config/environment.rb")
|
193
|
+
::Rails.application.eager_load!
|
194
|
+
config[:tag] = default_tag
|
195
|
+
else
|
196
|
+
require config[:require]
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def default_tag
|
201
|
+
dir = ::Rails.root
|
202
|
+
name = File.basename(dir)
|
203
|
+
# Capistrano release directory
|
204
|
+
if name.to_i != 0 && prevdir = File.dirname(dir)
|
205
|
+
if File.basename(prevdir) == 'releases'
|
206
|
+
return File.basename(File.dirname(prevdir))
|
207
|
+
end
|
208
|
+
end
|
209
|
+
name
|
210
|
+
end
|
211
|
+
|
212
|
+
def check_required_keys!
|
213
|
+
required_keys = [:redis_host, :client_name, :namespace, :listen_to]
|
214
|
+
return if required_keys.all? { |k| config[k] }
|
215
|
+
logger.info '======================================================================'
|
216
|
+
logger.info ' Tincan needs :redis_host, :client_name, :namespace, and :listen_to'
|
217
|
+
logger.info ' defined in config/tincan.yml.'
|
218
|
+
logger.info '======================================================================'
|
219
|
+
exit 1
|
220
|
+
end
|
221
|
+
|
222
|
+
def validate!
|
223
|
+
return if File.exist?(config[:require])
|
224
|
+
return if File.directory?(config[:require]) &&
|
225
|
+
File.exist?("#{config[:require]}/config/application.rb")
|
226
|
+
logger.info '=================================================='
|
227
|
+
logger.info ' Please point tincan to a Rails 3/4 application'
|
228
|
+
logger.info ' to load your receiver with -r [DIR|FILE].'
|
229
|
+
logger.info '=================================================='
|
230
|
+
exit 1
|
231
|
+
end
|
232
|
+
|
233
|
+
def parse_options(argv)
|
234
|
+
opts = {}
|
235
|
+
|
236
|
+
@parser = OptionParser.new do |o|
|
237
|
+
o.on '-d', '--daemon', 'Daemonize process' do |arg|
|
238
|
+
opts[:daemon] = arg
|
239
|
+
end
|
240
|
+
|
241
|
+
o.on '-e', '--environment ENV', 'Application environment' do |arg|
|
242
|
+
opts[:environment] = arg
|
243
|
+
end
|
244
|
+
|
245
|
+
o.on '-r', '--require [DIR]', 'Location of Rails application' do |arg|
|
246
|
+
opts[:require] = arg
|
247
|
+
end
|
248
|
+
|
249
|
+
o.on '-t', '--timeout NUM', 'Shutdown timeout' do |arg|
|
250
|
+
opts[:timeout] = Integer(arg)
|
251
|
+
end
|
252
|
+
|
253
|
+
o.on '-v', '--verbose', 'Print more verbose output' do |arg|
|
254
|
+
opts[:verbose] = arg
|
255
|
+
end
|
256
|
+
|
257
|
+
o.on '-L', '--logfile PATH', 'path to writable logfile' do |arg|
|
258
|
+
opts[:logfile] = arg
|
259
|
+
end
|
260
|
+
|
261
|
+
o.on '-P', '--pidfile PATH', 'path to pidfile' do |arg|
|
262
|
+
opts[:pidfile] = arg
|
263
|
+
end
|
264
|
+
|
265
|
+
o.on '-V', '--version', 'Print version and exit' do |_|
|
266
|
+
puts "Tincan #{Tincan::VERSION}"
|
267
|
+
exit 0
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
@parser.banner = 'tincan [options]'
|
272
|
+
@parser.on_tail '-h', '--help', 'Show help' do
|
273
|
+
logger.info @parser
|
274
|
+
exit 1
|
275
|
+
end
|
276
|
+
@parser.parse!(argv)
|
277
|
+
opts[:require] ||= '.'
|
278
|
+
if File.exist?('config/tincan.yml')
|
279
|
+
opts[:config_file] ||= 'config/tincan.yml'
|
280
|
+
end
|
281
|
+
opts[:logfile] = 'log/tincan.log' if opts[:daemon] && !opts[:logfile]
|
282
|
+
opts
|
283
|
+
end
|
284
|
+
|
285
|
+
def initialize_logger
|
286
|
+
@logger = ::Logger.new(config[:logfile] || STDOUT)
|
287
|
+
@logger.level = ::Logger::DEBUG if config[:verbose]
|
288
|
+
end
|
289
|
+
|
290
|
+
def write_pid
|
291
|
+
path = config[:pidfile] || 'tmp/pids/tincan.pid'
|
292
|
+
pidfile = File.expand_path(path)
|
293
|
+
File.open(pidfile, 'w') { |f| f.puts ::Process.pid }
|
294
|
+
end
|
295
|
+
|
296
|
+
def parse_config(cfile)
|
297
|
+
opts = {}
|
298
|
+
if File.exist?(cfile)
|
299
|
+
opts = YAML.load(ERB.new(IO.read(cfile)).result)
|
300
|
+
opts = opts.with_indifferent_access[environment]
|
301
|
+
end
|
302
|
+
opts
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|