fastly_nsq 0.9.5 → 0.10.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 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