fastly_nsq 1.7.0 → 1.8.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: be99f97fe78325dd7d99285e0f7f63387ad20cc3
4
- data.tar.gz: a85de6cdf4520e2e83844f71f6feaab7d1f81865
3
+ metadata.gz: e9dbcfdeefbc796bb32482bd3ab9806b548284d4
4
+ data.tar.gz: 7d0257332788312bbbbad212e3060fe3ae6a79ea
5
5
  SHA512:
6
- metadata.gz: a200725047fd0b365e03041b4a7806c7bee7daa557b2f6919dcd5f562b43c982d5597257c62b698de46aba63b02752ef970b22ea47f342183b911015676e7a29
7
- data.tar.gz: 3ddcf60d8bc3c62d9af1726a05135296b131effa6f9319c8665d39b2d4b9d6d08977f9aecd00bbe618894f8b1c5ffeedd09826487f52430638e214aa23f7a55c
6
+ metadata.gz: d0e7305d476a86045538ffe42ebda32a6998edd64df5535487c2acec9a003a2d5b1bff4720cb3d13fa34d68fcb94750b1a1b82bd550086a3f08b51efaa9d8353
7
+ data.tar.gz: 0715abca959a7d08400aeb4f5f233c37c61912e172cccd0cab7792d0e4763193f53751716a346b240ea6ffb16cd133da307df0e72f7e97b43d23767401f5cd64
data/.gitignore CHANGED
@@ -5,3 +5,5 @@
5
5
  /pkg/
6
6
  /vendor/cache/*.gem
7
7
  spec/examples.txt
8
+ .yardoc
9
+ /doc
data/README.md CHANGED
@@ -24,6 +24,7 @@ Please use [GitHub Issues] to report bugs.
24
24
 
25
25
  [GitHub Issues]: https://github.com/fastly/fastly_nsq/issues
26
26
 
27
+ **[Documentation](https://www.rubydoc.info/gems/fastly_nsq)**
27
28
 
28
29
  ## Install
29
30
 
@@ -243,19 +244,33 @@ FastlyNsq::Http::Nsqlookupd.nodes
243
244
  FastlyNsq::Http::Nsqlookupd.lookup(topic: 'foo')
244
245
  ```
245
246
 
246
- ### `Real vs. Fake`
247
+ ### Testing
247
248
 
248
- The real strategy
249
- creates a connection
250
- to `nsq-ruby`'s
251
- `Nsq::Producer` and `Nsq::Consumer` classes.
249
+ `FastlyNsq` provides a test mode and a helper class to make testing easier.
252
250
 
253
- The fake strategy
254
- mocks the connection
255
- to NSQ for testing purposes.
256
- It adheres to the same API
257
- as the real adapter.
251
+ In order to test classes that use FastlyNsq without having real connections
252
+ to NSQ:
258
253
 
254
+ ```ruby
255
+ require 'fastly_nsq/testing'
256
+
257
+ RSpec.configure do |config|
258
+ config.before(:each) do
259
+ FastlyNsq::Testing.fake!
260
+ FastlyNsq::Testing.reset!
261
+ end
262
+ end
263
+ ```
264
+
265
+ To test processor classes you can create test messages:
266
+
267
+ ```ruby
268
+ test_message = FastlyNsq::Testing.message(data: { 'count' => 123 })
269
+
270
+ My::ProcessorKlass.call(test_message)
271
+
272
+ expect(some_result)
273
+ ```
259
274
 
260
275
  ## Configuration
261
276
 
data/fastly_nsq.gemspec CHANGED
@@ -28,6 +28,7 @@ Gem::Specification.new do |gem|
28
28
  gem.add_development_dependency 'rspec-eventually', '0.2'
29
29
  gem.add_development_dependency 'timecop'
30
30
  gem.add_development_dependency 'webmock', '~> 3.0'
31
+ gem.add_development_dependency 'yard'
31
32
 
32
33
  gem.add_dependency 'concurrent-ruby', '~> 1.0'
33
34
  gem.add_dependency 'nsq-ruby', '~> 2.3'
data/lib/fastly_nsq.rb CHANGED
@@ -12,12 +12,37 @@ module FastlyNsq
12
12
  NotConnectedError = Class.new(StandardError)
13
13
  ConnectionFailed = Class.new(StandardError)
14
14
 
15
+ LIFECYCLE_EVENTS = %i[startup shutdown heartbeat].freeze
16
+
15
17
  class << self
18
+ # @return [String] NSQ Channel
16
19
  attr_accessor :channel
20
+
21
+ # @return [Proc] global preprocessor
17
22
  attr_accessor :preprocessor
23
+
24
+ # Maximum number of times an NSQ message will be attempted.
25
+ # When set to +nil+, the number of attempts will be unlimited.
26
+ # @return [Integer]
18
27
  attr_accessor :max_attempts
28
+
29
+ # @return [Logger]
19
30
  attr_writer :logger
20
31
 
32
+ ##
33
+ # Map of lifecycle events
34
+ # @return [Hash]
35
+ def events
36
+ @events ||= LIFECYCLE_EVENTS.each_with_object({}) { |e, a| a[e] = [] }
37
+ end
38
+
39
+ ##
40
+ # Create a FastlyNsq::Listener
41
+ #
42
+ # @param topic [String] NSQ topic on which to listen
43
+ # @param processor [Proc] processor that will be +call+ed per message
44
+ # @param options [Hash] additional options that are passed to FastlyNsq::Listener's constructor
45
+ # @return FastlyNsq::Listener
21
46
  def listen(topic, processor, **options)
22
47
  FastlyNsq::Listener.new(topic: topic, processor: processor, **options)
23
48
  end
@@ -26,22 +51,73 @@ module FastlyNsq
26
51
  @logger ||= Logger.new(nil)
27
52
  end
28
53
 
54
+ ##
55
+ # Configuration for FastlyNsq
56
+ # @example
57
+ # FastlyNsq.configure do |config|
58
+ # config.channel = 'Z'
59
+ # config.logger = Logger.new
60
+ # end
61
+ # @example
62
+ # FastlyNsq.configure do |config|
63
+ # config.channel = 'fnsq'
64
+ # config.logger = Logger.new
65
+ # config.preprocessor = ->(_) { FastlyNsq.logger.info 'PREPROCESSESES' }
66
+ # lc.listen 'posts', ->(m) { puts "posts: #{m.body}" }
67
+ # lc.listen 'blogs', ->(m) { puts "blogs: #{m.body}" }, priority: 3
68
+ # end
29
69
  def configure
30
70
  yield self
31
71
  end
32
72
 
73
+ ##
74
+ # Returns a new FastlyNsq::Manager or the memoized
75
+ # instance +@manager+.
76
+ # @return [FastlyNsq::Manager]
33
77
  def manager
34
78
  @manager ||= FastlyNsq::Manager.new
35
79
  end
36
80
 
81
+ ##
82
+ # Set a new manager and transfer listeners to the new manager.
83
+ # @param manager [FastlyNsq::Manager]
84
+ # @return [FastlyNsq::Manager]
37
85
  def manager=(manager)
38
86
  @manager&.transfer(manager)
39
87
  @manager = manager
40
88
  end
41
89
 
90
+ ##
91
+ # Return an array of NSQ lookupd http addresses sourced from ENV['NSQLOOKUPD_HTTP_ADDRESS']
92
+ # @return [Array<String>] list of nsqlookupd http addresses
42
93
  def lookupd_http_addresses
43
94
  ENV.fetch('NSQLOOKUPD_HTTP_ADDRESS').split(',').map(&:strip)
44
95
  end
96
+
97
+ # Register a block to run at a point in the lifecycle.
98
+ # :startup, :heartbeat or :shutdown are valid events.
99
+ #
100
+ # FastlyNsq.configure do |config|
101
+ # config.on(:shutdown) do
102
+ # puts "Goodbye cruel world!"
103
+ # end
104
+ # end
105
+ def on(event, &block)
106
+ event = event.to_sym
107
+ raise ArgumentError, "Invalid event name: #{event}" unless LIFECYCLE_EVENTS.include?(event)
108
+ events[event] << block
109
+ end
110
+
111
+ def fire_event(event)
112
+ blocks = FastlyNsq.events.fetch(event)
113
+ blocks.each do |block|
114
+ begin
115
+ block.call
116
+ rescue => e
117
+ logger.error "[#{event}] #{e.inspect}"
118
+ end
119
+ end
120
+ end
45
121
  end
46
122
  end
47
123
 
@@ -10,6 +10,20 @@ require 'fileutils'
10
10
  require 'optparse'
11
11
  require 'singleton'
12
12
 
13
+ # Provides functionality to support running FastlyNsq as a command line
14
+ # application that listens and processes NSQ messages.
15
+ #
16
+ # @example
17
+ # begin
18
+ # cli = FastlyNsq::CLI.instance
19
+ # cli.parse_options
20
+ # cli.run
21
+ # rescue => e
22
+ # raise e if $DEBUG
23
+ # STDERR.puts e.message
24
+ # STDERR.puts e.backtrace.join("\n")
25
+ # exit 1
26
+ # end
13
27
  class FastlyNsq::CLI
14
28
  include Singleton
15
29
 
@@ -1,15 +1,82 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Provides an adapter to an Nsq::Consumer
4
+ # and used to read messages off the queue.
5
+ #
6
+ # @example
7
+ # consumer = FastlyNsq::Consumer.new(
8
+ # topic: 'topic',
9
+ # channel: 'channel'
10
+ # )
11
+ # consumer.size #=> 1
12
+ # message = consumer.pop
13
+ # message.body #=> "{ 'data': { 'key': 'value' } }"
14
+ # message.finish
15
+ # consumer.size #=> 0
16
+ # consumer.terminate
17
+
3
18
  class FastlyNsq::Consumer
4
19
  extend Forwardable
5
20
 
6
- DEFAULT_CONNECTION_TIMEOUT = 5 # seconds
21
+ # Default NSQ connection timeout in seconds
22
+ DEFAULT_CONNECTION_TIMEOUT = 5
23
+
24
+ # @return [String] NSQ Channel
25
+ attr_reader :channel
26
+
27
+ # @return [String] NSQ Topic
28
+ attr_reader :topic
29
+
30
+ # @return [Nsq::Consumer]
31
+ attr_reader :connection
32
+
33
+ # @return [Integer] connection timeout in seconds
34
+ attr_reader :connect_timeout
7
35
 
8
- attr_reader :channel, :topic, :connection, :connect_timeout
36
+ # @return [Integer] maximum number of times an NSQ message will be attempted
9
37
  attr_reader :max_attempts
10
38
 
11
- def_delegators :connection, :size, :terminate, :connected?, :pop, :pop_without_blocking
39
+ # @!method connected?
40
+ # Delegated to +self.connection+
41
+ # @return [Nsq::Consumer#connected?]
42
+ # @see https://www.rubydoc.info/gems/nsq-ruby/Nsq/ClientBase#connected%3F-instance_method Nsq::ClientBase#connected?
43
+ # @!method pop
44
+ # Delegated to +self.connection+
45
+ # @return [Nsq::Consumer#pop]
46
+ # @see https://www.rubydoc.info/gems/nsq-ruby/Nsq%2FConsumer:pop Nsq::Consumer#pop
47
+ # @!method pop_without_blocking
48
+ # Delegated to +self.connection+
49
+ # @return [Nsq::Consumer#pop_without_blocking]
50
+ # @see https://www.rubydoc.info/gems/nsq-ruby/Nsq%2FConsumer:pop_without_blocking Nsq::Consumer#pop_without_blocking
51
+ # @!method size
52
+ # Delegated to +self.connection+
53
+ # @return [Nsq::Consumer#size]
54
+ # @see https://www.rubydoc.info/gems/nsq-ruby/Nsq%2FConsumer:size Nsq::Consumer#size
55
+ # @!method terminate
56
+ # Delegated to +self.connection+
57
+ # @return [Nsq::Consumer#terminate]
58
+ # @see https://www.rubydoc.info/gems/nsq-ruby/Nsq%2FConsumer:terminate Nsq::Consumer#terminate
59
+ def_delegators :connection, :connected?, :pop, :pop_without_blocking, :size, :terminate
12
60
 
61
+ ##
62
+ # Create a FastlyNsq::Consumer
63
+ #
64
+ # @param topic [String] NSQ topic from which to consume
65
+ # @param channel [String] NSQ channel from which to consume
66
+ # @param queue [#pop, #size] Queue object, most likely an instance of {FastlyNsq::Feeder}
67
+ # @param tls_options [Hash] Hash of TSL options passed the connection.
68
+ # In most cases this should be nil unless you need to override the
69
+ # default values set in ENV.
70
+ # @param connect_timeout [Integer] NSQ connection timeout in seconds
71
+ # @param max_attempts [Integer] maximum number of times an NSQ message will be attemped
72
+ # When set to +nil+, attempts will be unlimited
73
+ # @param options [Hash] addtional options forwarded to the connection contructor
74
+ #
75
+ # @example
76
+ # consumer = FastlyNsq::Consumer.new(
77
+ # topic: 'topic',
78
+ # channel: 'channel'
79
+ # )
13
80
  def initialize(topic:, channel:, queue: nil, tls_options: nil, connect_timeout: DEFAULT_CONNECTION_TIMEOUT, max_attempts: FastlyNsq.max_attempts, **options)
14
81
  @topic = topic
15
82
  @channel = channel
@@ -20,6 +87,9 @@ class FastlyNsq::Consumer
20
87
  @connection = connect(queue, **options)
21
88
  end
22
89
 
90
+ ##
91
+ # Is the message queue empty?
92
+ # @return [Boolean]
23
93
  def empty?
24
94
  size.zero?
25
95
  end
@@ -1,15 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ ##
4
+ # FastlyNsq::Feeder is a queue interface wrapper for the manager's thread pool.
5
+ # This allows a consumer read loop to post a message directly to a
6
+ # processor (FastlyNsq::Listener) with a specified priority.
3
7
  class FastlyNsq::Feeder
4
8
  attr_reader :processor, :priority
5
9
 
10
+ ##
11
+ # Create a FastlyNsq::Feeder
12
+ # @param processor [FastlyNsq::Listener]
13
+ # @param priority [Numeric]
6
14
  def initialize(processor, priority)
7
15
  @processor = processor
8
16
  @priority = priority
9
17
  end
10
18
 
19
+ ##
20
+ # Send a message to the processor with specified priority
21
+ #
22
+ # This will +post+ to the FastlyNsq.manager.pool with a queue priority and block
23
+ # that will +call+ed. FastlyNsq.manager.pool is a PriorityThreadPool which is a
24
+ # Concurrent::ThreadPoolExecutor that has @queue which in turn is a priority queue
25
+ # that manages job priority
26
+ #
27
+ # The ThreadPoolExecutor is what actually works the @queue and sends +call+ to the queued Proc.
28
+ # When that code is exec'ed +processer.call(message)+ is run. Processor in this context is
29
+ # a FastlyNsq::Listener
30
+ #
31
+ # @param message [Nsq::Message]
11
32
  # @see http://ruby-concurrency.github.io/concurrent-ruby/Concurrent/ThreadPoolExecutor.html#post-instance_method
12
- # @see {Nsq::Connection#read_loop}
33
+ # @see Nsq::Connection#read_loop
13
34
  def push(message)
14
35
  FastlyNsq.manager.pool.post(priority) { processor.call(message) }
15
36
  end
@@ -4,6 +4,16 @@ require 'net/https'
4
4
  require 'fastly_nsq/http/nsqd'
5
5
  require 'fastly_nsq/http/nsqlookupd'
6
6
 
7
+ ##
8
+ # Adapter class for HTTP requests to NSQD
9
+ #
10
+ # @example
11
+ # uri = URI.join(nsqd_url, '/info')
12
+ # client = FastlyNsq::Http.new(uri: uri)
13
+ # client.use_ssl
14
+ #
15
+ # @see FastlyNsq::Http::Nsqd
16
+ # @see FastlyNsq::Http::Nsqlookupd
7
17
  class FastlyNsq::Http
8
18
  def initialize(uri:, cert_filename: ENV['NSQ_SSL_CERTIFICATE'], key_filename: ENV['NSQ_SSL_KEY'])
9
19
  @uri = uri.is_a?(URI) ? uri : URI.parse(uri)
@@ -3,6 +3,11 @@
3
3
  require 'fastly_nsq/http'
4
4
 
5
5
  class FastlyNsq::Http
6
+ ##
7
+ # Provides an interface to the the functionality provided by
8
+ # the nsqd HTTP interface
9
+ #
10
+ # @see https://nsq.io/components/nsqd.html
6
11
  class Nsqd
7
12
  extend Forwardable
8
13
  def_delegator :client, :get
@@ -3,6 +3,11 @@
3
3
  require 'fastly_nsq/http'
4
4
 
5
5
  class FastlyNsq::Http
6
+ ##
7
+ # Provides an interface to the functionality exposed by
8
+ # the nsqlookupd HTTP interface
9
+ #
10
+ # @see https://nsq.io/components/nsqlookupd.html
6
11
  class Nsqlookupd
7
12
  extend Forwardable
8
13
  def_delegator :client, :get
@@ -2,6 +2,11 @@
2
2
 
3
3
  require 'fastly_nsq/safe_thread'
4
4
 
5
+ ##
6
+ # FastlyNsq::Launcher is a lighweight wrapper of a thread manager
7
+ # and heartbeat.
8
+ #
9
+ # This class is used internally by FastlyNsq::CLI.
5
10
  class FastlyNsq::Launcher
6
11
  include FastlyNsq::SafeThread
7
12
 
@@ -19,6 +24,7 @@ class FastlyNsq::Launcher
19
24
  @logger = logger
20
25
 
21
26
  FastlyNsq.manager = FastlyNsq::Manager.new(options)
27
+ FastlyNsq.fire_event :startup
22
28
  end
23
29
 
24
30
  def beat
@@ -28,6 +34,7 @@ class FastlyNsq::Launcher
28
34
  def stop
29
35
  @done = true
30
36
  manager.terminate(timeout)
37
+ FastlyNsq.fire_event :shutdown
31
38
  end
32
39
 
33
40
  def stop_listeners
@@ -57,6 +64,8 @@ class FastlyNsq::Launcher
57
64
  # ::Process.kill('dieing because...', $$)
58
65
  rescue => e
59
66
  logger.error "Heartbeat error: #{e.message}"
67
+ ensure
68
+ FastlyNsq.fire_event :heartbeat
60
69
  end
61
70
 
62
71
  def start_heartbeat
@@ -1,16 +1,81 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ ##
4
+ # The main interface to setting up a thread that listens for and
5
+ # processes NSQ messages from a given topic/channel.
6
+ #
7
+ # @example
8
+ # FastlyNsq::Listener.new(
9
+ # topic: topic,
10
+ # channel: channel,
11
+ # processor: ->(m) { puts 'got: '+ m.body }
12
+ # )
3
13
  class FastlyNsq::Listener
4
14
  extend Forwardable
5
15
 
16
+ # Default queue priority used when setting up the consumer queue
6
17
  DEFAULT_PRIORITY = 0
7
- DEFAULT_CONNECTION_TIMEOUT = 5 # seconds
8
18
 
19
+ # Default NSQ connection timeout in seconds
20
+ DEFAULT_CONNECTION_TIMEOUT = 5
21
+
22
+ # @!method connected?
23
+ # Delegated to +self.consumer+
24
+ # @return [FastlyNsq::Consumer#connected?]
9
25
  def_delegators :consumer, :connected?
10
26
 
11
- attr_reader :preprocessor, :topic, :processor, :priority, :channel, :logger, :consumer
27
+ # @return [String] NSQ Channel
28
+ attr_reader :channel
29
+
30
+ # @return [FastlyNsq::Consumer]
31
+ attr_reader :consumer
32
+
33
+ # @return [Logger]
34
+ attr_reader :logger
35
+
36
+ # @return [Integer] maxium number of times an NSQ message will be attempted
12
37
  attr_reader :max_attempts
13
38
 
39
+ # @return [Proc]
40
+ attr_reader :preprocessor
41
+
42
+ # @return [String] NSQ Topic
43
+ attr_reader :topic
44
+
45
+ # @return [Integer] Queue priority
46
+ attr_reader :priority
47
+
48
+ # @return [Proc] processor that is called for each message
49
+ attr_reader :processor
50
+
51
+ ##
52
+ # Create a FastlyNsq::Listener
53
+ #
54
+ # @param topic [String] NSQ topic on which to listen
55
+ # @param processor [Proc#call] Any object that responds to +call+. Each message will
56
+ # be processed with +processor.call(FastlyNsq::Message.new(nsq_message))+.
57
+ # The processor should return +true+ to indicate that processing is complete
58
+ # and NSQ message can be finished. The processor is passed an instance of {FastlyNsq::Message}
59
+ # so the provided Proc can optionally manage the message state using methods provided by {FastlyNsq::Message}.
60
+ # @param preprocessor [Proc#call] Any object that responds to +call+. Similar to the processor
61
+ # each message it processes via +preprocessor.call(message)+. Default: {FastlyNsq.preprocessor}
62
+ # @param channel [String] NSQ Channel on which to listen. Default: {FastlyNsq.channel}
63
+ # @param consumer [FastlyNsq::Consumer] interface to read messages off the queue. If value is +nil+ the
64
+ # constructor will create a {FastlyNsq::Consumer} based on the provided parameters.
65
+ # @param logger [Logger] Default: {FastlyNsq.logger}
66
+ # @param priority [Integer] Queue piority. Default: {DEFAULT_PRIORITY}
67
+ # @param connect_timeout [Integer] NSQ connection timeout in seconds. Default: {DEFAULT_CONNECTION_TIMEOUT}
68
+ # @param max_attempts [Integer] maximum number of times an NSQ message will be attemped Default: {FastlyNsq.max_attempts}
69
+ # When set to +nil+, attempts will be unlimited
70
+ # @param consumer_options [Hash] additional options forwarded to the {FastlyNsq::Consumer}} contructor
71
+ #
72
+ # @example
73
+ # FastlyNsq::Listener.new(
74
+ # topic: topic,
75
+ # channel: channel,
76
+ # processor: MessageProcessor,
77
+ # max_attempts: 15,
78
+ # )
14
79
  def initialize(topic:, processor:, preprocessor: FastlyNsq.preprocessor, channel: FastlyNsq.channel, consumer: nil,
15
80
  logger: FastlyNsq.logger, priority: DEFAULT_PRIORITY, connect_timeout: DEFAULT_CONNECTION_TIMEOUT,
16
81
  max_attempts: FastlyNsq.max_attempts, **consumer_options)
@@ -36,6 +101,15 @@ class FastlyNsq::Listener
36
101
  FastlyNsq.manager.add_listener(self)
37
102
  end
38
103
 
104
+ ##
105
+ # Process an NSQ message.
106
+ #
107
+ # @see FastlyNsq::Feeder#push
108
+ #
109
+ # @param nsq_message [Nsq::Message]
110
+ # @see {https://www.rubydoc.info/gems/nsq-ruby/Nsq/Message}
111
+ #
112
+ # @return [FastlyNsq::Message]
39
113
  def call(nsq_message)
40
114
  message = FastlyNsq::Message.new(nsq_message)
41
115
 
@@ -57,6 +131,10 @@ class FastlyNsq::Listener
57
131
  message
58
132
  end
59
133
 
134
+ ##
135
+ # Close the NSQ Conneciton
136
+ #
137
+ # @see FastlyNsq::Consumer#terminate
60
138
  def terminate
61
139
  return unless connected?
62
140
  consumer.terminate
@@ -1,11 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ ##
4
+ # Interface for tracking listeners and managing the processing pool.
3
5
  class FastlyNsq::Manager
4
6
  DEADLINE = 30
5
7
  DEFAULT_POOL_SIZE = 5
6
8
 
7
- attr_reader :done, :pool, :logger
9
+ # @return [Boolean] Set true when all listeners are stopped
10
+ attr_reader :done
8
11
 
12
+ # @return [FastlyNsq::PriorityThreadPool]
13
+ attr_reader :pool
14
+
15
+ # @return [Logger]
16
+ attr_reader :logger
17
+
18
+ ##
19
+ # Create a FastlyNsq::Manager
20
+ #
21
+ # @param logger [Logger]
22
+ # @param pool_options [Hash] Options forwarded to {FastlyNsq::PriorityThreadPool} constructor.
9
23
  def initialize(logger: FastlyNsq.logger, **pool_options)
10
24
  @done = false
11
25
  @logger = logger
@@ -14,18 +28,31 @@ class FastlyNsq::Manager
14
28
  )
15
29
  end
16
30
 
31
+ ##
32
+ # Hash of listeners. Keys are topics, values are {FastlyNsq::Listener} instances.
33
+ # @return [Hash]
17
34
  def topic_listeners
18
35
  @topic_listeners ||= {}
19
36
  end
20
37
 
38
+ ##
39
+ # Array of listening topic names
40
+ # @return [Array]
21
41
  def topics
22
42
  topic_listeners.keys
23
43
  end
24
44
 
45
+ ##
46
+ # Set of {FastlyNsq::Listener} objects
47
+ # @return [Set]
25
48
  def listeners
26
49
  topic_listeners.values.to_set
27
50
  end
28
51
 
52
+ ##
53
+ # Stop the manager.
54
+ # Terminates the listeners and stops all processing in the pool.
55
+ # @param deadline [Integer] Number of seconds to wait for pool to stop processing
29
56
  def terminate(deadline = DEADLINE)
30
57
  return if done
31
58
 
@@ -38,10 +65,16 @@ class FastlyNsq::Manager
38
65
  @done = true
39
66
  end
40
67
 
68
+ ##
69
+ # Manager state
70
+ # @return [Boolean]
41
71
  def stopped?
42
72
  done
43
73
  end
44
74
 
75
+ ##
76
+ # Add a {FastlyNsq::Listener} to the @topic_listeners
77
+ # @param listener [FastlyNsq::Listener}
45
78
  def add_listener(listener)
46
79
  logger.info { "topic #{listener.topic}, channel #{listener.channel}: listening" }
47
80
 
@@ -52,6 +85,10 @@ class FastlyNsq::Manager
52
85
  topic_listeners[listener.topic] = listener
53
86
  end
54
87
 
88
+ ##
89
+ # Transer listeners to a new manager and stop processing from the existing pool.
90
+ # @param new_manager [FastlyNsq::Manager] new manager to which listeners will be added
91
+ # @param deadline [Integer] Number of seconds to wait for exsiting pool to stop processing
55
92
  def transfer(new_manager, deadline: DEADLINE)
56
93
  new_manager.topic_listeners.merge!(topic_listeners)
57
94
  stop_processing(deadline)
@@ -59,6 +96,8 @@ class FastlyNsq::Manager
59
96
  @done = true
60
97
  end
61
98
 
99
+ ##
100
+ # Terminate all listeners
62
101
  def stop_listeners
63
102
  logger.info { 'Stopping listeners' }
64
103
  listeners.each(&:terminate)
@@ -67,6 +106,9 @@ class FastlyNsq::Manager
67
106
 
68
107
  protected
69
108
 
109
+ ##
110
+ # Shutdown the pool
111
+ # @param deadline [Integer] Number of seconds to wait for pool to stop processing
70
112
  def stop_processing(deadline)
71
113
  logger.info { 'Stopping processors' }
72
114
  pool.shutdown
@@ -2,14 +2,40 @@
2
2
 
3
3
  require 'json'
4
4
 
5
+ ##
6
+ # Adapter to Nsq::Message. Provides convenience methods for interacting
7
+ # with a message. Delegates management methods to the Nsq::Message
5
8
  class FastlyNsq::Message
6
9
  extend Forwardable
7
10
 
11
+ # @!method attempts
12
+ # Delegated to +self.nsq_message+
13
+ # @return [Nsq::Message#attempts]
14
+ # @see https://www.rubydoc.info/gems/nsq-ruby/Nsq/Message#attempts-instance_method
15
+ # @!method touch
16
+ # Delegated to +self.nsq_message+
17
+ # @return [Nsq::Message#touch]
18
+ # @see https://www.rubydoc.info/gems/nsq-ruby/Nsq/Message#touch-instance_method
19
+ # @!method timestamp
20
+ # Delegated to +self.nsq_message+
21
+ # @return [Nsq::Message#timestamp]
22
+ # @see https://www.rubydoc.info/gems/nsq-ruby/Nsq/Message#timestamp-instance_method
8
23
  def_delegators :@nsq_message, :attempts, :touch, :timestamp
9
24
 
10
- attr_reader :managed, :nsq_message, :raw_body
25
+ # @return [Symbol] Message state. Returns +nil+ if message has not been requeued or finished.
26
+ attr_reader :managed
27
+
28
+ # @return [Nsq::Message]
29
+ # @see https://www.rubydoc.info/gems/nsq-ruby/Nsq/Message
30
+ attr_reader :nsq_message
31
+
32
+ # @return [String] Nsq::Message body
33
+ attr_reader :raw_body
34
+
11
35
  alias to_s raw_body
12
36
 
37
+ ##
38
+ # @param nsq_message [Nsq::Message]
13
39
  def initialize(nsq_message)
14
40
  @nsq_message = nsq_message
15
41
  @raw_body = nsq_message.body
@@ -27,6 +53,8 @@ class FastlyNsq::Message
27
53
  @body ||= JSON.parse(raw_body)
28
54
  end
29
55
 
56
+ ##
57
+ # Finish an NSQ message
30
58
  def finish
31
59
  return managed if managed
32
60
 
@@ -34,6 +62,9 @@ class FastlyNsq::Message
34
62
  nsq_message.finish
35
63
  end
36
64
 
65
+ ##
66
+ # Requeue an NSQ Message
67
+ # @param timeout [Integer] timeout in milliseconds
37
68
  def requeue(timeout = nil)
38
69
  return managed if managed
39
70
  timeout ||= requeue_period
@@ -1,11 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ ##
4
+ # Provides interface for writing messages to NSQ.
5
+ # Manages tracking and creation of {FastlyNsq::Producer}s
6
+ # @example
7
+ # FastlyNsq::Messenger.deliver(
8
+ # message: message,
9
+ # topic: 'topic',
10
+ # meta: metadata_hash,
11
+ # )
3
12
  module FastlyNsq::Messenger
4
13
  DEFAULT_ORIGIN = 'Unknown'
5
14
  @originating_service = DEFAULT_ORIGIN
6
15
 
7
16
  module_function
8
17
 
18
+ ##
19
+ # Deliver an NSQ message
20
+ # @param message [#to_s] written to the +data+ key of the NSQ message payload
21
+ # @param topic [String] NSQ topic on which to deliver the message
22
+ # @param originating_service [String] added to meta key of message payload
23
+ # @param meta [Hash]
24
+ # @return [Void]
9
25
  def deliver(message:, topic:, originating_service: nil, meta: {})
10
26
  meta[:originating_service] = originating_service || self.originating_service
11
27
  meta[:sent_at] = Time.now.iso8601(5)
@@ -22,6 +38,9 @@ module FastlyNsq::Messenger
22
38
  @originating_service = service
23
39
  end
24
40
 
41
+ ##
42
+ # @param topic [String] NSQ topic
43
+ # @return [FastlyNsq::Producer] returns producer for given topic
25
44
  def producer_for(topic:)
26
45
  producer = producers[topic]
27
46
 
@@ -30,10 +49,16 @@ module FastlyNsq::Messenger
30
49
  producer
31
50
  end
32
51
 
52
+ ##
53
+ # Map of subscribed topics to FastlyNsq::Producer
54
+ # @return [Hash]
33
55
  def producers
34
56
  @producers ||= Hash.new { |hash, topic| hash[topic] = FastlyNsq::Producer.new(topic: topic) }
35
57
  end
36
58
 
59
+ ##
60
+ # Terminate producer for given topic
61
+ # @param topic [String] NSQ topic
37
62
  def terminate_producer(topic:)
38
63
  producer_for(topic: topic).terminate
39
64
  producers.delete(topic)
@@ -1,6 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FastlyNsq
4
+ ##
5
+ # Interface for testing FastlyNsq
6
+ # @example
7
+ # require 'fastly_nsq/testing'
8
+ # FastlyNsq::Testing.enabled? #=> true
9
+ # FastlyNsq::Testing.disabled? #=> false
10
+
11
+ # producer = FastlyNsq::Producer.new(topic: topic)
12
+ # listener = FastlyNsq::Listener.new(topic: topic, channel: channel, processor: ->(m) { puts 'got: '+ m.body })
13
+
14
+ # FastlyNsq::Testing.fake! # default, messages accumulate on the listeners
15
+
16
+ # producer.write '{"foo":"bar"}'
17
+ # listener.messages.size #=> 1
18
+
19
+ # FastlyNsq::Testing.reset! # remove all accumulated messages
20
+
21
+ # listener.messages.size #=> 0
22
+
23
+ # producer.write '{"foo":"bar"}'
24
+ # listener.messages.size #=> 1
25
+
26
+ # listener.drain
27
+ # # got: {"foo"=>"bar"}
28
+ # listener.messages.size #=> 0
29
+
30
+ # FastlyNsq::Testing.inline! # messages are processed as they are produced
31
+ # producer.write '{"foo":"bar"}'
32
+ # # got: {"foo"=>"bar"}
33
+ # listener.messages.size #=> 0
34
+
35
+ # FastlyNsq::Testing.disable! # do it live
36
+ # FastlyNsq::Testing.enable! # re-enable testing mode
4
37
  class Testing
5
38
  class << self
6
39
  attr_accessor :__test_mode
@@ -52,6 +85,18 @@ module FastlyNsq
52
85
  FastlyNsq::Messages.messages.clear
53
86
  end
54
87
 
88
+ ##
89
+ # Creates a FastlyNsq::TestMessage that is used to create a FastlyNsq::Message where the underlying
90
+ # +nsq_message+ is the TestMessage and not an Nsq::Message. This aids in testing application code that
91
+ # is doing message processing
92
+ #
93
+ # @param data [String] NSQ message data
94
+ # @param meta [String] NSQ message metadata
95
+ #
96
+ # @example
97
+ # test_message = FastlyNsq::Testing.message(data: post_data, meta: {})
98
+ # processor_klass.call(test_message)
99
+ # expect(Post.find(post_data['id']).not_to be nil
55
100
  def message(data:, meta: nil)
56
101
  test_message = FastlyNsq::TestMessage.new(JSON.dump('data' => data, 'meta' => meta))
57
102
  FastlyNsq::Message.new(test_message)
@@ -65,6 +110,10 @@ module FastlyNsq
65
110
  end
66
111
  end
67
112
 
113
+ ##
114
+ # Stub for Nsq::Message used for testing.
115
+ # Use this class instead of a struct or test stubs
116
+ # when testing application logic that requires a Nsq::Message.
68
117
  class TestMessage
69
118
  attr_reader :raw_body
70
119
  attr_reader :attempts
@@ -2,6 +2,9 @@
2
2
 
3
3
  module FastlyNsq
4
4
  class TlsOptions
5
+ ##
6
+ # Return hash of TLS options for creating an NSQ connection.
7
+ # @param context [Hash]
5
8
  def self.as_hash(context = nil)
6
9
  new(context).to_h
7
10
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FastlyNsq
4
- VERSION = '1.7.0'
4
+ VERSION = '1.8.0'
5
5
  end
@@ -69,4 +69,20 @@ RSpec.describe FastlyNsq do
69
69
  expect(subject.lookupd_http_addresses).to eq(ENV['NSQLOOKUPD_HTTP_ADDRESS'].split(','))
70
70
  end
71
71
  end
72
+
73
+ describe '#on' do
74
+ before { FastlyNsq.events.each { |(_, v)| v.clear } }
75
+ after { FastlyNsq.events.each { |(_, v)| v.clear } }
76
+
77
+ it 'registers callbacks for events' do
78
+ %i[startup shutdown heartbeat].each do |event|
79
+ block = -> {}
80
+ expect { FastlyNsq.on(event, &block) }.to change { FastlyNsq.events[event] }.by([block])
81
+ end
82
+ end
83
+
84
+ it 'limits callback registration to valid events' do
85
+ expect { FastlyNsq.on(:foo, &-> {}) }.to raise_error(ArgumentError, /Invalid event name/)
86
+ end
87
+ end
72
88
  end
@@ -3,19 +3,21 @@
3
3
  require 'spec_helper'
4
4
 
5
5
  RSpec.describe FastlyNsq::Launcher do
6
+ let!(:channel) { 'fnsq' }
6
7
  let!(:options) { { max_threads: 3, timeout: 9 } }
7
- let!(:launcher) { FastlyNsq::Launcher.new options }
8
8
  let!(:topic) { 'fnsq' }
9
- let!(:channel) { 'fnsq' }
9
+
10
+ let(:launcher) { FastlyNsq::Launcher.new options }
10
11
  let(:listener) { FastlyNsq::Listener.new(topic: topic, channel: channel, processor: ->(*) {}) }
12
+ let(:manager) { launcher.manager }
11
13
 
12
14
  before { reset_topic(topic, channel: channel) }
13
15
  before { expect { listener }.to eventually(be_connected).within(5) }
14
16
  after { listener.terminate if listener.connected? }
15
17
 
16
- let(:manager) { launcher.manager }
17
-
18
18
  it 'creates a manager with correct options' do
19
+ launcher
20
+
19
21
  expect(FastlyNsq.manager.pool.max_threads).to eq(3)
20
22
  end
21
23
 
@@ -30,6 +32,8 @@ RSpec.describe FastlyNsq::Launcher do
30
32
  end
31
33
 
32
34
  describe '#stop_listeners' do
35
+ before { launcher }
36
+
33
37
  it 'stops listeners and sets done' do
34
38
  expect(launcher).not_to be_stopping
35
39
  expect(manager).to receive(:stop_listeners)
@@ -42,9 +46,45 @@ RSpec.describe FastlyNsq::Launcher do
42
46
  end
43
47
 
44
48
  describe '#stop' do
49
+ before { launcher }
50
+
45
51
  it 'stops the manager within a deadline' do
46
52
  expect(manager).to receive(:terminate).with(options[:timeout])
47
53
  launcher.stop
48
54
  end
49
55
  end
56
+
57
+ describe 'callbacks' do
58
+ before { FastlyNsq.events.each { |(_, v)| v.clear } }
59
+ after { FastlyNsq.events.each { |(_, v)| v.clear } }
60
+
61
+ it 'fires :startup event on initialization' do
62
+ obj = spy
63
+ block = -> { obj.start }
64
+ FastlyNsq.on(:startup, &block)
65
+
66
+ launcher
67
+ expect(obj).to have_received(:start)
68
+ end
69
+
70
+ it 'fires :shutdown event on #stop' do
71
+ launcher
72
+
73
+ obj = spy
74
+ block = -> { obj.stop }
75
+ FastlyNsq.on(:shutdown, &block)
76
+
77
+ launcher.stop
78
+ expect(obj).to have_received(:stop)
79
+ end
80
+
81
+ it 'fires :heartbeat event on #heartbeat' do
82
+ obj = spy
83
+ block = -> { obj.beat }
84
+ FastlyNsq.on(:heartbeat, &block)
85
+ launcher.beat
86
+
87
+ expect { obj }.to eventually(have_received(:beat).at_least(:once)).within(0.5)
88
+ end
89
+ end
50
90
  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: 1.7.0
4
+ version: 1.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tommy O'Neil
@@ -13,7 +13,7 @@ authors:
13
13
  autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
- date: 2018-04-26 00:00:00.000000000 Z
16
+ date: 2018-05-30 00:00:00.000000000 Z
17
17
  dependencies:
18
18
  - !ruby/object:Gem::Dependency
19
19
  name: awesome_print
@@ -127,6 +127,20 @@ dependencies:
127
127
  - - "~>"
128
128
  - !ruby/object:Gem::Version
129
129
  version: '3.0'
130
+ - !ruby/object:Gem::Dependency
131
+ name: yard
132
+ requirement: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ type: :development
138
+ prerelease: false
139
+ version_requirements: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - ">="
142
+ - !ruby/object:Gem::Version
143
+ version: '0'
130
144
  - !ruby/object:Gem::Dependency
131
145
  name: concurrent-ruby
132
146
  requirement: !ruby/object:Gem::Requirement
@@ -254,7 +268,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
254
268
  version: '0'
255
269
  requirements: []
256
270
  rubyforge_project:
257
- rubygems_version: 2.5.1
271
+ rubygems_version: 2.6.14
258
272
  signing_key:
259
273
  specification_version: 4
260
274
  summary: Fastly NSQ Adapter