chasqui 1.0.0.pre.rc1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/README.md +90 -78
  4. data/chasqui.gemspec +1 -1
  5. data/lib/chasqui.rb +83 -5
  6. data/lib/chasqui/cli.rb +8 -2
  7. data/lib/chasqui/config.rb +70 -8
  8. data/lib/chasqui/{queue_adapters → queue_adapter}/redis_queue_adapter.rb +6 -12
  9. data/lib/chasqui/subscriber.rb +1 -78
  10. data/lib/chasqui/subscription_builder.rb +135 -0
  11. data/lib/chasqui/subscription_builder/resque_subscription_builder.rb +31 -0
  12. data/lib/chasqui/subscription_builder/sidekiq_subscription_builder.rb +27 -0
  13. data/lib/chasqui/subscriptions.rb +20 -38
  14. data/lib/chasqui/version.rb +1 -1
  15. data/spec/integration/pubsub_examples.rb +1 -0
  16. data/spec/integration/setup/resque.rb +38 -1
  17. data/spec/integration/setup/sidekiq.rb +40 -1
  18. data/spec/lib/chasqui/brokers/redis_broker_spec.rb +12 -10
  19. data/spec/lib/chasqui/config_spec.rb +3 -3
  20. data/spec/lib/chasqui/{queue_adapters → queue_adapter}/redis_queue_adapter_spec.rb +6 -19
  21. data/spec/lib/chasqui/subscriber_spec.rb +4 -114
  22. data/spec/lib/chasqui/subscription_builder/resque_subscription_builder_spec.rb +20 -0
  23. data/spec/lib/chasqui/subscription_builder/sidekiq_subscription_builder_spec.rb +27 -0
  24. data/spec/lib/chasqui/subscription_builder_spec.rb +41 -0
  25. data/spec/lib/chasqui/subscriptions_spec.rb +4 -4
  26. data/spec/lib/chasqui_spec.rb +76 -12
  27. data/spec/support/chasqui_spec_helpers.rb +0 -21
  28. data/spec/support/fake_worker.rb +2 -0
  29. data/spec/support/shared_examples/subscription_builder_examples.rb +99 -0
  30. metadata +22 -16
  31. data/lib/chasqui/worker.rb +0 -81
  32. data/spec/integration/setup/subscribers.rb +0 -30
  33. data/spec/lib/chasqui/worker_spec.rb +0 -96
  34. data/spec/support/fake_subscriber.rb +0 -13
@@ -5,4 +5,43 @@ Sidekiq.configure_server do |config|
5
5
  config.redis = { url: ENV['REDIS_URL'], namespace: ENV['REDIS_NAMESPACE'] }
6
6
  end
7
7
 
8
- require './spec/integration/setup/subscribers'
8
+ def log_event(worker, event, *args)
9
+ queue = worker.sidekiq_options_hash['queue']
10
+ event['worker_args'] = args
11
+
12
+ Chasqui.redis.rpush "#{queue}:event_log", event.to_json
13
+ end
14
+
15
+ class UserSignupWorker
16
+ include Sidekiq::Worker
17
+ sidekiq_options queue: 'app1'
18
+
19
+ def perform(event, *args)
20
+ log_event self, event, *args
21
+ end
22
+ end
23
+
24
+ class TransactionWorker
25
+ include Sidekiq::Worker
26
+ sidekiq_options queue: 'app2'
27
+
28
+ def perform(event, *args)
29
+ log_event self, event, *args
30
+ end
31
+ end
32
+
33
+ class UserCancelWorker
34
+ include Sidekiq::Worker
35
+ sidekiq_options queue: 'app2'
36
+
37
+ def perform(event, *args)
38
+ log_event self, event, *args
39
+ end
40
+ end
41
+
42
+ Chasqui.subscribe do
43
+ on 'user.signup', UserSignupWorker
44
+ on 'user.cancel', UserCancelWorker
45
+ on 'account.credit', TransactionWorker
46
+ on 'account.debit', TransactionWorker
47
+ end
@@ -1,18 +1,18 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  class Worker1
4
- include Chasqui::Subscriber
5
- subscribe channel: 'app1', queue: 'queue1'
4
+ @queue = 'queue1'
5
+ def self.perform(event, *args); end
6
6
  end
7
7
 
8
8
  class Worker2
9
- include Chasqui::Subscriber
10
- subscribe channel: 'app2', queue: 'queue2'
9
+ @queue = 'queue2'
10
+ def self.perform(event, *args); end
11
11
  end
12
12
 
13
13
  class Worker3
14
- include Chasqui::Subscriber
15
- subscribe channel: 'app1', queue: 'queue3'
14
+ @queue = 'queue3'
15
+ def self.perform(event, *args); end
16
16
  end
17
17
 
18
18
  describe Chasqui::RedisBroker do
@@ -27,9 +27,11 @@ describe Chasqui::RedisBroker do
27
27
 
28
28
  describe '#forward_event' do
29
29
  before do
30
- Chasqui.register Worker1
31
- Chasqui.register Worker2
32
- Chasqui.register Worker3
30
+ Chasqui.subscribe do
31
+ on 'app1', Worker1
32
+ on 'app2', Worker2
33
+ on 'app1', Worker3
34
+ end
33
35
  end
34
36
 
35
37
  it 'places the event on all subscriber queues' do
@@ -65,7 +67,7 @@ describe Chasqui::RedisBroker do
65
67
  Chasqui.publish 'app2', foo: 'bar'
66
68
 
67
69
  job = JSON.parse nnredis.blpop('queue:queue2')[1]
68
- expect(job).to include('class' => 'Chasqui::Workers::Worker2')
70
+ expect(job).to include('class' => 'Worker2')
69
71
 
70
72
  event = {
71
73
  'channel' => 'app2',
@@ -4,11 +4,11 @@ describe Chasqui::Config do
4
4
 
5
5
  context 'defaults' do
6
6
  it { expect(subject.channel_prefix).to be nil }
7
- it { expect(subject.default_queue).to eq('chasqui-subscribers') }
8
- it { expect(subject.inbox_queue).to eq('inbox') }
7
+ it { expect(subject.default_queue).to eq('chasqui-workers') }
8
+ it { expect(subject.inbox_queue).to eq('chasqui-inbox') }
9
9
  it { expect(subject.redis.client.db).to eq(0) }
10
10
  it { expect(subject.broker_poll_interval).to eq(3) }
11
- it { expect(subject.queue_adapter).to eq(Chasqui::QueueAdapters::RedisQueueAdapter) }
11
+ it { expect(subject.queue_adapter).to eq(Chasqui::QueueAdapter::RedisQueueAdapter) }
12
12
 
13
13
  it do
14
14
  # remove chasqui's test environment logger
@@ -1,14 +1,11 @@
1
1
  require 'spec_helper'
2
2
 
3
- class MySubscriber
4
- include Chasqui::Subscriber
5
- subscribe channel: 'channel-name', queue: 'queue-name'
6
- end
7
-
8
- describe Chasqui::QueueAdapters::RedisQueueAdapter do
3
+ describe Chasqui::QueueAdapter::RedisQueueAdapter do
9
4
  it_behaves_like 'a queue adapter'
10
5
 
11
- let(:subscriber) { MySubscriber }
6
+ let(:subscriber) do
7
+ Chasqui::Subscriber.new('channel-name', 'queue-name', FakeWorker)
8
+ end
12
9
 
13
10
  describe '#bind / #unbind' do
14
11
  let(:key) { 'subscriptions:channel-name' }
@@ -26,12 +23,7 @@ describe Chasqui::QueueAdapters::RedisQueueAdapter do
26
23
  it 'persists the subscriptions to redis' do
27
24
  subject.bind(subscriber)
28
25
  subscriptions = redis.smembers(key)
29
- expect(subscriptions).to eq(
30
- ['resque/Chasqui::Workers::MySubscriber/resque:queue:queue-name'])
31
-
32
- expect(Chasqui::Workers.constants).to include(:MySubscriber)
33
- worker = Chasqui::Workers.const_get :MySubscriber
34
- expect(worker.subscriber).to eq(MySubscriber)
26
+ expect(subscriptions).to eq(['resque/FakeWorker/resque:queue:queue-name'])
35
27
 
36
28
  redis.sadd key, 'random'
37
29
 
@@ -52,14 +44,9 @@ describe Chasqui::QueueAdapters::RedisQueueAdapter do
52
44
  it 'persists the subscription to redis' do
53
45
  subject.bind(subscriber)
54
46
  subscriptions = redis.smembers('subscriptions:channel-name')
55
- expect(subscriptions).to eq(
56
- ['sidekiq/Chasqui::Workers::MySubscriber/queue:queue-name'])
47
+ expect(subscriptions).to eq(['sidekiq/FakeWorker/queue:queue-name'])
57
48
  expect(redis_no_namespace.smembers 'queues').to eq(['queue-name'])
58
49
 
59
- expect(Chasqui::Workers.constants).to include(:MySubscriber)
60
- worker = Chasqui::Workers.const_get :MySubscriber
61
- expect(worker.subscriber).to eq(MySubscriber)
62
-
63
50
  redis.sadd key, 'random'
64
51
 
65
52
  subject.unbind(subscriber)
@@ -1,119 +1,9 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Chasqui::Subscriber do
4
- let(:subscriber_class) { Class.new { include Chasqui::Subscriber } }
5
- let(:redis) { Redis.new }
4
+ subject { Chasqui::Subscriber.new 'foo', 'bar', 'worker' }
6
5
 
7
- describe '#event' do
8
- it { expect(subscriber_class.new(event: 'foo').event).to eq('foo') }
9
- it { expect(-> { subscriber_class.new }).to raise_error(KeyError) }
10
- end
11
-
12
- describe '#redis' do
13
- let(:subscriber) { subscriber_class.new event: nil }
14
-
15
- context 'default' do
16
- it { expect(subscriber.redis).to eq(Chasqui.redis) }
17
- end
18
-
19
- context 'custom' do
20
- let(:redis) { Redis.new }
21
- let(:subscriber) { subscriber_class.new event: nil, redis: redis }
22
- it { expect(subscriber.redis.object_id).to eq(redis.object_id) }
23
- end
24
- end
25
-
26
- describe '#logger' do
27
- let(:subscriber) { subscriber_class.new event: nil }
28
-
29
- context 'default' do
30
- it { expect(subscriber.logger).to eq(Chasqui.logger) }
31
- end
32
-
33
- context 'custom' do
34
- let(:logger) { FakeLogger.new }
35
- let(:subscriber) { subscriber_class.new event: nil, logger: logger }
36
- it { expect(subscriber.logger).to eq(logger) }
37
- end
38
- end
39
-
40
- describe '.channels' do
41
- before do
42
- expect(Chasqui).to receive(:register).with(subscriber_class).at_least(:once)
43
- subscriber_class.subscribe channel: 'some.channel'
44
- end
45
-
46
- context 'default' do
47
- it { expect(subscriber_class.channels).to include('some.channel') }
48
- end
49
-
50
- context 'with default prefix' do
51
- before do
52
- Chasqui.config.channel_prefix = 'prefix'
53
- subscriber_class.subscribe channel: 'some.channel'
54
- end
55
-
56
- it { expect(subscriber_class.channels).to include('prefix.some.channel') }
57
- end
58
-
59
- context 'with custom prefix' do
60
- before { Chasqui.config.channel_prefix = 'prefix' }
61
-
62
- it 'uses the custom prefix' do
63
- subscriber_class.subscribe channel: 'another.channel', prefix: 'custom'
64
- expect(subscriber_class.channels).to include('custom.another.channel')
65
- end
66
-
67
- it 'removes the prefix' do
68
- subscriber_class.subscribe channel: 'another.channel', prefix: nil
69
- expect(subscriber_class.channels).to include('another.channel')
70
- end
71
- end
72
-
73
- context 'multiple channels' do
74
- before { subscriber_class.subscribe channel: ['foo', 'bar'], prefix: 'custom' }
75
-
76
- it 'subscribes to multiple channels' do
77
- expect(subscriber_class.channels).to eq(['custom.foo', 'custom.bar'])
78
- end
79
- end
80
- end
81
-
82
- describe '.queue' do
83
- context 'default' do
84
- it { expect(subscriber_class.queue).to eq(Chasqui.default_queue) }
85
- end
86
-
87
- context 'custom' do
88
- before do
89
- expect(Chasqui).to receive(:register).with(subscriber_class)
90
- subscriber_class.subscribe queue: 'custom-queue'
91
- end
92
-
93
- it { expect(subscriber_class.queue).to eq('custom-queue') }
94
- end
95
- end
96
-
97
- describe '#perform' do
98
- let(:subscriber) { subscriber_class.new event: nil }
99
-
100
- it 'is not implemented' do
101
- expect(-> {
102
- subscriber.perform foo: 'bar'
103
- }).to raise_error(NotImplementedError)
104
- end
105
- end
106
-
107
- describe '.inherited' do
108
- before { allow(Chasqui).to receive(:register) }
109
-
110
- it 'maintains a registry of inherited classes' do
111
- klass = Class.new do
112
- include Chasqui::Subscriber
113
- subscribe
114
- end
115
-
116
- expect(Chasqui::Subscriber.subscribers).to include(klass)
117
- end
118
- end
6
+ it { expect(subject.channel).to eq('foo') }
7
+ it { expect(subject.queue).to eq('bar') }
8
+ it { expect(subject.worker).to eq('worker') }
119
9
  end
@@ -0,0 +1,20 @@
1
+ require 'spec_helper'
2
+
3
+ describe Chasqui::ResqueSubscriptionBuilder do
4
+ resque_worker = Class.new do
5
+ @queue = 'pubsub'
6
+
7
+ def self.perform(event, *args)
8
+ end
9
+ end
10
+
11
+ it_behaves_like 'a subscription builder', resque_worker
12
+
13
+ def queue_name(worker)
14
+ worker.instance_variable_get(:@queue)
15
+ end
16
+
17
+ def perform(worker, *perform_args)
18
+ worker.perform *perform_args
19
+ end
20
+ end
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+
3
+ if sidekiq_supported_ruby_version?
4
+ describe Chasqui::SidekiqSubscriptionBuilder do
5
+ sidekiq_worker = Class.new do
6
+ include Sidekiq::Worker
7
+ sidekiq_options queue: 'pubsub'
8
+
9
+ def perform(event, *args)
10
+ end
11
+ end
12
+
13
+ it_behaves_like 'a subscription builder', sidekiq_worker
14
+
15
+ def queue_name(worker)
16
+ worker.sidekiq_options['queue']
17
+ end
18
+
19
+ def perform(worker, *perform_args)
20
+ worker.new.perform *perform_args
21
+ end
22
+
23
+ def expect_worker_to_support_backend(worker)
24
+ expect(worker.included_modules).to include(Sidekiq::Worker)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+
3
+ describe Chasqui::SubscriptionBuilder do
4
+ describe '.builder' do
5
+ let(:subscriptions) { double }
6
+ let(:options) { { foo: 'bar' } }
7
+
8
+ before { reset_config }
9
+
10
+ context 'resque' do
11
+ it do
12
+ Chasqui.config.worker_backend = :resque
13
+
14
+ builder = described_class.builder subscriptions, options
15
+ expect(builder.subscriptions).to eq(subscriptions)
16
+ expect(builder.default_options).to eq(options)
17
+ expect(builder).to be_instance_of(Chasqui::ResqueSubscriptionBuilder)
18
+ end
19
+ end
20
+
21
+ context 'sidekiq' do
22
+ it do
23
+ Chasqui.config.worker_backend = :sidekiq
24
+
25
+ builder = described_class.builder subscriptions, options
26
+ expect(builder.subscriptions).to eq(subscriptions)
27
+ expect(builder.default_options).to eq(options)
28
+ expect(builder).to be_instance_of(Chasqui::SidekiqSubscriptionBuilder)
29
+ end
30
+ end
31
+
32
+ context 'neither' do
33
+ it do
34
+ Chasqui.config.worker_backend = :unknown
35
+ expect(-> {
36
+ described_class.builder subscriptions, options
37
+ }).to raise_error(Chasqui::ConfigurationError)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -5,10 +5,10 @@ describe Chasqui::Subscriptions do
5
5
 
6
6
  subject { Chasqui::Subscriptions.new queue_adapter }
7
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' }
8
+ let(:a) { Chasqui::Subscriber.new 'ch1', 'foo', double('workerA') }
9
+ let(:b) { Chasqui::Subscriber.new 'ch1', 'bar', double('workerB') }
10
+ let(:c) { Chasqui::Subscriber.new 'ch2', 'foo', double('workerC') }
11
+ let(:d) { Chasqui::Subscriber.new 'ch1', 'foo', double('workerD') }
12
12
 
13
13
  before do
14
14
  reset_config
@@ -31,16 +31,7 @@ describe Chasqui do
31
31
  subject { Chasqui.subscriptions }
32
32
 
33
33
  it { should be_instance_of(Chasqui::Subscriptions) }
34
- it { expect(subject.queue_adapter).to be_instance_of(Chasqui::QueueAdapters::RedisQueueAdapter) }
35
- end
36
-
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
42
- end
43
- end
34
+ it { expect(subject.queue_adapter).to be_instance_of(Chasqui::QueueAdapter::RedisQueueAdapter) }
44
35
  end
45
36
 
46
37
  describe '.publish' do
@@ -57,7 +48,7 @@ describe Chasqui do
57
48
  end
58
49
 
59
50
  payloads.each do |payload|
60
- event = JSON.load Chasqui.redis.rpop('inbox')
51
+ event = JSON.load Chasqui.redis.rpop(Chasqui.config.inbox_queue)
61
52
  expect(event['channel']).to eq('test.event')
62
53
  expect(event['payload']).to eq(payload)
63
54
  expect(event['created_at'].to_f).to be_within(0.1).of(Time.now.to_f)
@@ -67,8 +58,81 @@ describe Chasqui do
67
58
 
68
59
  it 'supports retries' do
69
60
  Chasqui.publish 'test.event', :foo, :bar, foo: 'bar', job_options: { retry: false }
70
- event = JSON.load Chasqui.redis.rpop('inbox')
61
+ event = JSON.load Chasqui.redis.rpop(Chasqui.config.inbox_queue)
71
62
  expect(event['retry']).to eq(false)
72
63
  end
73
64
  end
65
+
66
+ describe '.subscribe' do
67
+ let(:fake_builder) do
68
+ Class.new(Chasqui::SubscriptionBuilder) do
69
+ def self.handlers
70
+ @handlers ||= []
71
+ end
72
+
73
+ def on(channel, worker, options={})
74
+ self.class.handlers << [channel, worker, options]
75
+ end
76
+ end
77
+ end
78
+
79
+ before { reset_config }
80
+
81
+ it 'evaluates the block with a subscription builder binding' do
82
+ results = {}
83
+ worker = Class.new
84
+ allow(Chasqui::SubscriptionBuilder).to receive(:builder)
85
+ .and_return(fake_builder.new(nil))
86
+
87
+ Chasqui.subscribe queue: 'foo' do
88
+ results[:builder] = self
89
+ on 'channel', worker
90
+ end
91
+
92
+ builder = results[:builder]
93
+ expect(fake_builder.handlers.first).to eq(['channel', worker, {}])
94
+ end
95
+ end
96
+
97
+ FakeWorker1 = Class.new
98
+ FakeWorker2 = Class.new
99
+
100
+ describe '.unsubscribe' do
101
+ let(:subscriptions) { Chasqui.subscriptions }
102
+
103
+ before do
104
+ reset_chasqui
105
+ Chasqui.config.queue_adapter = FakeQueueAdapter
106
+ allow_any_instance_of(FakeQueueAdapter).to receive(:bind)
107
+ end
108
+
109
+ it 'unsubscribes all workers' do
110
+ sub1 = Chasqui::Subscriber.new 'channel', 'queue', FakeWorker1
111
+ sub2 = Chasqui::Subscriber.new 'channel', 'queue', FakeWorker2
112
+
113
+ subscriptions.register sub1
114
+ subscriptions.register sub2
115
+ expect(subscriptions.find 'channel', 'queue').to eq([sub1, sub2])
116
+
117
+ expect_any_instance_of(FakeQueueAdapter).to receive(:unbind).with(sub1)
118
+ expect_any_instance_of(FakeQueueAdapter).to receive(:unbind).with(sub2)
119
+
120
+ Chasqui.unsubscribe('channel', 'queue')
121
+ expect(subscriptions.find 'channel', 'queue').to be_empty
122
+ end
123
+
124
+ it 'unsubscribes a single worker' do
125
+ sub1 = Chasqui::Subscriber.new 'channel', 'queue', FakeWorker1
126
+ sub2 = Chasqui::Subscriber.new 'channel', 'queue', FakeWorker2
127
+
128
+ subscriptions.register sub1
129
+ subscriptions.register sub2
130
+ expect(subscriptions.find 'channel', 'queue').to eq([sub1, sub2])
131
+
132
+ expect_any_instance_of(FakeQueueAdapter).to receive(:unbind).with(sub1)
133
+
134
+ Chasqui.unsubscribe('channel', 'queue', FakeWorker1)
135
+ expect(subscriptions.find 'channel', 'queue').to eq([sub2])
136
+ end
137
+ end
74
138
  end