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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'json'
2
4
 
3
5
  class FastlyNsq::Message
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FastlyNsq::Messenger
2
- DEFAULT_ORIGIN = 'Unknown'.freeze
4
+ DEFAULT_ORIGIN = 'Unknown'
3
5
  @originating_service = DEFAULT_ORIGIN
4
6
 
5
7
  module_function
6
8
 
7
- def deliver(message:, on_topic:, originating_service: nil, meta: {})
9
+ def deliver(message:, topic:, originating_service: nil, meta: {})
8
10
  meta[:originating_service] = originating_service || self.originating_service
9
11
 
10
12
  payload = {
@@ -12,9 +14,7 @@ module FastlyNsq::Messenger
12
14
  meta: meta,
13
15
  }
14
16
 
15
- producer_for(topic: on_topic) do |producer|
16
- producer.write payload.to_json
17
- end
17
+ producer_for(topic: topic) { |producer| producer.write payload.to_json }
18
18
  end
19
19
 
20
20
  def originating_service=(service)
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class FastlyNsq::PriorityQueue < FastContainers::PriorityQueue
4
+ alias << push
5
+ alias length size
6
+
7
+ def shift
8
+ pop
9
+ rescue RuntimeError
10
+ nil
11
+ end
12
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ class FastlyNsq::PriorityThreadPool < Concurrent::ThreadPoolExecutor
4
+ alias max_threads max_length
5
+
6
+ def initialize(*)
7
+ super
8
+
9
+ @queue = FastlyNsq::PriorityQueue.new(:max)
10
+ end
11
+
12
+ # tries to enqueue task
13
+ # @return [true, false] if enqueued
14
+ #
15
+ # @!visibility private
16
+ def ns_enqueue(*args, &task)
17
+ if !ns_limited_queue? || @queue.size < @max_queue
18
+ @queue.push([task, args[1..-1]], args[0])
19
+ true
20
+ else
21
+ false
22
+ end
23
+ end
24
+
25
+ # tries to assign task to a worker, tries to get one from @ready or to create new one
26
+ # @return [true, false] if task is assigned to a worker
27
+ #
28
+ # @!visibility private
29
+ def ns_assign_worker(*args, &task)
30
+ super(args[1..-1], &task)
31
+ end
32
+ end
@@ -1,41 +1,61 @@
1
- require 'forwardable'
2
-
3
- module FastlyNsq
4
- class Producer
5
- extend Forwardable
6
- def_delegator :connection, :terminate
7
- def_delegator :connection, :write
8
-
9
- def initialize(topic:, tls_options: nil, connector: nil)
10
- @topic = topic
11
- @tls_options = TlsOptions.as_hash(tls_options)
12
- @connector = connector
13
- Timeout.timeout(5) do
14
- sleep(0.1) until connection.connected?
15
- end
16
- rescue Timeout::Error => error
17
- FastlyNsq.logger.error "Producer for #{topic} failed to connect!"
18
- connection.terminate
19
- raise error
20
- end
1
+ # frozen_string_literal: true
21
2
 
22
- private
3
+ class FastlyNsq::Producer
4
+ DEFAULT_CONNECTION_TIMEOUT = 5 # seconds
23
5
 
24
- attr_reader :topic, :tls_options
6
+ attr_reader :topic, :connect_timeout, :connection, :logger
25
7
 
26
- def connection
27
- @connection ||= connector.new(params)
28
- end
8
+ def initialize(topic:, tls_options: nil, logger: FastlyNsq.logger, connect_timeout: DEFAULT_CONNECTION_TIMEOUT)
9
+ @topic = topic
10
+ @tls_options = FastlyNsq::TlsOptions.as_hash(tls_options)
11
+ @connect_timeout = connect_timeout
12
+ @logger = logger
29
13
 
30
- def connector
31
- @connector || FastlyNsq.strategy::Producer
32
- end
14
+ connect
15
+ end
16
+
17
+ def terminate
18
+ connection.terminate
19
+ @connection = nil
20
+ end
21
+
22
+ def connected?
23
+ return false unless connection
24
+
25
+ connection.connected?
26
+ end
33
27
 
34
- def params
35
- {
36
- nsqlookupd: ENV.fetch('NSQLOOKUPD_HTTP_ADDRESS').split(',').map(&:strip),
28
+ def write(message)
29
+ raise FastlyNsq::NotConnectedError unless connected?
30
+ connection.write message
31
+ end
32
+
33
+ def connect
34
+ lookupd = FastlyNsq.lookupd_http_addresses
35
+
36
+ @connection ||= Nsq::Producer.new(
37
+ tls_options.merge(
38
+ nsqlookupd: lookupd,
37
39
  topic: topic,
38
- }.merge(tls_options)
40
+ ),
41
+ )
42
+
43
+ timeout_args = [connect_timeout, FastlyNsq::ConnectionFailed]
44
+
45
+ if RUBY_VERSION > '2.4.0'
46
+ timeout_args << "Failed connection to #{lookupd} within #{connect_timeout} seconds"
39
47
  end
48
+
49
+ Timeout.timeout(*timeout_args) { Thread.pass until connection.connected? }
50
+
51
+ true
52
+ rescue FastlyNsq::ConnectionFailed
53
+ logger.error { "Producer for #{topic} failed to connect!" }
54
+ terminate
55
+ raise
40
56
  end
57
+
58
+ private
59
+
60
+ attr_reader :tls_options
41
61
  end
@@ -0,0 +1,239 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FastlyNsq
4
+ class Testing
5
+ class << self
6
+ attr_accessor :__test_mode
7
+
8
+ def __set_test_mode(mode)
9
+ if block_given?
10
+ current_mode = __test_mode
11
+ begin
12
+ self.__test_mode = mode
13
+ yield
14
+ ensure
15
+ self.__test_mode = current_mode
16
+ end
17
+ else
18
+ self.__test_mode = mode
19
+ end
20
+ end
21
+
22
+ def disable!(&block)
23
+ __set_test_mode(:disable, &block)
24
+ end
25
+
26
+ def fake!(&block)
27
+ __set_test_mode(:fake, &block)
28
+ end
29
+
30
+ def inline!(&block)
31
+ __set_test_mode(:inline, &block)
32
+ end
33
+
34
+ def enabled?
35
+ __test_mode != :disable
36
+ end
37
+
38
+ def disabled?
39
+ __test_mode == :disable
40
+ end
41
+
42
+ def fake?
43
+ __test_mode == :fake
44
+ end
45
+
46
+ def inline?
47
+ __test_mode == :inline
48
+ end
49
+
50
+ def reset!
51
+ return unless enabled?
52
+ FastlyNsq::Messages.messages.clear
53
+ end
54
+ end
55
+ end
56
+
57
+ module Messages
58
+ def self.messages
59
+ @messages ||= Hash.new { |h, k| h[k] = [] }
60
+ end
61
+ end
62
+
63
+ class TestMessage
64
+ attr_reader :raw_body
65
+
66
+ def initialize(raw_body)
67
+ @raw_body = raw_body
68
+ end
69
+
70
+ def body
71
+ JSON.parse(JSON.dump(raw_body))
72
+ rescue JSON::ParserError
73
+ raw_body
74
+ end
75
+
76
+ def finish
77
+ FastlyNsq::Messages.messages.find { |_, ms| ms.delete(self) }
78
+ end
79
+
80
+ def requeue(*)
81
+ # sure
82
+ true
83
+ end
84
+ end
85
+
86
+ module ProducerTesting
87
+ def connected?
88
+ return super unless FastlyNsq::Testing.enabled?
89
+ @connected = true if @connected.nil?
90
+ @connected
91
+ end
92
+
93
+ def terminate
94
+ return super unless FastlyNsq::Testing.enabled?
95
+
96
+ @connected = false
97
+ end
98
+
99
+ def write(message)
100
+ return super unless FastlyNsq::Testing.enabled?
101
+
102
+ raise FastlyNsq::NotConnectedError unless connected?
103
+
104
+ test_message = TestMessage.new(message)
105
+ FastlyNsq::Messages.messages[topic] << test_message
106
+
107
+ if FastlyNsq::Testing.inline?
108
+ listener = FastlyNsq.manager.topic_listeners[topic]
109
+ return unless listener
110
+ listener.call test_message
111
+ end
112
+
113
+ true
114
+ end
115
+
116
+ def connection
117
+ return super unless FastlyNsq::Testing.enabled?
118
+ return nil unless connected?
119
+ self
120
+ end
121
+
122
+ def connect
123
+ return super unless FastlyNsq::Testing.enabled?
124
+ @connected = true
125
+ end
126
+
127
+ def messages
128
+ raise NoMethodError unless FastlyNsq::Testing.enabled?
129
+
130
+ FastlyNsq::Messages.messages[topic]
131
+ end
132
+ end
133
+
134
+ Producer.prepend(ProducerTesting)
135
+
136
+ module ListenerTesting
137
+ module ClassMethods
138
+ def messages(topic = nil)
139
+ return FastlyNsq::Messages.messages.values.flatten unless topic
140
+
141
+ FastlyNsq::Messages.messages[topic]
142
+ end
143
+
144
+ def drain(topic = nil)
145
+ topics = topic ? [topic] : FastlyNsq::Messages.messages.keys
146
+ topics.each do |t|
147
+ messages = FastlyNsq::Messages.messages[t]
148
+ next unless messages.any?
149
+ listener = FastlyNsq.manager.topic_listeners[t]
150
+ next unless listener
151
+
152
+ messages.dup.each { |message| listener.call(message) }
153
+ end
154
+ end
155
+
156
+ def clear
157
+ FastlyNsq::Messages.messages.clear
158
+ end
159
+ end
160
+
161
+ def self.prepended(klass)
162
+ klass.prepend(ClassMethods)
163
+ super
164
+ end
165
+
166
+ def terminate
167
+ return super unless FastlyNsq::Testing.enabled?
168
+
169
+ @connected = false
170
+ end
171
+
172
+ def connected?
173
+ return super unless FastlyNsq::Testing.enabled?
174
+ @connected = true if @connected.nil?
175
+
176
+ @connected
177
+ end
178
+
179
+ def drain
180
+ raise NoMethodError unless FastlyNsq::Testing.enabled?
181
+
182
+ self.class.drain(topic)
183
+ end
184
+
185
+ def messages
186
+ raise NoMethodError unless FastlyNsq::Testing.enabled?
187
+
188
+ self.class.messages(topic)
189
+ end
190
+ end
191
+
192
+ FastlyNsq::Listener.prepend(ListenerTesting)
193
+
194
+ module ConsumerTesting
195
+ module ClassMethods
196
+ def messages(topic = nil)
197
+ return FastlyNsq::Messages.messages.values.flatten unless topic
198
+
199
+ FastlyNsq::Messages.messages[topic]
200
+ end
201
+
202
+ def clear
203
+ FastlyNsq::Messages.messages.clear
204
+ end
205
+ end
206
+
207
+ def self.prepended(klass)
208
+ klass.prepend(ClassMethods)
209
+ super
210
+ end
211
+
212
+ def terminate
213
+ FastlyNsq::Testing.enabled? || super
214
+ end
215
+
216
+ def empty?
217
+ FastlyNsq::Testing.enabled? ? messages.empty? : super
218
+ end
219
+
220
+ def size
221
+ FastlyNsq::Testing.enabled? ? messages.size : super
222
+ end
223
+
224
+ def terminated?
225
+ FastlyNsq::Testing.enabled? ? false : super
226
+ end
227
+
228
+ def messages
229
+ raise NoMethodError unless FastlyNsq::Testing.enabled?
230
+
231
+ FastlyNsq::Messages.messages[topic]
232
+ end
233
+ end
234
+
235
+ FastlyNsq::Consumer.prepend(ConsumerTesting)
236
+ end
237
+
238
+ # Default to fake testing
239
+ FastlyNsq::Testing.fake!
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FastlyNsq
2
4
  class TlsOptions
3
5
  def self.as_hash(context = nil)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FastlyNsq
2
- VERSION = '0.13.2'.freeze
4
+ VERSION = '1.0.2'
3
5
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
  require 'fastly_nsq/cli'
3
5
 
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe FastlyNsq::Consumer do
6
+ let!(:topic) { 'fnsq' }
7
+ let!(:channel) { 'fnsq' }
8
+ let!(:queue) { nil }
9
+
10
+ subject { described_class.new(topic: topic, channel: channel, queue: queue) }
11
+
12
+ before { reset_topic(topic, channel: channel) }
13
+ before { expect { subject }.to eventually(be_connected).within(5) }
14
+
15
+ after { subject.terminate if subject.connected? }
16
+
17
+ it { should be_connected }
18
+
19
+ it 'should terminate' do
20
+ expect { subject.terminate }.to change(subject, :connected?).to(false)
21
+ end
22
+
23
+ describe 'with a specified queue' do
24
+ let!(:queue) { Queue.new }
25
+
26
+ it 'passes #queue to Nsq::Consumer' do
27
+ message = 'foo'
28
+
29
+ FastlyNsq::Messenger.deliver(message: message, topic: topic)
30
+
31
+ expect { queue.size }.to eventually(eq 1).within(15)
32
+ fastly_message = FastlyNsq::Message.new(queue.pop)
33
+
34
+ expect(fastly_message.data).to eq(message)
35
+ fastly_message.finish
36
+ end
37
+ end
38
+
39
+ it { should delegate(:size).to(:connection) }
40
+ it { should delegate(:terminate).to(:connection) }
41
+ it { should delegate(:pop).to(:connection) }
42
+ it { should delegate(:pop_without_blocking).to(:connection) }
43
+
44
+ describe 'with a message' do
45
+ let(:message) { 'foo' }
46
+
47
+ before { FastlyNsq::Messenger.deliver(message: message, topic: topic) }
48
+
49
+ it 'should not be empty' do
50
+ expect { subject }.to eventually(be_empty).within(15)
51
+ end
52
+
53
+ describe 'that has finished' do
54
+ before { subject.pop.finish }
55
+
56
+ it { should be_empty }
57
+ end
58
+ end
59
+ end