fastly_nsq 0.13.2 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
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