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
@@ -1,22 +1,16 @@
1
1
  module Chasqui
2
- module QueueAdapters
2
+ module QueueAdapter
3
3
  class RedisQueueAdapter
4
4
  extend Forwardable
5
5
  def_delegators :Chasqui, :redis
6
6
 
7
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
8
+ redis.sadd key(subscriber.channel), queue_description(subscriber)
9
+ worker_redis.sadd 'queues', subscriber.queue
14
10
  end
15
11
 
16
12
  def unbind(subscriber)
17
- subscriber.channels.each do |channel|
18
- redis.srem key(channel), queue_description(subscriber)
19
- end
13
+ redis.srem key(subscriber.channel), queue_description(subscriber)
20
14
  end
21
15
 
22
16
  private
@@ -27,7 +21,7 @@ module Chasqui
27
21
 
28
22
  def queue_description(subscriber)
29
23
  queue_name = [worker_namespace, 'queue', subscriber.queue].compact.join(':')
30
- "#{worker_backend}/Chasqui::Workers::#{subscriber.name}/#{queue_name}"
24
+ "#{worker_backend}/#{subscriber.worker.name}/#{queue_name}"
31
25
  end
32
26
 
33
27
  def worker_redis
@@ -35,7 +29,7 @@ module Chasqui
35
29
  when :resque
36
30
  Resque.redis
37
31
  when :sidekiq
38
- Sidekiq.redis { |r| r }
32
+ ::Sidekiq.redis { |r| r }
39
33
  end
40
34
  end
41
35
 
@@ -1,81 +1,4 @@
1
1
  module Chasqui
2
- module Subscriber
3
- attr_reader :event
4
-
5
- def self.included(klass)
6
- klass.extend Chasqui::Subscriber::ClassMethods
7
- end
8
-
9
- def initialize(options={})
10
- @event = options.fetch(:event)
11
-
12
- @logger = options[:logger]
13
- @redis = options[:redis]
14
- end
15
-
16
- def redis
17
- @redis ||= Chasqui.redis
18
- end
19
-
20
- def logger
21
- @logger ||= Chasqui.logger
22
- end
23
-
24
- def perform(payload)
25
- raise NotImplementedError
26
- end
27
-
28
- class << self
29
- attr_reader :subscribers
30
-
31
- def register_subscriber(klass)
32
- @subscribers ||= Set.new
33
- @subscribers << klass
34
- Chasqui.register klass
35
- end
36
- end
37
-
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
- )
46
- end
47
-
48
- def subscribe(args={})
49
- queue = args.fetch :queue, Chasqui.default_queue
50
-
51
- subscriber_config.channels = prefixed_channels args
52
- subscriber_config.queue = queue
53
-
54
- Chasqui::Subscriber.register_subscriber self
55
- end
56
-
57
- def channels
58
- subscriber_config.channels
59
- end
60
-
61
- def queue
62
- subscriber_config.queue
63
- end
64
-
65
- private
66
-
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
-
73
- if prefix
74
- channels.map { |c| "#{prefix}.#{c}" }
75
- else
76
- channels
77
- end
78
- end
79
- end
2
+ class Subscriber < Struct.new(:channel, :queue, :worker)
80
3
  end
81
4
  end
@@ -0,0 +1,135 @@
1
+ module Chasqui
2
+ # A namespace for defining dynamically generated worker classes for callable
3
+ # objects.
4
+ module Workers
5
+ end
6
+
7
+ # Provides the context used in {Chasqui.subscribe} to bind workers to
8
+ # channels via the {#on} method.
9
+ class SubscriptionBuilder
10
+ # The collection of currently registered subscriptions.
11
+ # @return [Chasqui::Subscriptions]
12
+ attr_reader :subscriptions
13
+
14
+ # Default options for calls to {#on}.
15
+ # @return [Hash]
16
+ attr_reader :default_options
17
+
18
+ # @visibility private
19
+ # You should not instantiate this class directly. Instead use the
20
+ # {Chasqui.subscribe} method to create a subscription builder context.
21
+ def initialize(subscriptions, options={})
22
+ @subscriptions = subscriptions
23
+ @default_options = options
24
+ end
25
+
26
+ # Bind a worker to a channel.
27
+ #
28
+ # The broker will place jobs on the worker's queue for each event published
29
+ # to the given channel.
30
+ #
31
+ # @param channel [String] the channel name
32
+ # @param worker_or_proc [#perform,.perform,#call] a Sidekiq Worker
33
+ # class, Resque worker class, or proc to handle events published to
34
+ # channel. If a proc is used as a worker, +#on+ will define a new worker
35
+ # class that delegates +#perform+ to +proc#call+.
36
+ # @param options [Hash]
37
+ #
38
+ # @option options [String] :queue the worker queue.
39
+ # When given, this option will override the queue defined by the worker
40
+ # class. This option is recommended when using a proc as a worker.
41
+ # @option options [String] :queue_prefix prefix for queue.
42
+ # When supplied, the value of this option is prepended to the queue name.
43
+ # Use this option to namespace your queues in order to prevent collisions
44
+ # with queues from other applicaitons sharing the same Redis database.
45
+ def on(channel, worker_or_proc, options={})
46
+ options = default_options.merge(options)
47
+ worker = build_worker(channel, worker_or_proc, options)
48
+
49
+ queue = full_queue_name(worker, options)
50
+ set_queue_name(worker, queue)
51
+
52
+ subscriptions.register Chasqui::Subscriber.new(channel, queue, worker)
53
+ end
54
+
55
+ # @visibility private
56
+ # Instantiate a new subscription builder suitable for the configured
57
+ # {Chasqui.worker_backend worker_backend}.
58
+ def self.builder(subscriptions, options={})
59
+ builder_for_backend.new subscriptions, options
60
+ end
61
+
62
+ protected
63
+
64
+ def get_queue_name(worker)
65
+ raise NotImplementedError
66
+ end
67
+
68
+ def set_queue_name(worker, queue)
69
+ raise NotImplementedError
70
+ end
71
+
72
+ def define_worker_class(channel, callable, options)
73
+ raise NotImplementedError
74
+ end
75
+
76
+ def redefine_perform_method(worker, &block)
77
+ raise NotImplementedError
78
+ end
79
+
80
+ private
81
+
82
+ def self.builder_for_backend
83
+ case Chasqui.worker_backend
84
+ when :resque
85
+ ResqueSubscriptionBuilder
86
+ when :sidekiq
87
+ SidekiqSubscriptionBuilder
88
+ else
89
+ msg = <<-ERR.gsub(/^ {8}/, '')
90
+ No worker backend configured.
91
+
92
+ # To configure a worker backend:
93
+ Chasqui.config do |c|
94
+ c.worker_backend = :resque # or :sidekiq
95
+ end
96
+ ERR
97
+ raise Chasqui::ConfigurationError.new msg
98
+ end
99
+ end
100
+
101
+ def full_queue_name(worker, options={})
102
+ queue = options.fetch :queue, get_queue_name(worker)
103
+ prefix = options[:queue_prefix]
104
+
105
+ prefix ? "#{prefix}:#{queue}" : queue
106
+ end
107
+
108
+ def build_worker(channel, worker_or_proc, options={})
109
+ worker = worker_or_proc
110
+
111
+ if worker.respond_to? :call
112
+ worker = define_worker_class(channel, worker_or_proc, options)
113
+ Chasqui::Workers.const_set worker_class_name(channel), worker
114
+ end
115
+
116
+ redefine_perform_method(worker) do |klass|
117
+ klass.send :define_method, :perform_with_event do |event|
118
+ perform_without_event event, *event['payload']
119
+ end
120
+
121
+ klass.send :alias_method, :perform_without_event, :perform
122
+ klass.send :alias_method, :perform, :perform_with_event
123
+ end
124
+
125
+ worker
126
+ end
127
+
128
+ def worker_class_name(channel)
129
+ segments = channel.split(/[^\w]/).map(&:downcase)
130
+ name = segments.each { |w| w[0] = w[0].upcase }.join
131
+
132
+ "#{name}Worker".freeze
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,31 @@
1
+ module Chasqui
2
+ class ResqueSubscriptionBuilder < SubscriptionBuilder
3
+ protected
4
+
5
+ def get_queue_name(worker)
6
+ worker.instance_variable_get :@queue
7
+ end
8
+
9
+ def set_queue_name(worker, queue)
10
+ worker.instance_variable_set :@queue, queue
11
+ end
12
+
13
+ def define_worker_class(channel, callable, options)
14
+ Class.new do
15
+ @queue = Chasqui.default_queue
16
+ define_singleton_method :perform, callable
17
+ end
18
+ end
19
+
20
+ def redefine_perform_method(worker)
21
+ return if worker.respond_to?(:perform_with_event)
22
+
23
+ worker.instance_eval do
24
+ class << self
25
+ yield self
26
+ end
27
+ end
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,27 @@
1
+ module Chasqui
2
+ class SidekiqSubscriptionBuilder < SubscriptionBuilder
3
+ protected
4
+
5
+ def get_queue_name(worker)
6
+ worker.sidekiq_options['queue']
7
+ end
8
+
9
+ def set_queue_name(worker, queue)
10
+ worker.sidekiq_options queue: queue
11
+ end
12
+
13
+ def define_worker_class(channel, callable, options)
14
+ Class.new do
15
+ include ::Sidekiq::Worker
16
+ sidekiq_options queue: Chasqui.default_queue
17
+ define_method :perform, callable
18
+ end
19
+ end
20
+
21
+ def redefine_perform_method(worker, &block)
22
+ return if worker.instance_methods.include?(:perform_with_event)
23
+
24
+ yield worker
25
+ end
26
+ end
27
+ end
@@ -9,15 +9,29 @@ module Chasqui
9
9
  end
10
10
 
11
11
  def register(subscriber)
12
- subscriber.channels.each do |channel|
13
- register_one channel, subscriber
14
- end
12
+ queue = subscriber.queue.to_s
13
+ channel = subscriber.channel.to_s
14
+
15
+ @subscriptions[queue] ||= {}
16
+ @subscriptions[queue][channel] ||= {}
17
+ @subscriptions[queue][channel][subscriber.worker] = subscriber
18
+
19
+ @subscribers[subscriber.worker] = subscriber
20
+
21
+ queue_adapter.bind subscriber
15
22
  end
16
23
 
17
24
  def unregister(subscriber)
18
- subscriber.channels.each do |channel|
19
- unregister_one channel, subscriber
25
+ queue = subscriber.queue.to_s
26
+ channel = subscriber.channel.to_s
27
+
28
+ queue_adapter.unbind subscriber
29
+
30
+ if @subscriptions[queue] && @subscriptions[queue][channel]
31
+ @subscriptions[queue][channel].delete subscriber.worker
20
32
  end
33
+
34
+ @subscribers.delete subscriber.worker
21
35
  end
22
36
 
23
37
  def find(channel, queue)
@@ -29,39 +43,7 @@ module Chasqui
29
43
  end
30
44
 
31
45
  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
46
+ @subscribers.key? subscriber.worker
65
47
  end
66
48
  end
67
49
  end
@@ -1,3 +1,3 @@
1
1
  module Chasqui
2
- VERSION = '1.0.0-rc1'
2
+ VERSION = '1.0.0'
3
3
  end
@@ -67,6 +67,7 @@ shared_examples 'pubsub' do |start_workers_method|
67
67
  actual = JSON.parse payload
68
68
  expect(actual['channel']).to eq(expected[:channel])
69
69
  expect(actual['payload']).to eq(expected[:payload])
70
+ expect(actual['payload']).to eq(actual['worker_args'])
70
71
  end
71
72
  end
72
73
  end
@@ -4,4 +4,41 @@ require 'resque'
4
4
  Resque.redis = ENV['REDIS_URL'] if ENV['REDIS_URL']
5
5
  Resque.redis.namespace = :resque
6
6
 
7
- require './spec/integration/setup/subscribers'
7
+ def log_event(worker, event, *args)
8
+ queue = worker.instance_variable_get(:@queue)
9
+ event['worker_args'] = args
10
+ event['class'] = worker.name
11
+
12
+ Chasqui.redis.rpush "#{queue}:event_log", event.to_json
13
+ end
14
+
15
+ class UserSignupWorker
16
+ @queue = 'app1'
17
+
18
+ def self.perform(event, *args)
19
+ log_event self, event, *args
20
+ end
21
+ end
22
+
23
+ class TransactionWorker
24
+ @queue = 'app2'
25
+
26
+ def self.perform(event, *args)
27
+ log_event self, event, *args
28
+ end
29
+ end
30
+
31
+ class UserCancelWorker
32
+ @queue = 'app2'
33
+
34
+ def self.perform(event, *args)
35
+ log_event self, event, *args
36
+ end
37
+ end
38
+
39
+ Chasqui.subscribe do
40
+ on 'user.signup', UserSignupWorker
41
+ on 'user.cancel', UserCancelWorker
42
+ on 'account.credit', TransactionWorker
43
+ on 'account.debit', TransactionWorker
44
+ end