fastly_nsq 1.7.0 → 1.8.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: 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