captainu-tincan 0.7.0
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 +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
|