fastly_nsq 0.9.5 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4cee7eb576e0bfa95f3d894c52e99e15585a3832
4
- data.tar.gz: 4c84b8b5b2ab8f59d88599cb7e06acdf64ed3873
3
+ metadata.gz: e00ef3c8f9fbbd967b2e97e2621a63734cbf953b
4
+ data.tar.gz: f5d6e19dbcd964481c2543b9a772ccde35c6b22f
5
5
  SHA512:
6
- metadata.gz: f00684ae35a919fb7b9c8cf0e08d6590aff4a5a36e101cbc8c3c36dff58b3e042e3ca5c1eb84b41a5fd41cb5a53e9c0e459ad4ce5470431d89819b05e2f8ca2b
7
- data.tar.gz: 6ea4abebaf60a445f714060eaf27f97d8fd2f9b937ac25cf5b704171bd8aa53b29ce227dafc22a0c7720d07270e72fe8d27a958628c53da7d31f7a148fc8ca53
6
+ metadata.gz: 304ccf3b5012900f32aae93993656b5f6d5866d325b91267848b64d4c2c321e78ee40cbe812e418761f4c597827153598c5d874e7bfe0d8059cfd4ab2a4a3f6c
7
+ data.tar.gz: 7aac209a0fdb90f650bd0287393a367eb16585de140bf2768635f0a0bcb39f98d6600eb5d96c4ad66ff6da5ce4d68a1236c6f20f27cde2e0889c9545706ba672
data/.gitignore CHANGED
@@ -1,4 +1,5 @@
1
1
  /.bundle
2
+ /.env.local
2
3
  /Gemfile.lock
3
4
  /html/
4
5
  /pkg/
data/README.md CHANGED
@@ -159,6 +159,20 @@ The task can also define a `call`-able "preprocessor" (called before any `Proces
159
159
  See the [`Rakefile`](examples/Rakefile) file
160
160
  for more detail.
161
161
 
162
+ ### `FastlyNsq::CLI`
163
+
164
+ To help facilitate running the `FastlyNsq::Listener` in a blocking fashion
165
+ outside your application, a `CLI` and bin script [`fastly_nsq`](bin/fastly_nsq)
166
+ are provided.
167
+
168
+ This can be setup ahead of time by calling `FastlyNsq.configure` and passing
169
+ block. An exmaple of this can be found here: [`Example Config`](exmaple_config_class.rb)
170
+
171
+ An example of using the cli:
172
+ ```bash
173
+ ./bin/fastly_nsq -r ./example_config_class.rb -L ./test.log -P ./fastly_nsq.pid -v -d
174
+ ```
175
+
162
176
  ### FastlyNsq::Messgener
163
177
 
164
178
  Wrapper around a producer for sending messages and persisting producer objects.
data/bin/fastly_nsq ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'pry'
4
+ require 'fastly_nsq/cli'
5
+
6
+ begin
7
+ cli = FastlyNsq::CLI.instance
8
+ cli.parse_options
9
+ cli.run
10
+ rescue => e
11
+ raise e if $DEBUG
12
+ STDERR.puts e.message
13
+ STDERR.puts e.backtrace.join("\n")
14
+ exit 1
15
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+ class MessageProcessor
3
+ def self.process(message)
4
+ FastlyNsq.logger.info("IN PROCESS: #{message}")
5
+
6
+ # Do Soemthing with message
7
+ end
8
+ end
9
+
10
+ FastlyNsq.configure do |config|
11
+ config.channel = 'my_channel'
12
+ config.logger = Logger.new
13
+ config.preprocessor = ->(_) { FastlyNsq.logger.info 'PREPROCESSESES' }
14
+
15
+ config.listener_config do |lc|
16
+ lc.add_topic('some_topic', MessageProcessor)
17
+ lc.add_topic('some_other_topic', MessageProcessor)
18
+ end
19
+ end
data/fastly_nsq.gemspec CHANGED
@@ -23,6 +23,7 @@ Gem::Specification.new do |gem|
23
23
  gem.add_development_dependency 'awesome_print', '~> 1.6'
24
24
  gem.add_development_dependency 'bundler', '~> 1.12'
25
25
  gem.add_development_dependency 'bundler-audit', '~> 0.5.0'
26
+ gem.add_development_dependency 'dotenv'
26
27
  gem.add_development_dependency 'overcommit', '~> 0.32.0'
27
28
  gem.add_development_dependency 'pry-byebug', '~> 3.3'
28
29
  gem.add_development_dependency 'rake', '~> 11.1.2'
data/lib/fastly_nsq.rb CHANGED
@@ -10,15 +10,51 @@ require 'fastly_nsq/tls_options'
10
10
  require 'fastly_nsq/version'
11
11
 
12
12
  module FastlyNsq
13
- def self.logger=(logger)
13
+ module_function
14
+
15
+ def channel=(channel)
16
+ @channel ||= channel
17
+ end
18
+
19
+ def logger=(logger)
14
20
  strategy.logger = logger
15
21
  end
16
22
 
17
- def self.logger
23
+ def self.preprocessor=(preprocessor)
24
+ @preprocessor ||= preprocessor
25
+ end
26
+
27
+ def channel
28
+ @channel
29
+ end
30
+
31
+ def logger
18
32
  strategy.logger
19
33
  end
20
34
 
21
- def self.strategy
35
+ def self.preprocessor
36
+ @preprocessor
37
+ end
38
+
39
+ def strategy
22
40
  Strategy.for_queue
23
41
  end
42
+
43
+ def configure
44
+ yield self
45
+ end
46
+
47
+ def topic_map
48
+ @listener_config.topic_map
49
+ end
50
+
51
+ def listener_config
52
+ @listener_config ||= FastlyNsq::Listener::Config.new
53
+ yield @listener_config if block_given?
54
+ @listener_config
55
+ end
56
+
57
+ def reset_config
58
+ @listener_config = nil
59
+ end
24
60
  end
@@ -0,0 +1,238 @@
1
+ # frozen_string_literal: true
2
+ $stdout.sync = true
3
+
4
+ require 'fastly_nsq'
5
+ require 'fastly_nsq/launcher'
6
+ require 'fastly_nsq/manager'
7
+ require 'fileutils'
8
+ require 'optparse'
9
+ require 'singleton'
10
+
11
+ class FastlyNsq::CLI
12
+ include Singleton
13
+
14
+ attr_reader :options
15
+
16
+ def parse_options(args = ARGV)
17
+ parse(args)
18
+ setup_logger
19
+ check_pid
20
+ daemonize if daemonize?
21
+ write_pid
22
+ end
23
+
24
+ def run
25
+ startup
26
+ begin
27
+ # Multithreading begins here ----
28
+ launcher.run
29
+
30
+ read_loop
31
+ rescue Interrupt
32
+ FastlyNsq.logger.info 'Shutting down'
33
+ launcher.stop
34
+ # Explicitly exit so busy Processor threads can't block
35
+ # process shutdown.
36
+ FastlyNsq.logger.info 'Bye!'
37
+ exit(0)
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def launcher
44
+ @launcher ||= FastlyNsq::Launcher.new(options)
45
+ end
46
+
47
+ def read_loop
48
+ trapped_read_io = trap_signals
49
+ loop do
50
+ readable_io = IO.select([trapped_read_io])
51
+ break unless readable_io
52
+ signal = readable_io.first[0].gets.strip
53
+ handle_signal signal
54
+ end
55
+ end
56
+
57
+ def startup
58
+ require options[:require] if options[:require]
59
+ FastlyNsq.logger.info "Running in #{RUBY_DESCRIPTION}"
60
+ FastlyNsq.logger.info 'Starting processing, hit Ctrl-C to stop' unless options[:daemon]
61
+ end
62
+
63
+ def parse(args)
64
+ opts = {}
65
+
66
+ @parser = OptionParser.new do |o|
67
+ o.on '-d', '--daemon', 'Daemonize process' do |arg|
68
+ opts[:daemonize] = arg
69
+ end
70
+
71
+ o.on '-L', '--logfile PATH', 'path to writable logfile' do |arg|
72
+ opts[:logfile] = arg
73
+ end
74
+
75
+ o.on '-P', '--pidfile PATH', 'path to pidfile' do |arg|
76
+ opts[:pidfile] = arg
77
+ end
78
+
79
+ o.on '-r', '--require [PATH|DIR]', 'Location of message_processor definition' do |arg|
80
+ opts[:require] = arg
81
+ end
82
+
83
+ o.on '-v', '--verbose', 'enable verbose logging output' do |arg|
84
+ opts[:verbose] = arg
85
+ end
86
+ end
87
+
88
+ @parser.banner = 'fastly_nsq [options]'
89
+ @parser.parse!(args)
90
+
91
+ @options = opts
92
+ end
93
+
94
+ def check_pid
95
+ if pidfile?
96
+ case pid_status(pidfile)
97
+ when :running, :not_owned
98
+ puts "A server is already running. Check #{pidfile}"
99
+ exit(1)
100
+ when :dead
101
+ File.delete(pidfile)
102
+ end
103
+ end
104
+ end
105
+
106
+ def pid_status(pidfile)
107
+ return :exited unless File.exist?(pidfile)
108
+ pid = ::File.read(pidfile).to_i
109
+ return :dead if pid == 0
110
+ Process.kill(0, pid) # check process status
111
+ :running
112
+ rescue Errno::ESRCH
113
+ :dead
114
+ rescue Errno::EPERM
115
+ :not_owned
116
+ end
117
+
118
+ def setup_logger
119
+ FastlyNsq.logger = Logger.new(options[:logfile]) if options[:logfile]
120
+
121
+ FastlyNsq.logger.level = ::Logger::DEBUG if options[:verbose]
122
+ end
123
+
124
+ def write_pid
125
+ if pidfile?
126
+ begin
127
+ File.open(pidfile, ::File::CREAT | ::File::EXCL | ::File::WRONLY) do |f|
128
+ f.write Process.pid.to_s
129
+ end
130
+ at_exit { File.delete(pidfile) if File.exist?(pidfile) }
131
+ rescue Errno::EEXIST
132
+ check_pid
133
+ retry
134
+ end
135
+ end
136
+ end
137
+
138
+ def trap_signals
139
+ self_read, self_write = IO.pipe
140
+ sigs = %w(INT TERM TTIN USR1)
141
+
142
+ sigs.each do |sig|
143
+ begin
144
+ trap sig do
145
+ self_write.puts(sig)
146
+ end
147
+ rescue ArgumentError
148
+ puts "Signal #{sig} not supported"
149
+ end
150
+ end
151
+ self_read
152
+ end
153
+
154
+ def handle_signal(sig)
155
+ FastlyNsq.logger.debug "Got #{sig} signal"
156
+ case sig
157
+ when 'INT'
158
+ # Handle Ctrl-C in JRuby like MRI
159
+ # http://jira.codehaus.org/browse/JRUBY-4637
160
+ raise Interrupt
161
+ when 'TERM'
162
+ # Heroku sends TERM and then waits 10 seconds for process to exit.
163
+ raise Interrupt
164
+ when 'USR1'
165
+ FastlyNsq.logger.info 'Received USR1, no longer accepting new work'
166
+ launcher.quiet
167
+ when 'TTIN'
168
+ handle_ttin
169
+ end
170
+ end
171
+
172
+ def handle_ttin
173
+ Thread.list.each do |thread|
174
+ FastlyNsq.logger.warn "Thread TID-#{thread.object_id.to_s(36)} #{thread['fastly_nsq_label']}"
175
+ if thread.backtrace
176
+ FastlyNsq.logger.warn thread.backtrace.join("\n")
177
+ else
178
+ FastlyNsq.logger.warn '<no backtrace available>'
179
+ end
180
+ end
181
+ end
182
+
183
+ def daemonize
184
+ return unless options[:daemonize]
185
+
186
+ files_to_reopen = []
187
+ ObjectSpace.each_object(File) do |file|
188
+ files_to_reopen << file unless file.closed?
189
+ end
190
+
191
+ ::Process.daemon(true, true)
192
+
193
+ reopen(files_to_reopen)
194
+
195
+ [$stdout, $stderr].each do |io|
196
+ File.open(options.fetch(:logfile, '/dev/null'), 'ab') do |f|
197
+ io.reopen(f)
198
+ end
199
+ io.sync = true
200
+ end
201
+ $stdin.reopen('/dev/null')
202
+
203
+ setup_logger
204
+ end
205
+
206
+ def reopen(files)
207
+ files.each do |file|
208
+ begin
209
+ file.reopen file.path, 'a+'
210
+ file.sync = true
211
+ rescue IOError => e
212
+ FastlyNsq.logger.warn "IOError reopening file: #{e.message}"
213
+ rescue StandardError => e
214
+ FastlyNsq.logger.error "Non IOError reopening file: #{e.message}"
215
+ end
216
+ end
217
+ end
218
+
219
+ def logfile?
220
+ !logfile.nil?
221
+ end
222
+
223
+ def pidfile?
224
+ !pidfile.nil?
225
+ end
226
+
227
+ def daemonize?
228
+ options[:daemonize]
229
+ end
230
+
231
+ def logfile
232
+ options[:logfile]
233
+ end
234
+
235
+ def pidfile
236
+ options[:pidfile]
237
+ end
238
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+ require 'fastly_nsq/safe_thread'
3
+
4
+ class FastlyNsq::Launcher
5
+ include FastlyNsq::SafeThread
6
+
7
+ def initialize(options)
8
+ @done = false
9
+ @manager = FastlyNsq::Manager.new options
10
+ @options = options
11
+ end
12
+
13
+ def run
14
+ @thread = safe_thread('heartbeat', &method(:start_heartbeat))
15
+ @manager.start
16
+ end
17
+
18
+ def quiet
19
+ @done = true
20
+ @manager.quiet
21
+ end
22
+
23
+ # Shuts down the process. This method does not
24
+ # return until all work is complete and cleaned up.
25
+ # It can take up to the timeout to complete.
26
+ def stop
27
+ deadline = Time.now + @options.fetch(:timeout, 10)
28
+ quiet
29
+ @manager.stop deadline
30
+ end
31
+
32
+ def stopping?
33
+ @done
34
+ end
35
+
36
+ private
37
+
38
+ def heartbeat
39
+ FastlyNsq.logger.debug do
40
+ [
41
+ 'HEARTBEAT:',
42
+ 'thread_status:', @manager.listeners.map(&:status).join(', '),
43
+ 'listener_count:', @manager.listeners.count
44
+ ].join(' ')
45
+ end
46
+
47
+ # TODO: Check the health of the system overall and kill it if needed
48
+ # ::Process.kill('dieing because...', $$)
49
+ rescue => e
50
+ FastlyNsq.logger.error "heartbeat error: #{e.message}"
51
+ end
52
+
53
+ def start_heartbeat
54
+ loop do
55
+ heartbeat
56
+ sleep 5
57
+ end
58
+ FastlyNsq.logger.info('Heartbeat stopping...')
59
+ end
60
+ end
@@ -1,59 +1,114 @@
1
+ # frozen_string_literal: true
1
2
  require 'fastly_nsq/message'
3
+ require 'fastly_nsq/manager'
4
+ require 'fastly_nsq/safe_thread'
5
+ require 'fastly_nsq/listener/config'
2
6
 
3
7
  module FastlyNsq
4
8
  class Listener
5
- def self.listen_to(**args)
6
- new(**args).go
9
+ include FastlyNsq::SafeThread
10
+
11
+ def self.listen_to(*args)
12
+ new(*args).go
7
13
  end
8
14
 
9
15
  def initialize(topic:, processor:, channel: nil, consumer: nil, **options)
10
- @topic = topic
11
- @processor = processor
12
16
  @consumer = consumer || FastlyNsq::Consumer.new(topic: topic, channel: channel)
17
+ @done = false
13
18
  @logger = options.fetch :logger, FastlyNsq.logger
19
+ @manager = options[:manager] || FastlyNsq::Manager.new
14
20
  @preprocessor = options[:preprocessor]
21
+ @processor = processor
22
+ @thread = nil
23
+ @topic = topic
15
24
  end
16
25
 
17
- def go(run_once: false)
18
- exit_on 'INT'
19
- exit_on 'TERM'
26
+ def identity
27
+ {
28
+ consumer: @consumer,
29
+ logger: @logger,
30
+ manager: @manager,
31
+ preprocessor: @preprocessor,
32
+ processor: @processor,
33
+ topic: @topic,
34
+ }
35
+ end
36
+
37
+ def reset_then_dup
38
+ reset
39
+ dup
40
+ end
20
41
 
21
- loop do
42
+ def start
43
+ @logger.info { "> Listener Started: topic #{@topic}" }
44
+ @thread ||= safe_thread('listener', &method(:go))
45
+ end
46
+
47
+ def go(run_once: false)
48
+ until @done
22
49
  next_message do |message|
23
50
  log message
24
51
  preprocess message
25
- processor.process message
52
+ @processor.process message
26
53
  end
27
54
 
28
- break if run_once
55
+ terminate if run_once
29
56
  end
30
57
 
31
- consumer.terminate
58
+ @manager.listener_stopped(self)
59
+ rescue FastlyNsq::Shutdown
60
+ @manager.listener_stopped(self)
61
+ rescue Exception => ex # rubocop:disable Lint/RescueException
62
+ @logger.error ex.inspect
63
+ @manager.listener_killed(self)
64
+ ensure
65
+ cleanup
32
66
  end
33
67
 
34
- private
68
+ def status
69
+ @thread.status if @thread
70
+ end
71
+
72
+ def terminate
73
+ @done = true
74
+ return unless @thread
75
+ @logger.info "< Listener TERM: topic #{@topic}"
76
+ end
35
77
 
36
- attr_reader :topic, :consumer, :preprocessor, :processor, :logger
78
+ def kill
79
+ @done = true
80
+ return unless @thread
81
+ @logger.info "< Listener KILL: topic #{@topic}"
82
+ @thread.raise FastlyNsq::Shutdown
83
+ end
84
+
85
+ private
37
86
 
38
87
  def log(message)
39
- logger.info "[NSQ] Message Received: #{message}" if logger
88
+ @logger.info "[NSQ] Message Received: #{message}" if @logger
89
+ end
90
+
91
+ def cleanup
92
+ @consumer.terminate
93
+ @logger.info '< Consumer terminated'
40
94
  end
41
95
 
42
96
  def next_message
43
- message = consumer.pop # TODO: consumer.pop do |message|
97
+ message = @consumer.pop # TODO: consumer.pop do |message|
44
98
  result = yield FastlyNsq::Message.new(message.body)
45
99
  message.finish if result
46
100
  end
47
101
 
48
102
  def preprocess(message)
49
- preprocessor.call(message) if preprocessor
103
+ @preprocessor.call(message) if @preprocessor
50
104
  end
51
105
 
52
- def exit_on(signal)
53
- Signal.trap(signal) do
54
- consumer.terminate
55
- exit
56
- end
106
+ def reset
107
+ @done = false
108
+ @thread = nil
109
+ self
57
110
  end
58
111
  end
59
112
  end
113
+
114
+ class FastlyNsq::Shutdown < StandardError; end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+ module FastlyNsq
3
+ class Listener
4
+ class Config
5
+ attr_reader :topic_map
6
+
7
+ def initialize
8
+ @topic_map = {}
9
+ end
10
+
11
+ def add_topic(topic_name, processor)
12
+ FastlyNsq.logger.info("topic: #{topic_name} : klass #{processor}")
13
+ validate topic_name, processor
14
+ topic_map[topic_name] = processor
15
+ end
16
+
17
+ private
18
+
19
+ def validate(topic_name, processor)
20
+ unless processor.respond_to? :process
21
+ error_msg = "ConfigurationError: processor: #{processor} for #{topic_name} does not respond to :process!"
22
+ FastlyNsq.logger.error error_msg
23
+ raise ::ConfigurationError, error_msg
24
+ end
25
+
26
+ if topic_map[topic_name]
27
+ FastlyNsq.logger.warn("topic: #{topic_name} was added more than once")
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ class ConfigurationError < StandardError; end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+ require 'set'
3
+
4
+ class FastlyNsq::Manager
5
+ attr_reader :listeners
6
+
7
+ def initialize(options = {})
8
+ @options = options
9
+ @done = false
10
+ @listeners = Set.new
11
+ @plock = Mutex.new
12
+ end
13
+
14
+ def start
15
+ setup_configured_listeners
16
+ @listeners.each(&:start)
17
+ end
18
+
19
+ def quiet
20
+ return if @done
21
+ @done = true
22
+
23
+ FastlyNsq.logger.info { 'Terminating quiet listeners' }
24
+ @listeners.each(&:terminate)
25
+ end
26
+
27
+ PAUSE_TIME = 0.5
28
+
29
+ def stop(deadline)
30
+ quiet
31
+
32
+ sleep PAUSE_TIME
33
+ return if @listeners.empty?
34
+
35
+ FastlyNsq.logger.info { 'Pausing to allow workers to finish...' }
36
+ remaining = deadline - Time.now
37
+ while remaining > PAUSE_TIME
38
+ return if @listeners.empty?
39
+ sleep PAUSE_TIME
40
+ remaining = deadline - Time.now
41
+ end
42
+ return if @listeners.empty?
43
+
44
+ hard_shutdown
45
+ end
46
+
47
+ def stopped?
48
+ @done
49
+ end
50
+
51
+ def listener_stopped(listener)
52
+ @plock.synchronize do
53
+ @listeners.delete listener
54
+ end
55
+ end
56
+
57
+ def listener_killed(listener)
58
+ @plock.synchronize do
59
+ @listeners.delete listener
60
+ unless @done
61
+ FastlyNsq.logger.info { "recreating listener for: #{listener.identity}" }
62
+ new_listener = listener.reset_then_dup
63
+ @listeners << new_listener
64
+ new_listener.start
65
+ end
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def setup_configured_listeners
72
+ FastlyNsq.logger.debug { "options #{@options.inspect}" }
73
+ FastlyNsq.logger.debug { "starting listeners: #{FastlyNsq.topic_map.inspect}" }
74
+
75
+ FastlyNsq.topic_map.each_pair do |topic, processor|
76
+ @listeners << setup_listener(topic, processor)
77
+ end
78
+ end
79
+
80
+ def setup_listener(topic, processor)
81
+ FastlyNsq.logger.info { "Listening to topic:'#{topic}' on channel: '#{FastlyNsq.channel}'" }
82
+ FastlyNsq::Listener.new(
83
+ {
84
+ topic: topic,
85
+ channel: FastlyNsq.channel,
86
+ processor: processor,
87
+ preprocessor: FastlyNsq.preprocessor,
88
+ manager: self,
89
+ },
90
+ )
91
+ end
92
+
93
+ def hard_shutdown
94
+ cleanup = nil
95
+ @plock.synchronize do
96
+ cleanup = @listeners.dup
97
+ end
98
+
99
+ unless cleanup.empty?
100
+ FastlyNsq.logger.warn { "Terminating #{cleanup.size} busy worker threads" }
101
+ end
102
+
103
+ cleanup.each(&:kill)
104
+ end
105
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+ module FastlyNsq::SafeThread
3
+ def safe_thread(name, &block)
4
+ Thread.new do
5
+ Thread.current['fastly_nsq_label'] = name
6
+ watchdog(name, &block)
7
+ end
8
+ end
9
+
10
+ def watchdog(last_words)
11
+ yield
12
+ rescue => ex
13
+ FastlyNsq.logger.error ex
14
+ FastlyNsq.logger.error last_words
15
+ FastlyNsq.logger.error ex.backtrace.join("\n") unless ex.backtrace.nil?
16
+ raise ex
17
+ end
18
+ end
@@ -1,3 +1,3 @@
1
1
  module FastlyNsq
2
- VERSION = '0.9.5'.freeze
2
+ VERSION = '0.10.0'.freeze
3
3
  end
@@ -0,0 +1,5 @@
1
+ require 'spec_helper'
2
+ require 'fastly_nsq/cli'
3
+
4
+ RSpec.describe FastlyNsq::CLI do
5
+ end
@@ -0,0 +1,56 @@
1
+ require 'spec_helper'
2
+ require 'fastly_nsq/launcher'
3
+
4
+ RSpec.describe FastlyNsq::Launcher do
5
+ let(:launcher) { FastlyNsq::Launcher.new options }
6
+ let(:manager) { instance_double 'Manager', start: nil, quiet: nil, stop: nil }
7
+ let(:thread) { instance_double 'Thread' }
8
+ let(:options) { { joe: 'biden' } }
9
+
10
+ before do
11
+ allow(FastlyNsq::Manager).to receive(:new).and_return(manager)
12
+ allow(launcher).to receive(:safe_thread).and_return(thread)
13
+ end
14
+
15
+ it 'creates a manager with correct options' do
16
+ expect(FastlyNsq::Manager).to have_received(:new).with(options)
17
+ end
18
+
19
+ describe '#run' do
20
+ it 'creates a heartbeat thread' do
21
+ launcher.run
22
+ expect(launcher).to have_received(:safe_thread).with('heartbeat')
23
+ expect(launcher.stopping?).to eq false
24
+ end
25
+
26
+ it 'starts the setup manager' do
27
+ launcher.run
28
+ expect(manager).to have_received(:start)
29
+ expect(launcher.stopping?).to eq false
30
+ end
31
+ end
32
+
33
+ describe '#quiet' do
34
+ it 'quites the manager and sets done' do
35
+ expect(launcher.stopping?).to eq false
36
+ launcher.quiet
37
+ expect(manager).to have_received(:quiet)
38
+ expect(launcher.stopping?).to eq true
39
+ end
40
+ end
41
+
42
+ describe '#stop' do
43
+ it 'stops the manager within a deadline' do
44
+ now = Time.now
45
+ allow(Time).to receive(:now).and_return(now)
46
+ launcher.stop
47
+ expect(manager).to have_received(:stop).with(now + 10)
48
+ end
49
+
50
+ it 'quites the manager' do
51
+ launcher.stop
52
+ expect(manager).to have_received(:quiet)
53
+ expect(launcher.stopping?).to eq true
54
+ end
55
+ end
56
+ end
@@ -4,7 +4,7 @@ RSpec.describe FastlyNsq::Listener do
4
4
  let(:topic) { 'testing_topic' }
5
5
  let(:channel) { 'testing_channel' }
6
6
  let(:consumer) { FastlyNsq::FakeBackend::Consumer.new topic: topic, channel: channel }
7
- let(:logger) { double 'Logger', info: nil }
7
+ let(:logger) { double 'Logger', info: nil, debug: nil, error: nil }
8
8
 
9
9
  module TestMessageProcessor
10
10
  @@messages_processed = []
@@ -36,7 +36,7 @@ RSpec.describe FastlyNsq::Listener do
36
36
 
37
37
  let(:message) { TestMessageProcessor::Message.new 'this is message body' }
38
38
  let(:messages_processed) { TestMessageProcessor.messages_processed }
39
- let(:expected_message) { TestMessageProcessor::Message.new('this is message body') }
39
+ let(:expected_message) { TestMessageProcessor::Message.new 'this is message body' }
40
40
  let(:expected_messages) { [expected_message] }
41
41
 
42
42
  describe 'instantiating without a consumer' do
@@ -53,6 +53,12 @@ RSpec.describe FastlyNsq::Listener do
53
53
  end
54
54
  end
55
55
 
56
+ context 'when not passed a manager' do
57
+ it 'creates a blank manager' do
58
+ expect(listener.identity[:manager]).to_not be_nil
59
+ end
60
+ end
61
+
56
62
  context 'when using the fake queue and it is empty', fake_queue: true do
57
63
  before do
58
64
  TestMessageProcessor.clear
@@ -112,5 +118,74 @@ RSpec.describe FastlyNsq::Listener do
112
118
  expect(preprocessor_was_called).to be_truthy
113
119
  end
114
120
  end
121
+
122
+ context 'when running as a thread' do
123
+ let(:manager) { double 'Manager', listener_stopped: nil, listener_killed: nil }
124
+ let(:thread) { double 'FakeThread', raise: nil, kill: nil, status: 'fake_thread' }
125
+ let(:listener) do
126
+ FastlyNsq::Listener.new topic: topic,
127
+ processor: TestMessageProcessor,
128
+ logger: logger,
129
+ consumer: consumer,
130
+ manager: manager
131
+ end
132
+
133
+ describe 'shutdown' do
134
+ it 'informs the manager of a shutdown when run once' do
135
+ listener.go run_once: true
136
+
137
+ expect(manager).to have_received(:listener_stopped).with(listener)
138
+ end
139
+ end
140
+
141
+ before do
142
+ allow(listener).to receive(:safe_thread).and_return(thread)
143
+ end
144
+
145
+ it 'starts and provide status' do
146
+ listener.start
147
+ expect(logger).to have_received(:info)
148
+ expect(listener.status).to eq 'fake_thread'
149
+ end
150
+
151
+ it 'can describe itself' do
152
+ id = listener.identity
153
+ expect(id[:consumer]).to_not be_nil
154
+ expect(id[:logger]).to be logger
155
+ expect(id[:manager]).to be manager
156
+ expect(id[:preprocessor]).to be_nil
157
+ expect(id[:processor]).to_not be_nil
158
+ expect(id[:topic]).to_not be_nil
159
+ end
160
+
161
+ it 'can be cleanly duplicated' do
162
+ new_listener = listener.reset_then_dup
163
+
164
+ expect(listener.identity).to eq new_listener.identity
165
+ end
166
+
167
+ it 'can be terminated' do
168
+ listener.start
169
+ state = listener.instance_variable_get(:@done)
170
+ expect(state).to eq false
171
+ listener.terminate
172
+
173
+ state = listener.instance_variable_get(:@done)
174
+ expect(logger).to have_received(:info).twice
175
+ expect(state).to eq true
176
+ end
177
+
178
+ it 'can be killed' do
179
+ listener.start
180
+ state = listener.instance_variable_get(:@done)
181
+ expect(state).to eq false
182
+ listener.kill
183
+
184
+ state = listener.instance_variable_get(:@done)
185
+ expect(logger).to have_received(:info).twice
186
+ expect(thread).to have_received(:raise).with(FastlyNsq::Shutdown)
187
+ expect(state).to eq true
188
+ end
189
+ end
115
190
  end
116
191
  end
@@ -0,0 +1,129 @@
1
+ require 'spec_helper'
2
+ require 'fastly_nsq/manager'
3
+
4
+ RSpec.describe FastlyNsq::Manager do
5
+ class TestProcessor
6
+ def self.process(message)
7
+ FastlyNsq.logger.info("IN PROCESS: #{message}")
8
+ end
9
+ end
10
+
11
+ let(:listener_1) { instance_double 'Listener1', new: nil, start: nil, terminate: nil, kill: nil, reset_then_dup: listener_dup }
12
+ let(:listener_2) { instance_double 'Listener2', new: nil, start: nil, terminate: nil, kill: nil, reset_then_dup: listener_dup }
13
+ let(:listener_dup) { instance_double 'ListenerDup', start: nil }
14
+ let(:manager) { FastlyNsq::Manager.new options }
15
+ let(:options) { { joe: 'biden' } }
16
+
17
+ let(:configed_topics) do
18
+ [
19
+ { topic: 'warm_topic', klass: TestProcessor },
20
+ { topic: 'cool_topic', klass: TestProcessor },
21
+ ]
22
+ end
23
+
24
+ before do
25
+ logger = Logger.new(STDOUT)
26
+ logger.level = Logger::FATAL
27
+
28
+ FastlyNsq.configure do |config|
29
+ config.channel = 'william'
30
+ config.logger = logger
31
+ config.listener_config do |lc|
32
+ configed_topics.each do |t|
33
+ lc.add_topic t[:topic], t[:klass]
34
+ end
35
+ end
36
+ end
37
+
38
+ allow(FastlyNsq::Listener).to receive(:new).and_return(listener_1, listener_2)
39
+ manager.start
40
+ end
41
+
42
+ after do
43
+ FastlyNsq.reset_config
44
+ end
45
+
46
+ describe '#start' do
47
+ it 'sets up each configured listener' do
48
+ configed_topics.each do |t|
49
+ expect(FastlyNsq::Listener).to have_received(:new).with(
50
+ {
51
+ channel: FastlyNsq.channel,
52
+ manager: manager,
53
+ preprocessor: FastlyNsq.preprocessor,
54
+ processor: t[:klass],
55
+ topic: t[:topic],
56
+ },
57
+ )
58
+ end
59
+ end
60
+
61
+ it 'starts all listeners' do
62
+ expect(listener_1).to have_received(:start)
63
+ expect(listener_2).to have_received(:start)
64
+ end
65
+
66
+ it 'populates @listeners with all created listeners' do
67
+ expect(manager.listeners).to eq Set.new([listener_1, listener_2])
68
+ end
69
+ end
70
+
71
+ describe '#quiet' do
72
+ it 'does nothing if stopping' do
73
+ manager.instance_variable_set(:@done, true)
74
+ manager.quiet
75
+ expect(listener_1).to_not have_received(:terminate)
76
+ expect(listener_2).to_not have_received(:terminate)
77
+ end
78
+
79
+ it 'terminates all listeners' do
80
+ manager.quiet
81
+ expect(listener_1).to have_received(:terminate)
82
+ expect(listener_2).to have_received(:terminate)
83
+ end
84
+ end
85
+
86
+ describe '#stop' do
87
+ it 'does nothing if no listeners exist post quiet' do
88
+ allow(manager).to receive(:quiet)
89
+ allow(manager).to receive(:hard_shutdown)
90
+ manager.instance_variable_set(:@listeners, Set.new)
91
+
92
+ manager.stop(Time.now)
93
+
94
+ expect(manager).to have_received(:quiet)
95
+ expect(manager).to_not have_received(:hard_shutdown)
96
+ end
97
+
98
+ it 'forces shutdown if listeners remain after deadline' do
99
+ allow(manager).to receive(:hard_shutdown).and_call_original
100
+
101
+ manager.stop(Time.now)
102
+
103
+ expect(manager).to have_received(:hard_shutdown)
104
+ expect(listener_1).to have_received(:kill)
105
+ expect(listener_2).to have_received(:kill)
106
+ end
107
+ end
108
+
109
+ describe '#listener_stopped' do
110
+ it 'removes listeners from set of listeners' do
111
+ manager.listener_stopped(listener_1)
112
+ expect(manager.listeners).to eq Set.new([listener_2])
113
+ end
114
+ end
115
+
116
+ describe '#listener_killed' do
117
+ it 'removes listeners from set of listeners' do
118
+ manager.quiet # mark for stopping
119
+ manager.listener_killed(listener_1)
120
+ expect(manager.listeners).to eq Set.new([listener_2])
121
+ end
122
+
123
+ it 'creates and starts replacements if not stopping' do
124
+ manager.listener_killed(listener_1)
125
+ expect(manager.listeners).to eq Set.new([listener_dup, listener_2])
126
+ expect(listener_dup).to have_received(:start)
127
+ end
128
+ end
129
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fastly_nsq
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.5
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tommy O'Neil
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2017-05-25 00:00:00.000000000 Z
14
+ date: 2017-06-09 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: awesome_print
@@ -55,6 +55,20 @@ dependencies:
55
55
  - - "~>"
56
56
  - !ruby/object:Gem::Version
57
57
  version: 0.5.0
58
+ - !ruby/object:Gem::Dependency
59
+ name: dotenv
60
+ requirement: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ type: :development
66
+ prerelease: false
67
+ version_requirements: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
58
72
  - !ruby/object:Gem::Dependency
59
73
  name: overcommit
60
74
  requirement: !ruby/object:Gem::Requirement
@@ -175,7 +189,8 @@ dependencies:
175
189
  version: 2.0.5
176
190
  description: Helper classes for Fastly's NSQ Services
177
191
  email: tommy@fastly.com
178
- executables: []
192
+ executables:
193
+ - fastly_nsq
179
194
  extensions: []
180
195
  extra_rdoc_files: []
181
196
  files:
@@ -192,25 +207,35 @@ files:
192
207
  - LICENSE.txt
193
208
  - README.md
194
209
  - Rakefile
210
+ - bin/fastly_nsq
195
211
  - env_configuration_for_local_gem_tests.yml
212
+ - example_config_class.rb
196
213
  - examples/.sample.env
197
214
  - examples/Rakefile
198
215
  - fastly_nsq.gemspec
199
216
  - lib/fastly_nsq.rb
217
+ - lib/fastly_nsq/cli.rb
200
218
  - lib/fastly_nsq/consumer.rb
201
219
  - lib/fastly_nsq/fake_backend.rb
220
+ - lib/fastly_nsq/launcher.rb
202
221
  - lib/fastly_nsq/listener.rb
222
+ - lib/fastly_nsq/listener/config.rb
223
+ - lib/fastly_nsq/manager.rb
203
224
  - lib/fastly_nsq/message.rb
204
225
  - lib/fastly_nsq/messenger.rb
205
226
  - lib/fastly_nsq/producer.rb
206
227
  - lib/fastly_nsq/rake_task.rb
228
+ - lib/fastly_nsq/safe_thread.rb
207
229
  - lib/fastly_nsq/strategy.rb
208
230
  - lib/fastly_nsq/tls_options.rb
209
231
  - lib/fastly_nsq/version.rb
232
+ - spec/lib/fastly_nsq/cli_spec.rb
210
233
  - spec/lib/fastly_nsq/consumer_spec.rb
211
234
  - spec/lib/fastly_nsq/fake_backend_spec.rb
212
235
  - spec/lib/fastly_nsq/fastly_nsq_spec.rb
236
+ - spec/lib/fastly_nsq/launcher_spec.rb
213
237
  - spec/lib/fastly_nsq/listener_spec.rb
238
+ - spec/lib/fastly_nsq/manager_spec.rb
214
239
  - spec/lib/fastly_nsq/message_spec.rb
215
240
  - spec/lib/fastly_nsq/messenger_spec.rb
216
241
  - spec/lib/fastly_nsq/producer_spec.rb