chasqui 1.0.0.pre.rc1 → 1.0.0

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 (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