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 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