chasqui 0.9.3 → 1.0.0.pre.rc1

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/README.md +60 -39
  4. data/Vagrantfile +15 -0
  5. data/lib/chasqui.rb +28 -55
  6. data/lib/chasqui/broker.rb +3 -3
  7. data/lib/chasqui/{multi_broker.rb → brokers/redis_broker.rb} +11 -11
  8. data/lib/chasqui/config.rb +26 -6
  9. data/lib/chasqui/queue_adapter.rb +14 -0
  10. data/lib/chasqui/queue_adapters/redis_queue_adapter.rb +51 -0
  11. data/lib/chasqui/subscriber.rb +56 -49
  12. data/lib/chasqui/subscriptions.rb +67 -0
  13. data/lib/chasqui/version.rb +1 -1
  14. data/lib/chasqui/worker.rb +81 -0
  15. data/spec/integration/pubsub_examples.rb +20 -20
  16. data/spec/integration/resque_spec.rb +1 -1
  17. data/spec/integration/setup/subscribers.rb +25 -9
  18. data/spec/integration/sidekiq_spec.rb +1 -1
  19. data/spec/lib/chasqui/{multi_broker_spec.rb → brokers/redis_broker_spec.rb} +54 -26
  20. data/spec/lib/chasqui/cli_spec.rb +1 -1
  21. data/spec/lib/chasqui/config_spec.rb +121 -0
  22. data/spec/lib/chasqui/fake_queue_adapter_spec.rb +5 -0
  23. data/spec/lib/chasqui/queue_adapters/redis_queue_adapter_spec.rb +73 -0
  24. data/spec/lib/chasqui/subscriber_spec.rb +95 -43
  25. data/spec/lib/chasqui/subscriptions_spec.rb +60 -0
  26. data/spec/lib/chasqui/worker_spec.rb +96 -0
  27. data/spec/lib/chasqui_spec.rb +32 -191
  28. data/spec/spec_helper.rb +1 -1
  29. data/spec/support/chasqui_spec_helpers.rb +28 -0
  30. data/spec/support/fake_queue_adapter.rb +3 -0
  31. data/spec/support/fake_subscriber.rb +2 -1
  32. data/spec/support/shared_examples/queue_adapter_examples.rb +13 -0
  33. metadata +25 -18
  34. data/lib/chasqui/subscription.rb +0 -53
  35. data/lib/chasqui/workers/resque_worker.rb +0 -25
  36. data/lib/chasqui/workers/sidekiq_worker.rb +0 -45
  37. data/lib/chasqui/workers/worker.rb +0 -34
  38. data/spec/lib/chasqui/subscription_spec.rb +0 -35
  39. data/spec/lib/chasqui/workers/resque_worker_spec.rb +0 -27
  40. data/spec/lib/chasqui/workers/sidekiq_worker_spec.rb +0 -34
@@ -0,0 +1,60 @@
1
+ require 'spec_helper'
2
+
3
+ describe Chasqui::Subscriptions do
4
+ let(:queue_adapter) { FakeQueueAdapter.new }
5
+
6
+ subject { Chasqui::Subscriptions.new queue_adapter }
7
+
8
+ let(:a) { new_subscriber 'SubscriberA', channel: 'ch1', queue: 'foo' }
9
+ let(:b) { new_subscriber 'SubscriberB', channel: 'ch1', queue: 'bar' }
10
+ let(:c) { new_subscriber 'SubscriberC', channel: 'ch2', queue: 'foo' }
11
+ let(:d) { new_subscriber 'SubscriberD', channel: 'ch1', queue: 'foo' }
12
+
13
+ before do
14
+ reset_config
15
+
16
+ allow(Chasqui).to receive(:register)
17
+
18
+ [a, b, c, d].each do |subscriber|
19
+ expect(queue_adapter).to receive(:bind).with(subscriber)
20
+ subject.register subscriber
21
+ end
22
+ end
23
+
24
+ it 'registers subscribers' do
25
+ [a, b, c, d].each do |subscriber|
26
+ expect(subject.subscribed? subscriber).to be true
27
+ end
28
+
29
+ expect(subject.subscribers.size).to eq(4)
30
+
31
+ group1 = subject.find 'ch1', 'foo'
32
+ expect(group1).to include(a)
33
+ expect(group1).to include(d)
34
+
35
+ group2 = subject.find 'ch1', 'bar'
36
+ expect(group2.size).to eq(1)
37
+ expect(subject.find 'ch1', 'bar').to include(b)
38
+
39
+ group3 = subject.find 'ch2', 'foo'
40
+ expect(group3.size).to eq(1)
41
+ expect(subject.find 'ch2', 'foo').to include(c)
42
+ end
43
+
44
+ it 'unregisters subscribers' do
45
+ [a, b, c].each do |subscriber|
46
+ expect(queue_adapter).to receive(:unbind).with(subscriber)
47
+ subject.unregister subscriber
48
+ expect(subject.subscribed? subscriber).to be false
49
+ end
50
+
51
+ expect(subject.subscribed? d).to be true
52
+
53
+ expect(subject.find 'ch1', 'bar').to be_empty
54
+ expect(subject.find 'ch2', 'foo').to be_empty
55
+
56
+ remaining = subject.find 'ch1', 'foo'
57
+ expect(remaining.size).to eq(1)
58
+ expect(remaining).to include(d)
59
+ end
60
+ end
@@ -0,0 +1,96 @@
1
+ require 'spec_helper'
2
+
3
+ class MockSubscriber
4
+ include Chasqui::Subscriber
5
+ subscribe channel: 'foo-channel', prefix: nil, queue: 'foo-queue'
6
+
7
+ def perform(payload)
8
+ self.class.info[:event] = event
9
+ self.class.info[:payload] = payload
10
+ self.class.info[:redis] = redis
11
+ self.class.info[:logger] = logger
12
+ end
13
+
14
+ class << self
15
+ def info
16
+ @info ||= {}
17
+ end
18
+ end
19
+ end
20
+
21
+ describe Chasqui::Worker do
22
+ before do
23
+ reset_config
24
+ Chasqui::Worker.workers.clear
25
+ MockSubscriber.instance_variable_set :@info, nil
26
+ end
27
+
28
+ let(:subscriber) { MockSubscriber }
29
+
30
+ context 'no worker backend' do
31
+ describe '.create' do
32
+ it 'raises' do
33
+ Chasqui.config.worker_backend = :does_not_exist
34
+
35
+ expect(-> {
36
+ Chasqui::Worker.create subscriber
37
+ }).to raise_error(Chasqui::ConfigurationError)
38
+ end
39
+ end
40
+ end
41
+
42
+ context 'resque' do
43
+ before { Chasqui.config.worker_backend = :resque }
44
+ after { Chasqui::Workers.send :remove_const, :MockSubscriber }
45
+
46
+ describe '.create' do
47
+ let(:worker) { Chasqui::Worker.create subscriber }
48
+
49
+ it { expect(worker.name).to eq('Chasqui::Workers::MockSubscriber') }
50
+ it { expect(worker.instance_variable_get(:@queue)).to eq(subscriber.queue) }
51
+ it { expect(worker.instance_variable_get(:@subscriber)).to eq(subscriber) }
52
+
53
+ it 'delegates #perform to the subscriber' do
54
+ event = {
55
+ 'channel' => 'foo-channel',
56
+ 'payload' => { 'some' => 'data' }
57
+ }
58
+
59
+ worker.perform event
60
+
61
+ expect(subscriber.info[:event]).to eq(event)
62
+ expect(subscriber.info[:payload]).to eq(event['payload'])
63
+ end
64
+ end
65
+ end
66
+
67
+ if sidekiq_supported_ruby_version?
68
+ context 'sidekiq' do
69
+ before { Chasqui.config.worker_backend = :sidekiq }
70
+ after { Chasqui::Workers.send :remove_const, :MockSubscriber }
71
+
72
+ describe '.create' do
73
+ let(:worker) { Chasqui::Worker.create subscriber }
74
+
75
+ it { expect(worker.name).to eq('Chasqui::Workers::MockSubscriber') }
76
+ it { expect(worker.instance_variable_get(:@subscriber)).to eq(subscriber) }
77
+ it { expect(worker.sidekiq_options).to include('queue' => 'foo-queue') }
78
+ it { expect(worker.included_modules).to include(Sidekiq::Worker) }
79
+
80
+ it 'delegates #perform to the subscriber' do
81
+ event = {
82
+ 'channel' => 'foo-channel',
83
+ 'payload' => { 'some' => 'data' }
84
+ }
85
+
86
+ sidekiq_worker = worker.new
87
+ sidekiq_worker.perform event
88
+
89
+ expect(subscriber.info[:event]).to eq(event)
90
+ expect(subscriber.info[:payload]).to eq(event['payload'])
91
+ expect(subscriber.info[:logger]).to eq(sidekiq_worker.logger)
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -5,90 +5,40 @@ describe Chasqui do
5
5
  expect(Chasqui::VERSION).not_to be nil
6
6
  end
7
7
 
8
- describe '.configure' do
9
- before { reset_config }
10
-
11
- context 'defaults' do
12
- it { expect(Chasqui.channel).to eq('__default') }
13
- it { expect(Chasqui.inbox_queue).to eq('inbox') }
14
- it { expect(Chasqui.redis.client.db).to eq(0) }
15
- it { expect(Chasqui.config.broker_poll_interval).to eq(3) }
16
- it { expect(Chasqui.config.worker_backend).to eq(nil) }
17
-
18
- it do
19
- # remove chasqui's test environment logger
20
- Chasqui.config[:logger] = nil
21
- expect(Chasqui.logger).to be_kind_of(Logger)
22
- end
23
-
24
- it { expect(Chasqui.logger.level).to eq(Logger::INFO) }
25
- it { expect(Chasqui.logger.progname).to eq('chasqui') }
26
- end
27
-
28
- it 'configures the channel' do
29
- Chasqui.config.channel = 'com.example.test'
30
- expect(Chasqui.channel).to eq('com.example.test')
31
- end
32
-
33
- it 'accepts a block' do
34
- Chasqui.configure { |config| config.channel = 'com.example.test' }
35
- expect(Chasqui.channel).to eq('com.example.test')
36
- end
37
-
38
- it 'configures the inbox queue' do
39
- Chasqui.config.inbox_queue = 'foo'
40
- expect(Chasqui.inbox).to eq('foo')
41
- end
42
-
43
- it 'configures the broker poll interval' do
44
- Chasqui.config.broker_poll_interval = 1
45
- expect(Chasqui.config.broker_poll_interval).to eq(1)
8
+ describe '.config' do
9
+ it 'returns a config object' do
10
+ expect(Chasqui.config).to be_instance_of(Chasqui::Config)
46
11
  end
12
+ end
47
13
 
48
- context 'redis' do
49
- it 'accepts config options' do
50
- redis_config = { host: '10.0.3.24' }
51
- Chasqui.config.redis = redis_config
52
- expect(Chasqui.redis.client.host).to eq('10.0.3.24')
53
- end
54
-
55
- it 'accepts an initialized client' do
56
- redis = Redis.new db: 2
57
- Chasqui.config.redis = redis
58
- expect(Chasqui.redis.client.db).to eq(2)
59
- end
60
-
61
- it 'accepts URLs' do
62
- Chasqui.config.redis = 'redis://10.0.1.21:12345/0'
63
- expect(Chasqui.redis.client.host).to eq('10.0.1.21')
14
+ describe '.configure' do
15
+ it 'yields a config object' do
16
+ Chasqui.configure do |c|
17
+ expect(c).to be_instance_of(Chasqui::Config)
64
18
  end
19
+ end
20
+ end
65
21
 
66
- it 'uses a namespace' do
67
- Chasqui.redis.set 'foo', 'bar'
68
- expect(Chasqui.redis.redis.get 'chasqui:foo').to eq('bar')
22
+ describe 'config delegates' do
23
+ Chasqui::CONFIG_SETTINGS.each do |setting|
24
+ it "responds to #{setting}" do
25
+ expect(Chasqui).to respond_to(setting)
69
26
  end
70
27
  end
28
+ end
71
29
 
72
- describe 'logger' do
73
- it 'accepts a log device' do
74
- logs = StringIO.new
75
- Chasqui.config.logger = logs
76
- Chasqui.logger.info "status"
77
- Chasqui.logger.warn "error"
78
-
79
- logs.rewind
80
- output = logs.read
30
+ describe '.subscriptions' do
31
+ subject { Chasqui.subscriptions }
81
32
 
82
- %w(chasqui INFO status WARN error).each do |text|
83
- expect(output).to match(text)
84
- end
85
- end
33
+ it { should be_instance_of(Chasqui::Subscriptions) }
34
+ it { expect(subject.queue_adapter).to be_instance_of(Chasqui::QueueAdapters::RedisQueueAdapter) }
35
+ end
86
36
 
87
- it 'accepts a logger-like object' do
88
- fake_logger = FakeLogger.new
89
- Chasqui.config.logger = fake_logger
90
- expect(Chasqui.logger).to eq(fake_logger)
91
- expect(Chasqui.logger.progname).to eq('chasqui')
37
+ describe 'subscription management delegates' do
38
+ [:register, :unregister].each do |m|
39
+ it "delegates :#{m} to #subscriptions" do
40
+ expect(Chasqui.subscriptions).to receive(m)
41
+ Chasqui.send m
92
42
  end
93
43
  end
94
44
  end
@@ -106,128 +56,19 @@ describe Chasqui do
106
56
  Chasqui.publish 'test.event', *args
107
57
  end
108
58
 
109
- payloads.each do |data|
59
+ payloads.each do |payload|
110
60
  event = JSON.load Chasqui.redis.rpop('inbox')
111
- expect(event['event']).to eq('test.event')
112
- expect(event['channel']).to eq('__default')
113
- expect(event['data']).to eq(data)
114
- expect(event['created_at'].to_f).to be_within(0.01).of(Time.now.to_f)
61
+ expect(event['channel']).to eq('test.event')
62
+ expect(event['payload']).to eq(payload)
63
+ expect(event['created_at'].to_f).to be_within(0.1).of(Time.now.to_f)
64
+ expect(event['retry']).to eq(true)
115
65
  end
116
66
  end
117
67
 
118
- it 'supports channels' do
119
- Chasqui.config.channel = 'my.app'
120
- Chasqui.publish 'test.event', :foo
121
- event = JSON.load Chasqui.redis.rpop('inbox')
122
- expect(event['event']).to eq('test.event')
123
- expect(event['channel']).to eq('my.app')
124
- expect(event['data']).to eq(['foo'])
125
- end
126
-
127
68
  it 'supports retries' do
128
- Chasqui.publish 'test.event', :foo, :bar, foo: 'bar', job_options: { retry: true }
69
+ Chasqui.publish 'test.event', :foo, :bar, foo: 'bar', job_options: { retry: false }
129
70
  event = JSON.load Chasqui.redis.rpop('inbox')
130
- expect(event['retry']).to eq(true)
131
- end
132
- end
133
-
134
- describe '.subscribe' do
135
- before do
136
- reset_chasqui
137
- Resque.redis.namespace = :resque
138
- Chasqui.config.worker_backend = :resque
139
- end
140
-
141
- context 'with defaults' do
142
- it 'subscribes to events on the default channel' do
143
- sub = Chasqui.subscribe queue: 'my-queue'
144
-
145
- channel = Chasqui.config.channel
146
- queues = Chasqui.redis.smembers Chasqui.subscription_key(channel)
147
- expect(queues).to eq(['resque/resque:queue:my-queue'])
148
- end
149
- end
150
-
151
- context 'resque worker subscriptions' do
152
- before { Resque.redis.namespace = 'blah' }
153
-
154
- it 'creates subscriptions using the appropriate redis namespace' do
155
- sub1 = Chasqui.subscribe channel: 'com.example.admin', queue: 'app1-queue'
156
- sub2 = Chasqui.subscribe channel: 'com.example.admin', queue: 'app2-queue'
157
- sub3 = Chasqui.subscribe channel: 'com.example.video', queue: 'app1-queue'
158
-
159
- queues = Chasqui.redis.smembers Chasqui.subscription_key("com.example.admin")
160
- expect(queues.sort).to eq(['resque/blah:queue:app1-queue', 'resque/blah:queue:app2-queue'])
161
-
162
- queues = Chasqui.redis.smembers Chasqui.subscription_key("com.example.video")
163
- expect(queues).to eq(['resque/blah:queue:app1-queue'])
164
-
165
- expect(Chasqui.subscription('app1-queue')).to eq(sub1)
166
- expect(Chasqui.subscription('app2-queue')).to eq(sub2)
167
- expect(sub1).to eq(sub3)
168
- end
169
- end
170
-
171
- if sidekiq_supported_ruby_version?
172
- context 'sidekiq worker subscriptions' do
173
- before do
174
- Chasqui.config.worker_backend = :sidekiq
175
- end
176
-
177
- it 'creates subscriptions using the appropriate redis namespace' do
178
- Chasqui.subscribe channel: 'com.example.admin', queue: 'app1-queue'
179
- queues = Chasqui.redis.smembers Chasqui.subscription_key("com.example.admin")
180
- expect(queues.sort).to eq(['sidekiq/queue:app1-queue'])
181
-
182
- Sidekiq.redis = { url: redis.client.options[:url], namespace: 'foobar' }
183
- Chasqui.subscribe channel: 'com.example.video', queue: 'app2-queue'
184
- queues = Chasqui.redis.smembers Chasqui.subscription_key("com.example.video")
185
- expect(queues.sort).to eq(['sidekiq/foobar:queue:app2-queue'])
186
- end
187
- end
188
- end
189
-
190
- it 'returns a subscription' do
191
- subscription = Chasqui.subscribe channel: 'com.example.admin', queue: 'app1-queue'
192
- expect(subscription.subscriber).to be_kind_of(Chasqui::Subscriber)
193
- end
194
-
195
- it 'yields a subscriber configuration context' do
196
- $context = nil
197
- Chasqui.subscribe channel: 'bar', queue: 'foo' do
198
- $context = self
199
- end
200
- expect($context).to be_kind_of(Chasqui::Subscriber)
201
- end
202
- end
203
-
204
- describe '.unsubscribe' do
205
- before do
206
- reset_chasqui
207
- Chasqui.config.worker_backend = :resque
208
- Resque.redis.namespace = 'ns0'
209
- Chasqui.subscribe channel: 'com.example.admin', queue: 'app1-queue'
210
- Chasqui.subscribe channel: 'com.example.admin', queue: 'app2-queue'
211
- Chasqui.subscribe channel: 'com.example.video', queue: 'app1-queue'
212
- end
213
-
214
- it 'removes the subscription' do
215
- subscription_id = Chasqui.unsubscribe 'com.example.admin', queue: 'app1-queue'
216
- expect(subscription_id).to eq('resque/ns0:queue:app1-queue')
217
- expect(redis.smembers(Chasqui.subscription_key 'com.example.admin').sort).to eq(['resque/ns0:queue:app2-queue'])
218
- expect(redis.smembers(Chasqui.subscription_key 'com.example.video').sort).to eq(['resque/ns0:queue:app1-queue'])
219
- end
220
-
221
- it 'returns nil for unknown subscriptions' do
222
- subscription_id = Chasqui.unsubscribe 'unknown', queue: 'unknown'
223
- expect(subscription_id).to be nil
224
- end
225
- end
226
-
227
- describe '.subscriber_class_name' do
228
- it 'transforms queue name into a subscribe class name' do
229
- expect(Chasqui.subscriber_class_name('my-queue')).to eq(:Subscriber__my_queue)
230
- expect(Chasqui.subscriber_class_name('queue:my-queue')).to eq(:Subscriber__my_queue)
71
+ expect(event['retry']).to eq(false)
231
72
  end
232
73
  end
233
74
  end
data/spec/spec_helper.rb CHANGED
@@ -7,7 +7,7 @@ require 'chasqui'
7
7
  require 'resque'
8
8
  require 'pp'
9
9
 
10
- Dir['spec/support/*.rb'].each { |file| require File.expand_path(file) }
10
+ Dir['spec/support/**/*.rb'].each { |file| require File.expand_path(file) }
11
11
 
12
12
  RSpec.configure do |c|
13
13
  c.include ChasquiSpecHelpers
@@ -3,13 +3,23 @@ module ChasquiSpecHelpers
3
3
  def reset_chasqui
4
4
  flush_redis
5
5
  reset_config
6
+ reset_chasqui_workers
6
7
  end
7
8
 
8
9
  def reset_config
9
10
  Chasqui.instance_variable_set(:@config, nil)
11
+ Chasqui.instance_variable_set(:@subscriptions, nil)
10
12
  Chasqui.config.logger = './tmp/test.log'
11
13
  end
12
14
 
15
+ def reset_chasqui_workers
16
+ Chasqui::Worker.workers.clear
17
+
18
+ Chasqui::Workers.constants.each do |c|
19
+ Chasqui::Workers.send :remove_const, c
20
+ end
21
+ end
22
+
13
23
  def redis
14
24
  Chasqui.redis
15
25
  end
@@ -23,4 +33,22 @@ module ChasquiSpecHelpers
23
33
  nnredis.keys('*').each { |k| nnredis.del k }
24
34
  end
25
35
 
36
+ def new_subscriber(class_name, options={})
37
+ queue = options.fetch :queue
38
+ channel = options.fetch :channel
39
+
40
+ @subscriber_registry ||= {}
41
+
42
+ if @subscriber_registry[class_name] && options[:force]
43
+ Object.send :remove_const, class_name
44
+ @subscriber_registry[class_name] = nil
45
+ end
46
+
47
+ @subscriber_registry[class_name] ||= Class.new
48
+
49
+ @subscriber_registry[class_name].tap do |sub|
50
+ sub.send :include, Chasqui::Subscriber
51
+ sub.subscribe channel: channel, queue: queue
52
+ end
53
+ end
26
54
  end