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 +4 -4
- data/.gitignore +1 -0
- data/README.md +14 -0
- data/bin/fastly_nsq +15 -0
- data/example_config_class.rb +19 -0
- data/fastly_nsq.gemspec +1 -0
- data/lib/fastly_nsq.rb +39 -3
- data/lib/fastly_nsq/cli.rb +238 -0
- data/lib/fastly_nsq/launcher.rb +60 -0
- data/lib/fastly_nsq/listener.rb +76 -21
- data/lib/fastly_nsq/listener/config.rb +34 -0
- data/lib/fastly_nsq/manager.rb +105 -0
- data/lib/fastly_nsq/safe_thread.rb +18 -0
- data/lib/fastly_nsq/version.rb +1 -1
- data/spec/lib/fastly_nsq/cli_spec.rb +5 -0
- data/spec/lib/fastly_nsq/launcher_spec.rb +56 -0
- data/spec/lib/fastly_nsq/listener_spec.rb +77 -2
- data/spec/lib/fastly_nsq/manager_spec.rb +129 -0
- metadata +28 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e00ef3c8f9fbbd967b2e97e2621a63734cbf953b
|
4
|
+
data.tar.gz: f5d6e19dbcd964481c2543b9a772ccde35c6b22f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 304ccf3b5012900f32aae93993656b5f6d5866d325b91267848b64d4c2c321e78ee40cbe812e418761f4c597827153598c5d874e7bfe0d8059cfd4ab2a4a3f6c
|
7
|
+
data.tar.gz: 7aac209a0fdb90f650bd0287393a367eb16585de140bf2768635f0a0bcb39f98d6600eb5d96c4ad66ff6da5ce4d68a1236c6f20f27cde2e0889c9545706ba672
|
data/.gitignore
CHANGED
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
|
-
|
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.
|
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.
|
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
|
data/lib/fastly_nsq/listener.rb
CHANGED
@@ -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
|
-
|
6
|
-
|
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
|
18
|
-
|
19
|
-
|
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
|
-
|
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
|
-
|
55
|
+
terminate if run_once
|
29
56
|
end
|
30
57
|
|
31
|
-
|
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
|
-
|
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
|
-
|
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
|
53
|
-
|
54
|
-
|
55
|
-
|
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
|
data/lib/fastly_nsq/version.rb
CHANGED
@@ -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
|
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.
|
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-
|
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
|