fastly_nsq 0.13.2 → 1.0.2

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.
Files changed (68) hide show
  1. checksums.yaml +5 -5
  2. data/.env +4 -0
  3. data/.overcommit.yml +3 -3
  4. data/.rubocop.yml +11 -1
  5. data/.travis.yml +8 -1
  6. data/Gemfile +9 -0
  7. data/README.md +52 -82
  8. data/Rakefile +2 -0
  9. data/bin/fastly_nsq +1 -0
  10. data/docker-compose.yml +23 -0
  11. data/examples/.sample.env +0 -3
  12. data/fastly_nsq.gemspec +7 -8
  13. data/lib/fastly_nsq.rb +44 -50
  14. data/lib/fastly_nsq/cli.rb +20 -14
  15. data/lib/fastly_nsq/consumer.rb +26 -30
  16. data/lib/fastly_nsq/feeder.rb +16 -0
  17. data/lib/fastly_nsq/http/nsqd.rb +7 -1
  18. data/lib/fastly_nsq/http/nsqlookupd.rb +1 -1
  19. data/lib/fastly_nsq/launcher.rb +31 -23
  20. data/lib/fastly_nsq/listener.rb +34 -103
  21. data/lib/fastly_nsq/manager.rb +48 -72
  22. data/lib/fastly_nsq/message.rb +2 -0
  23. data/lib/fastly_nsq/messenger.rb +5 -5
  24. data/lib/fastly_nsq/priority_queue.rb +12 -0
  25. data/lib/fastly_nsq/priority_thread_pool.rb +32 -0
  26. data/lib/fastly_nsq/producer.rb +52 -32
  27. data/lib/fastly_nsq/testing.rb +239 -0
  28. data/lib/fastly_nsq/tls_options.rb +2 -0
  29. data/lib/fastly_nsq/version.rb +3 -1
  30. data/spec/{lib/fastly_nsq/cli_spec.rb → cli_spec.rb} +2 -0
  31. data/spec/consumer_spec.rb +59 -0
  32. data/spec/fastly_nsq_spec.rb +72 -0
  33. data/spec/feeder_spec.rb +22 -0
  34. data/spec/{lib/fastly_nsq/http → http}/nsqd_spec.rb +1 -1
  35. data/spec/{lib/fastly_nsq/http → http}/nsqlookupd_spec.rb +1 -1
  36. data/spec/{lib/fastly_nsq/http_spec.rb → http_spec.rb} +3 -1
  37. data/spec/integration_spec.rb +48 -0
  38. data/spec/launcher_spec.rb +50 -0
  39. data/spec/listener_spec.rb +184 -0
  40. data/spec/manager_spec.rb +111 -0
  41. data/spec/matchers/delegate.rb +32 -0
  42. data/spec/{lib/fastly_nsq/message_spec.rb → message_spec.rb} +2 -0
  43. data/spec/{lib/fastly_nsq/messenger_spec.rb → messenger_spec.rb} +7 -5
  44. data/spec/priority_thread_pool_spec.rb +19 -0
  45. data/spec/producer_spec.rb +94 -0
  46. data/spec/spec_helper.rb +32 -28
  47. data/spec/support/http.rb +37 -0
  48. data/spec/support/webmock.rb +22 -0
  49. data/spec/{lib/fastly_nsq/tls_options_spec.rb → tls_options_spec.rb} +2 -0
  50. metadata +54 -96
  51. data/env_configuration_for_local_gem_tests.yml +0 -5
  52. data/example_config_class.rb +0 -20
  53. data/examples/Rakefile +0 -41
  54. data/lib/fastly_nsq/fake_backend.rb +0 -114
  55. data/lib/fastly_nsq/listener/config.rb +0 -35
  56. data/lib/fastly_nsq/rake_task.rb +0 -78
  57. data/lib/fastly_nsq/strategy.rb +0 -36
  58. data/spec/lib/fastly_nsq/consumer_spec.rb +0 -72
  59. data/spec/lib/fastly_nsq/fake_backend_spec.rb +0 -135
  60. data/spec/lib/fastly_nsq/fastly_nsq_spec.rb +0 -10
  61. data/spec/lib/fastly_nsq/launcher_spec.rb +0 -56
  62. data/spec/lib/fastly_nsq/listener_spec.rb +0 -213
  63. data/spec/lib/fastly_nsq/manager_spec.rb +0 -127
  64. data/spec/lib/fastly_nsq/producer_spec.rb +0 -60
  65. data/spec/lib/fastly_nsq/rake_task_spec.rb +0 -142
  66. data/spec/lib/fastly_nsq/strategy_spec.rb +0 -36
  67. data/spec/lib/fastly_nsq_spec.rb +0 -18
  68. data/spec/support/env_helpers.rb +0 -15
@@ -25,19 +25,17 @@ class FastlyNsq::CLI
25
25
 
26
26
  def run
27
27
  startup
28
- begin
29
- # Multithreading begins here ----
30
- launcher.run
31
-
32
- read_loop
33
- rescue Interrupt
34
- FastlyNsq.logger.info 'Shutting down'
35
- launcher.stop
36
- # Explicitly exit so busy Processor threads can't block
37
- # process shutdown.
38
- FastlyNsq.logger.info 'Bye!'
39
- exit(0)
40
- end
28
+
29
+ launcher.beat
30
+
31
+ read_loop
32
+ rescue Interrupt
33
+ FastlyNsq.logger.info 'Shutting down'
34
+ launcher.stop
35
+ # Explicitly exit so busy Processor threads can't block
36
+ # process shutdown.
37
+ FastlyNsq.logger.info 'Bye!'
38
+ exit(0)
41
39
  end
42
40
 
43
41
  private
@@ -75,6 +73,10 @@ class FastlyNsq::CLI
75
73
  opts = {}
76
74
 
77
75
  @parser = OptionParser.new do |o|
76
+ o.on '-c', '--concurrency COUNT', 'Number of threads used to process messages' do |arg|
77
+ opts[:max_threads] = arg
78
+ end
79
+
78
80
  o.on '-d', '--daemon', 'Daemonize process' do |arg|
79
81
  opts[:daemonize] = arg
80
82
  end
@@ -178,7 +180,7 @@ class FastlyNsq::CLI
178
180
  raise Interrupt
179
181
  when 'USR1'
180
182
  FastlyNsq.logger.info 'Received USR1, no longer accepting new work'
181
- launcher.quiet
183
+ launcher.stop_listeners
182
184
  when 'TTIN'
183
185
  handle_ttin
184
186
  end
@@ -247,6 +249,10 @@ class FastlyNsq::CLI
247
249
  options[:logfile]
248
250
  end
249
251
 
252
+ def max_threads
253
+ options[:max_threads]
254
+ end
255
+
250
256
  def pidfile
251
257
  options[:pidfile]
252
258
  end
@@ -1,43 +1,39 @@
1
- require 'forwardable'
1
+ # frozen_string_literal: true
2
2
 
3
- module FastlyNsq
4
- class Consumer
5
- extend Forwardable
6
- def_delegator :connection, :pop
7
- def_delegator :connection, :pop_without_blocking
8
- def_delegator :connection, :size
9
- def_delegator :connection, :terminate
3
+ class FastlyNsq::Consumer
4
+ extend Forwardable
10
5
 
11
- def initialize(topic:, channel:, tls_options: nil, connector: nil)
12
- @topic = topic
13
- @channel = channel
14
- @tls_options = TlsOptions.as_hash(tls_options)
15
- @connector = connector
16
- connection
17
- end
6
+ DEFAULT_CONNECTION_TIMEOUT = 5 # seconds
18
7
 
19
- def empty?
20
- connection.size.zero?
21
- end
8
+ attr_reader :channel, :topic, :connection, :connect_timeout
22
9
 
23
- private
10
+ def_delegators :connection, :size, :terminate, :connected?, :pop, :pop_without_blocking
24
11
 
25
- attr_reader :channel, :topic, :tls_options
12
+ def initialize(topic:, channel:, queue: nil, tls_options: nil, connect_timeout: DEFAULT_CONNECTION_TIMEOUT)
13
+ @topic = topic
14
+ @channel = channel
15
+ @tls_options = FastlyNsq::TlsOptions.as_hash(tls_options)
16
+ @connect_timeout = connect_timeout
26
17
 
27
- def connection
28
- @connection ||= connector.new(params)
29
- end
18
+ @connection = connect(queue)
19
+ end
20
+
21
+ def empty?
22
+ size.zero?
23
+ end
24
+
25
+ private
30
26
 
31
- def connector
32
- @connector || FastlyNsq.strategy::Consumer
33
- end
27
+ attr_reader :tls_options
34
28
 
35
- def params
29
+ def connect(queue)
30
+ Nsq::Consumer.new(
36
31
  {
37
- nsqlookupd: ENV.fetch('NSQLOOKUPD_HTTP_ADDRESS').split(',').map(&:strip),
32
+ nsqlookupd: FastlyNsq.lookupd_http_addresses,
38
33
  topic: topic,
39
34
  channel: channel,
40
- }.merge(tls_options)
41
- end
35
+ queue: queue,
36
+ }.merge(tls_options),
37
+ )
42
38
  end
43
39
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ class FastlyNsq::Feeder
4
+ attr_reader :processor, :priority
5
+
6
+ def initialize(processor, priority)
7
+ @processor = processor
8
+ @priority = priority
9
+ end
10
+
11
+ # @see http://ruby-concurrency.github.io/concurrent-ruby/Concurrent/ThreadPoolExecutor.html#post-instance_method
12
+ # @see {Nsq::Connection#read_loop}
13
+ def push(message)
14
+ FastlyNsq.manager.pool.post(priority) { processor.call(message) }
15
+ end
16
+ end
@@ -8,7 +8,13 @@ class FastlyNsq::Http
8
8
  def_delegator :client, :get
9
9
  def_delegator :client, :post
10
10
 
11
- BASE_NSQD_URL = ENV.fetch 'NSQD_URL', "https://#{ENV.fetch('NSQD_HTTPS_ADDRESS', '')}"
11
+ BASE_NSQD_URL = ENV.fetch('NSQD_URL') do
12
+ if ENV['NSQD_HTTPS_ADDRESS']
13
+ "https://#{ENV.fetch('NSQD_HTTPS_ADDRESS')}"
14
+ else
15
+ "http://#{ENV.fetch('NSQD_HTTP_ADDRESS')}"
16
+ end
17
+ end
12
18
  VALID_FORMATS = %w[text json].freeze
13
19
 
14
20
  ##
@@ -7,7 +7,7 @@ class FastlyNsq::Http
7
7
  extend Forwardable
8
8
  def_delegator :client, :get
9
9
 
10
- BASE_NSQLOOKUPD_URL = "http://#{ENV.fetch('NSQLOOKUPD_HTTP_ADDRESS', '').split(',')[0]}".freeze
10
+ BASE_NSQLOOKUPD_URL = "http://#{ENV.fetch('NSQLOOKUPD_HTTP_ADDRESS', '').split(',')[0]}"
11
11
 
12
12
  ##
13
13
  # List of producers for a given topic
@@ -5,29 +5,34 @@ require 'fastly_nsq/safe_thread'
5
5
  class FastlyNsq::Launcher
6
6
  include FastlyNsq::SafeThread
7
7
 
8
- def initialize(options)
9
- @done = false
10
- @manager = FastlyNsq::Manager.new options
11
- @options = options
8
+ attr_reader :timeout, :logger
9
+ attr_accessor :pulse
10
+
11
+ def manager
12
+ FastlyNsq.manager
12
13
  end
13
14
 
14
- def run
15
- @thread = safe_thread('heartbeat', &method(:start_heartbeat))
16
- @manager.start
15
+ def initialize(timeout: 5, pulse: 5, logger: FastlyNsq.logger, **options)
16
+ @done = false
17
+ @timeout = timeout
18
+ @pulse = pulse
19
+ @logger = logger
20
+
21
+ FastlyNsq.manager = FastlyNsq::Manager.new(options)
17
22
  end
18
23
 
19
- def quiet
20
- @done = true
21
- @manager.quiet
24
+ def beat
25
+ @heartbeat ||= safe_thread('heartbeat', &method(:start_heartbeat))
22
26
  end
23
27
 
24
- # Shuts down the process. This method does not
25
- # return until all work is complete and cleaned up.
26
- # It can take up to the timeout to complete.
27
28
  def stop
28
- deadline = Time.now + @options.fetch(:timeout, 5)
29
- quiet
30
- @manager.stop deadline
29
+ @done = true
30
+ manager.terminate(timeout)
31
+ end
32
+
33
+ def stop_listeners
34
+ @done = true
35
+ manager.stop_listeners
31
36
  end
32
37
 
33
38
  def stopping?
@@ -37,25 +42,28 @@ class FastlyNsq::Launcher
37
42
  private
38
43
 
39
44
  def heartbeat
40
- FastlyNsq.logger.debug do
45
+ logger.debug do
41
46
  [
42
47
  'HEARTBEAT:',
43
- 'thread_status:', @manager.listeners.map(&:status).join(', '),
44
- 'listener_count:', @manager.listeners.count
48
+ 'busy:', manager.pool.length,
49
+ 'processed:', manager.pool.completed_task_count,
50
+ 'max_threads:', manager.pool.max_length,
51
+ 'max_queue_size:', manager.pool.largest_length,
52
+ 'listeners:', manager.listeners.count
45
53
  ].join(' ')
46
54
  end
47
55
 
48
56
  # TODO: Check the health of the system overall and kill it if needed
49
57
  # ::Process.kill('dieing because...', $$)
50
58
  rescue => e
51
- FastlyNsq.logger.error "heartbeat error: #{e.message}"
59
+ logger.error "Heartbeat error: #{e.message}"
52
60
  end
53
61
 
54
62
  def start_heartbeat
55
- loop do
63
+ until manager.stopped?
56
64
  heartbeat
57
- sleep 5
65
+ sleep pulse
58
66
  end
59
- FastlyNsq.logger.info('Heartbeat stopping...')
67
+ logger.info('Heartbeat stopping...')
60
68
  end
61
69
  end
@@ -1,117 +1,48 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'fastly_nsq/message'
4
- require 'fastly_nsq/manager'
5
- require 'fastly_nsq/safe_thread'
6
- require 'fastly_nsq/listener/config'
3
+ class FastlyNsq::Listener
4
+ extend Forwardable
7
5
 
8
- module FastlyNsq
9
- class Listener
10
- include FastlyNsq::SafeThread
6
+ DEFAULT_PRIORITY = 0
7
+ DEFAULT_CONNECTION_TIMEOUT = 5 # seconds
11
8
 
12
- def self.listen_to(*args)
13
- new(*args).go
14
- end
9
+ def_delegators :consumer, :connected?
15
10
 
16
- def initialize(topic:, processor:, channel: nil, consumer: nil, **options)
17
- @consumer = consumer || FastlyNsq::Consumer.new(topic: topic, channel: channel)
18
- @done = false
19
- @logger = options.fetch :logger, FastlyNsq.logger
20
- @manager = options[:manager] || FastlyNsq::Manager.new
21
- @preprocessor = options[:preprocessor]
22
- @processor = processor
23
- @thread = nil
24
- @topic = topic
25
- end
11
+ attr_reader :preprocessor, :topic, :processor, :priority, :channel, :logger, :consumer
26
12
 
27
- def identity
28
- {
29
- consumer: @consumer,
30
- manager: @manager,
31
- preprocessor: @preprocessor,
32
- processor: @processor,
33
- topic: @topic,
34
- }
35
- end
13
+ def initialize(topic:, processor:, preprocessor: FastlyNsq.preprocessor, channel: FastlyNsq.channel, consumer: nil,
14
+ logger: FastlyNsq.logger, priority: DEFAULT_PRIORITY, connect_timeout: DEFAULT_CONNECTION_TIMEOUT)
36
15
 
37
- def reset_then_dup
38
- reset
39
- dup
40
- end
16
+ raise ArgumentError, "processor #{processor.inspect} does not respond to #call" unless processor.respond_to?(:call)
17
+ raise ArgumentError, "priority #{priority.inspect} must be a Integer" unless priority.is_a?(Integer)
41
18
 
42
- def start
43
- @logger.info { "> Listener Started: topic #{@topic}" }
44
- @thread ||= safe_thread('listener', &method(:go))
45
- end
19
+ @channel = channel
20
+ @logger = logger
21
+ @preprocessor = preprocessor
22
+ @processor = processor
23
+ @topic = topic
24
+ @priority = priority
46
25
 
47
- def go(run_once: false)
48
- until @done
49
- next_message do |message|
50
- log message
51
- preprocess message
52
- @processor.process message
53
- end
26
+ @consumer = consumer || FastlyNsq::Consumer.new(topic: topic,
27
+ connect_timeout: connect_timeout,
28
+ channel: channel,
29
+ queue: FastlyNsq::Feeder.new(self, priority))
54
30
 
55
- terminate if run_once
56
- end
57
-
58
- @manager.listener_stopped(self)
59
- rescue FastlyNsq::Shutdown
60
- @manager.listener_stopped(self)
61
- rescue Exception => e # rubocop:disable Lint/RescueException
62
- @logger.error e.inspect
63
- @manager.listener_killed(self)
64
- end
65
-
66
- def status
67
- @thread.status if @thread
68
- end
69
-
70
- def terminate
71
- @done = true
72
- cleanup
73
- return unless @thread
74
- @logger.info "< Listener TERM: topic #{@topic}"
75
- # Interrupt a Consumer blocking in pop with no messages otherwise it will never shutdown
76
- @thread.raise FastlyNsq::Shutdown if @consumer.empty?
77
- end
78
-
79
- def kill
80
- @done = true
81
- cleanup
82
- return unless @thread
83
- @logger.info "< Listener KILL: topic #{@topic}"
84
- @thread.raise FastlyNsq::Shutdown
85
- end
86
-
87
- private
88
-
89
- def log(message)
90
- @logger.info "[NSQ] Message received on topic [#{@topic}]: #{message}" if @logger
91
- end
92
-
93
- def cleanup
94
- @consumer.terminate
95
- @logger.info "< Consumer terminated: topic [#{@topic}]"
96
- end
97
-
98
- def next_message
99
- nsq_message = @consumer.pop # TODO: consumer.pop do |message|
100
- message = FastlyNsq::Message.new(nsq_message)
101
- result = yield message
102
- message.finish if result
103
- end
31
+ FastlyNsq.manager.add_listener(self)
32
+ end
104
33
 
105
- def preprocess(message)
106
- @preprocessor.call(message) if @preprocessor
107
- end
34
+ def call(nsq_message)
35
+ message = FastlyNsq::Message.new(nsq_message)
36
+ logger.info "[NSQ] Message received on topic [#{topic}]: #{message}"
37
+ preprocessor&.call(message)
38
+ result = processor.call(message)
39
+ message.finish if result
40
+ message
41
+ end
108
42
 
109
- def reset
110
- @done = false
111
- @thread = nil
112
- self
113
- end
43
+ def terminate
44
+ return unless connected?
45
+ consumer.terminate
46
+ logger.info "< Consumer terminated: topic [#{topic}]"
114
47
  end
115
48
  end
116
-
117
- class FastlyNsq::Shutdown < StandardError; end
@@ -1,104 +1,80 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'set'
4
-
5
3
  class FastlyNsq::Manager
6
- attr_reader :listeners
4
+ DEADLINE = 30
5
+ DEFAULT_POOL_SIZE = 5
6
+
7
+ attr_reader :done, :pool, :logger
7
8
 
8
- def initialize(options = {})
9
- @options = options
10
- @done = false
11
- @listeners = Set.new
12
- @plock = Mutex.new
9
+ def initialize(logger: FastlyNsq.logger, **pool_options)
10
+ @done = false
11
+ @logger = logger
12
+ @pool = FastlyNsq::PriorityThreadPool.new(
13
+ { fallback_policy: :caller_runs, max_threads: DEFAULT_POOL_SIZE }.merge(pool_options),
14
+ )
13
15
  end
14
16
 
15
- def start
16
- setup_configured_listeners
17
- @listeners.each(&:start)
17
+ def topic_listeners
18
+ @topic_listeners ||= {}
18
19
  end
19
20
 
20
- def quiet
21
- return if @done
22
- @done = true
21
+ def topics
22
+ topic_listeners.keys
23
+ end
23
24
 
24
- FastlyNsq.logger.info { 'Terminating quiet listeners' }
25
- @listeners.each(&:terminate)
25
+ def listeners
26
+ topic_listeners.values.to_set
26
27
  end
27
28
 
28
- PAUSE_TIME = 0.5
29
+ def terminate(deadline = DEADLINE)
30
+ return if done
29
31
 
30
- def stop(deadline)
31
- quiet
32
+ stop_listeners
32
33
 
33
- sleep PAUSE_TIME
34
- return if @listeners.empty?
34
+ return if pool.shutdown?
35
35
 
36
- FastlyNsq.logger.info { 'Pausing to allow workers to finish...' }
37
- remaining = deadline - Time.now
38
- while remaining > PAUSE_TIME
39
- return if @listeners.empty?
40
- sleep PAUSE_TIME
41
- remaining = deadline - Time.now
42
- end
43
- return if @listeners.empty?
36
+ stop_processing(deadline)
44
37
 
45
- hard_shutdown
38
+ @done = true
46
39
  end
47
40
 
48
41
  def stopped?
49
- @done
42
+ done
50
43
  end
51
44
 
52
- def listener_stopped(listener)
53
- @plock.synchronize do
54
- @listeners.delete listener
55
- end
56
- end
45
+ def add_listener(listener)
46
+ logger.info { "Listening to topic:'#{listener.topic}' on channel: '#{listener.channel}'" }
57
47
 
58
- def listener_killed(listener)
59
- @plock.synchronize do
60
- @listeners.delete listener
61
- unless @done
62
- FastlyNsq.logger.info { "recreating listener for: #{listener.identity}" }
63
- new_listener = listener.reset_then_dup
64
- @listeners << new_listener
65
- new_listener.start
66
- end
48
+ if topic_listeners[listener.topic]
49
+ logger.warn { "topic: #{listener.topic} was added more than once" }
67
50
  end
68
- end
69
-
70
- private
71
51
 
72
- def setup_configured_listeners
73
- FastlyNsq.logger.debug { "options #{@options.inspect}" }
74
- FastlyNsq.logger.debug { "starting listeners: #{FastlyNsq.topic_map.inspect}" }
52
+ topic_listeners[listener.topic] = listener
53
+ end
75
54
 
76
- FastlyNsq.topic_map.each_pair do |topic, processor|
77
- @listeners << setup_listener(topic, processor)
78
- end
55
+ def transfer(new_manager, deadline: DEADLINE)
56
+ new_manager.topic_listeners.merge!(topic_listeners)
57
+ stop_processing(deadline)
58
+ topic_listeners.clear
59
+ @done = true
79
60
  end
80
61
 
81
- def setup_listener(topic, processor)
82
- FastlyNsq.logger.info { "Listening to topic:'#{topic}' on channel: '#{FastlyNsq.channel}'" }
83
- FastlyNsq::Listener.new(
84
- topic: topic,
85
- channel: FastlyNsq.channel,
86
- processor: processor,
87
- preprocessor: FastlyNsq.preprocessor,
88
- manager: self,
89
- )
62
+ def stop_listeners
63
+ logger.info { 'Stopping listeners' }
64
+ listeners.each(&:terminate)
65
+ topic_listeners.clear
90
66
  end
91
67
 
92
- def hard_shutdown
93
- cleanup = nil
94
- @plock.synchronize do
95
- cleanup = @listeners.dup
96
- end
68
+ protected
97
69
 
98
- unless cleanup.empty?
99
- FastlyNsq.logger.warn { "Terminating #{cleanup.size} busy worker threads" }
100
- end
70
+ def stop_processing(deadline)
71
+ logger.info { 'Stopping processors' }
72
+ pool.shutdown
73
+
74
+ logger.info { 'Waiting for processors to finish...' }
75
+ return if pool.wait_for_termination(deadline)
101
76
 
102
- cleanup.each(&:kill)
77
+ logger.info { 'Killing processors...' }
78
+ pool.kill
103
79
  end
104
80
  end