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,14 @@
1
+ module Chasqui
2
+ module QueueAdapter
3
+ def bind(subscriber)
4
+ raise NotImplementedError
5
+ end
6
+
7
+ def unbind(subscriber)
8
+ raise NotImplementedError
9
+ end
10
+ end
11
+
12
+ module QueueAdapters
13
+ end
14
+ end
@@ -0,0 +1,51 @@
1
+ module Chasqui
2
+ module QueueAdapters
3
+ class RedisQueueAdapter
4
+ extend Forwardable
5
+ def_delegators :Chasqui, :redis
6
+
7
+ def bind(subscriber)
8
+ Chasqui::Worker.create subscriber
9
+
10
+ subscriber.channels.each do |channel|
11
+ redis.sadd key(channel), queue_description(subscriber)
12
+ worker_redis.sadd 'queues', subscriber.queue
13
+ end
14
+ end
15
+
16
+ def unbind(subscriber)
17
+ subscriber.channels.each do |channel|
18
+ redis.srem key(channel), queue_description(subscriber)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def key(channel)
25
+ "subscriptions:#{channel}"
26
+ end
27
+
28
+ def queue_description(subscriber)
29
+ queue_name = [worker_namespace, 'queue', subscriber.queue].compact.join(':')
30
+ "#{worker_backend}/Chasqui::Workers::#{subscriber.name}/#{queue_name}"
31
+ end
32
+
33
+ def worker_redis
34
+ case worker_backend
35
+ when :resque
36
+ Resque.redis
37
+ when :sidekiq
38
+ Sidekiq.redis { |r| r }
39
+ end
40
+ end
41
+
42
+ def worker_namespace
43
+ worker_redis.namespace if worker_redis.respond_to? :namespace
44
+ end
45
+
46
+ def worker_backend
47
+ Chasqui.config.worker_backend
48
+ end
49
+ end
50
+ end
51
+ end
@@ -1,74 +1,81 @@
1
- require 'set'
2
-
3
1
  module Chasqui
2
+ module Subscriber
3
+ attr_reader :event
4
4
 
5
- HandlerAlreadyRegistered = Class.new StandardError
5
+ def self.included(klass)
6
+ klass.extend Chasqui::Subscriber::ClassMethods
7
+ end
6
8
 
7
- class Subscriber
8
- attr_accessor :redis, :current_event
9
- attr_reader :queue, :channel
9
+ def initialize(options={})
10
+ @event = options.fetch(:event)
10
11
 
11
- def initialize(queue, channel)
12
- @queue = queue
13
- @channel = channel
12
+ @logger = options[:logger]
13
+ @redis = options[:redis]
14
14
  end
15
15
 
16
- def on(event_name, &block)
17
- pattern = pattern_for_event event_name
16
+ def redis
17
+ @redis ||= Chasqui.redis
18
+ end
18
19
 
19
- if handler_patterns.include? pattern
20
- raise HandlerAlreadyRegistered.new "handler already registered for event: #{event_name}"
21
- else
22
- handler_patterns << pattern
23
- define_handler_method pattern, &block
24
- end
20
+ def logger
21
+ @logger ||= Chasqui.logger
22
+ end
23
+
24
+ def perform(payload)
25
+ raise NotImplementedError
25
26
  end
26
27
 
27
- def perform(redis_for_worker, event)
28
- self.redis = redis_for_worker
29
- self.current_event = event
28
+ class << self
29
+ attr_reader :subscribers
30
30
 
31
- matching_handler_patterns_for(event['event']).each do |pattern|
32
- call_handler pattern, *event['data']
31
+ def register_subscriber(klass)
32
+ @subscribers ||= Set.new
33
+ @subscribers << klass
34
+ Chasqui.register klass
33
35
  end
34
36
  end
35
37
 
36
- def matching_handler_patterns_for(event_name)
37
- handler_patterns.select do |pattern|
38
- pattern =~ event_name
38
+ module ClassMethods
39
+ SubscriberConfig = Struct.new :channels, :queue
40
+
41
+ def subscriber_config
42
+ @subscriber_config ||= SubscriberConfig.new(
43
+ Chasqui.channel_prefix,
44
+ Chasqui.default_queue
45
+ )
39
46
  end
40
- end
41
47
 
42
- def call_handler(pattern, *args)
43
- send "handler__#{pattern.to_s}", *args
44
- end
48
+ def subscribe(args={})
49
+ queue = args.fetch :queue, Chasqui.default_queue
45
50
 
46
- def evaluate(&block)
47
- @self_before_instance_eval = eval "self", block.binding
48
- instance_eval &block
49
- end
51
+ subscriber_config.channels = prefixed_channels args
52
+ subscriber_config.queue = queue
50
53
 
51
- private
54
+ Chasqui::Subscriber.register_subscriber self
55
+ end
52
56
 
53
- def handler_patterns
54
- @handler_patterns ||= Set.new
55
- end
57
+ def channels
58
+ subscriber_config.channels
59
+ end
56
60
 
57
- def method_missing(method, *args, &block)
58
- if @self_before_instance_eval
59
- @self_before_instance_eval.send method, *args, &block
60
- else
61
- super
61
+ def queue
62
+ subscriber_config.queue
62
63
  end
63
- end
64
64
 
65
- def pattern_for_event(event_name)
66
- /\A#{event_name.to_s.downcase.gsub('*', '.*')}\z/
67
- end
65
+ private
68
66
 
69
- def define_handler_method(pattern, &block)
70
- self.class.send :define_method, "handler__#{pattern.to_s}", &block
71
- end
67
+ def prefixed_channels(args)
68
+ channel = args.fetch :channel, Chasqui.channel_prefix
69
+ prefix = args.fetch :prefix, Chasqui.channel_prefix
70
+
71
+ channels = channel.respond_to?(:each) ? channel : [channel]
72
72
 
73
+ if prefix
74
+ channels.map { |c| "#{prefix}.#{c}" }
75
+ else
76
+ channels
77
+ end
78
+ end
79
+ end
73
80
  end
74
81
  end
@@ -0,0 +1,67 @@
1
+ module Chasqui
2
+ class Subscriptions
3
+ attr_reader :queue_adapter
4
+
5
+ def initialize(queue_adapter)
6
+ @subscriptions = {}
7
+ @subscribers ||= {}
8
+ @queue_adapter = queue_adapter
9
+ end
10
+
11
+ def register(subscriber)
12
+ subscriber.channels.each do |channel|
13
+ register_one channel, subscriber
14
+ end
15
+ end
16
+
17
+ def unregister(subscriber)
18
+ subscriber.channels.each do |channel|
19
+ unregister_one channel, subscriber
20
+ end
21
+ end
22
+
23
+ def find(channel, queue)
24
+ @subscriptions[queue.to_s][channel.to_s].values
25
+ end
26
+
27
+ def subscribers
28
+ @subscribers.values
29
+ end
30
+
31
+ def subscribed?(subscriber)
32
+ @subscribers.key? subscriber.object_id
33
+ end
34
+
35
+ def autoregister!
36
+ Subscriber.subscribers.each { |s| register s }
37
+ end
38
+
39
+ private
40
+
41
+ def register_one(channel, subscriber)
42
+ q = subscriber.queue.to_s
43
+ c = channel.to_s
44
+
45
+ @subscriptions[q] ||= {}
46
+ @subscriptions[q][c] ||= {}
47
+ @subscriptions[q][c][subscriber.object_id] = subscriber
48
+
49
+ @subscribers[subscriber.object_id] = subscriber
50
+
51
+ queue_adapter.bind subscriber
52
+ end
53
+
54
+ def unregister_one(channel, subscriber)
55
+ q = subscriber.queue.to_s
56
+ c = channel.to_s
57
+
58
+ queue_adapter.unbind subscriber
59
+
60
+ if @subscriptions[q] && @subscriptions[q][c]
61
+ @subscriptions[q][c].delete subscriber.object_id
62
+ end
63
+
64
+ @subscribers.delete subscriber.object_id
65
+ end
66
+ end
67
+ end
@@ -1,3 +1,3 @@
1
1
  module Chasqui
2
- VERSION = "0.9.3"
2
+ VERSION = '1.0.0-rc1'
3
3
  end
@@ -0,0 +1,81 @@
1
+ module Chasqui
2
+ module Chasqui::Workers
3
+ end
4
+
5
+ class Worker
6
+ class << self
7
+
8
+ BACKENDS = {
9
+ resque: 'ResqueWorkerFactory',
10
+ sidekiq: 'SidekiqWorkerFactory'
11
+ }
12
+
13
+ def create(subscriber)
14
+ workers[subscriber.object_id] ||= create_worker(subscriber)
15
+ end
16
+
17
+ def workers
18
+ @workers ||= {}
19
+ end
20
+
21
+ private
22
+
23
+ def check_for_worker_backend
24
+ unless BACKENDS.keys.include? Chasqui.worker_backend
25
+ msg = "Chasqui.config.worker_backend must be one of #{BACKENDS.keys}"
26
+ raise ConfigurationError.new msg
27
+ end
28
+ end
29
+
30
+ def worker_factory
31
+ Chasqui.const_get BACKENDS[Chasqui.worker_backend]
32
+ end
33
+
34
+ def create_worker(subscriber)
35
+ check_for_worker_backend
36
+ worker = worker_factory.create subscriber
37
+ Chasqui::Workers.const_set subscriber.name, worker
38
+ end
39
+
40
+ end
41
+ end
42
+
43
+ class ResqueWorkerFactory
44
+ def self.create(subscriber)
45
+ Class.new do
46
+ @queue = subscriber.queue
47
+ @subscriber = subscriber
48
+
49
+ class << self
50
+ attr_reader :subscriber
51
+ end
52
+
53
+ def self.perform(event)
54
+ instance = @subscriber.new event: event, logger: Resque.logger
55
+ instance.perform event['payload']
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ class SidekiqWorkerFactory
62
+ def self.create(subscriber)
63
+ Class.new do
64
+ include Sidekiq::Worker
65
+ sidekiq_options 'queue' => 'foo-queue'
66
+
67
+ @subscriber = subscriber
68
+
69
+ class << self
70
+ attr_reader :subscriber
71
+ end
72
+
73
+ def perform(event)
74
+ instance = self.class.subscriber.new event: event, logger: logger
75
+ instance.perform event['payload']
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ end
@@ -1,31 +1,31 @@
1
1
  require 'spec_helper'
2
2
 
3
- shared_examples 'pubsub' do |namespace, start_workers_method|
3
+ shared_examples 'pubsub' do |start_workers_method|
4
4
 
5
5
  def events_to_publish
6
6
  [
7
- { event: 'user.signup', data: ['Kelly'] },
8
- { event: 'account.credit', data: [1337, 'Kelly'] },
9
- { event: 'account.debit', data: [10, 'Kelly'] },
10
- { event: 'user.signup', data: ['Travis'] },
11
- { event: 'account.debit', data: [9000, 'Kelly'] },
12
- { event: 'user.cancel', data: ['Kelly'] },
13
- { event: 'account.credit', data: [42, 'Travis'] },
7
+ { channel: 'user.signup', payload: ['Kelly'] },
8
+ { channel: 'account.credit', payload: [1337, 'Kelly'] },
9
+ { channel: 'account.debit', payload: [10, 'Kelly'] },
10
+ { channel: 'user.signup', payload: ['Travis'] },
11
+ { channel: 'account.debit', payload: [9000, 'Kelly'] },
12
+ { channel: 'user.cancel', payload: ['Kelly'] },
13
+ { channel: 'account.credit', payload: [42, 'Travis'] },
14
14
  ]
15
15
  end
16
16
 
17
17
  def expected_events
18
18
  {
19
19
  'app1' => [
20
- { event: 'user.signup', data: ['Kelly'] },
21
- { event: 'user.signup', data: ['Travis'] },
20
+ { channel: 'user.signup', payload: ['Kelly'] },
21
+ { channel: 'user.signup', payload: ['Travis'] },
22
22
  ],
23
23
  'app2' => [
24
- { event: 'account.credit', data: [1337, 'Kelly'] },
25
- { event: 'account.debit', data: [10, 'Kelly'] },
26
- { event: 'account.debit', data: [9000, 'Kelly'] },
27
- { event: 'user.cancel', data: ['Kelly'] },
28
- { event: 'account.credit', data: [42, 'Travis'] },
24
+ { channel: 'account.credit', payload: [1337, 'Kelly'] },
25
+ { channel: 'account.debit', payload: [10, 'Kelly'] },
26
+ { channel: 'account.debit', payload: [9000, 'Kelly'] },
27
+ { channel: 'user.cancel', payload: ['Kelly'] },
28
+ { channel: 'account.credit', payload: [42, 'Travis'] },
29
29
  ],
30
30
  }
31
31
  end
@@ -37,7 +37,7 @@ shared_examples 'pubsub' do |namespace, start_workers_method|
37
37
  @redis.keys('*').each { |k| @redis.del k }
38
38
 
39
39
  Chasqui.configure do |c|
40
- c.channel = 'integration'
40
+ c.channel_prefix = 'integration'
41
41
  c.redis = @redis_url
42
42
  end
43
43
 
@@ -56,17 +56,17 @@ shared_examples 'pubsub' do |namespace, start_workers_method|
56
56
 
57
57
  it 'works' do
58
58
  events_to_publish.each do |event|
59
- Chasqui.publish event[:event], *event[:data]
59
+ Chasqui.publish event[:channel], *event[:payload]
60
60
  end
61
61
 
62
62
  begin
63
63
  Timeout::timeout(10) do
64
64
  expected_events.each do |subscriber_queue, events|
65
65
  events.each do |expected|
66
- _, payload = @redis.blpop "#{namespace}:#{subscriber_queue}:event_log"
66
+ _, payload = Chasqui.redis.blpop "#{subscriber_queue}:event_log"
67
67
  actual = JSON.parse payload
68
- expect(actual['event']).to eq(expected[:event])
69
- expect(actual['data']).to eq(expected[:data])
68
+ expect(actual['channel']).to eq(expected[:channel])
69
+ expect(actual['payload']).to eq(expected[:payload])
70
70
  end
71
71
  end
72
72
  end
@@ -2,7 +2,7 @@ require 'spec_helper'
2
2
  require 'integration/pubsub_examples'
3
3
 
4
4
  describe "resque integration", integration: true do
5
- include_examples 'pubsub', :resque, :start_resque_workers
5
+ include_examples 'pubsub', :start_resque_workers
6
6
 
7
7
  def start_resque_workers
8
8
  @subscriber_queues.each do |queue|
@@ -1,14 +1,30 @@
1
- def log_event subscriber, args
2
- event = subscriber.current_event
3
- payload = { event: event['event'], data: args }.to_json
4
- subscriber.redis.rpush "#{subscriber.queue}:event_log", payload
1
+ def log_event subscriber, payload
2
+ Chasqui.redis.rpush "#{subscriber.class.queue}:event_log", subscriber.event.to_json
5
3
  end
6
4
 
7
- Chasqui.subscribe channel: 'integration', queue: 'app1' do
8
- on('user.signup') { |*args| log_event self, args }
5
+ class UserSignupSubscriber
6
+ include Chasqui::Subscriber
7
+ subscribe channel: 'user.signup', queue: 'app1'
8
+
9
+ def perform(payload)
10
+ log_event self, payload
11
+ end
9
12
  end
10
13
 
11
- Chasqui.subscribe channel: 'integration', queue: 'app2' do
12
- on('account.*') { |*args| log_event self, args }
13
- on('user.cancel') { |*args| log_event self, args }
14
+ class AccountSubscriber
15
+ include Chasqui::Subscriber
16
+ subscribe channel: ['account.credit', 'account.debit'], queue: 'app2'
17
+
18
+ def perform(payload)
19
+ log_event self, payload
20
+ end
21
+ end
22
+
23
+ class UserCancelSubscriber
24
+ include Chasqui::Subscriber
25
+ subscribe channel: 'user.cancel', queue: 'app2'
26
+
27
+ def perform(payload)
28
+ log_event self, payload
29
+ end
14
30
  end