captainu-tincan 0.7.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: 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
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/.rubocop.yml ADDED
@@ -0,0 +1,8 @@
1
+ AssignmentInCondition:
2
+ Enabled: false
3
+ MethodLength:
4
+ Max: 20
5
+ RescueException:
6
+ Enabled: false
7
+ NumericLiterals:
8
+ Enabled: false
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
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in Tincan.gemspec
4
+ gemspec
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
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/tincan/cli'
4
+
5
+ begin
6
+ cli = Tincan::CLI.instance
7
+ cli.parse
8
+ cli.run
9
+ rescue => e
10
+ raise e if $DEBUG
11
+ STDERR.puts e.message
12
+ STDERR.puts e.backtrace.join("\n")
13
+ exit 1
14
+ end
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
@@ -0,0 +1,8 @@
1
+ Dir[File.join(File.dirname(__FILE__), 'tincan', '*.rb')].each do |file|
2
+ require file
3
+ end
4
+
5
+ # Provides an easy way to register senders and receivers on a reliable Redis
6
+ # message queue.
7
+ module Tincan
8
+ end
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