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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/README.md +90 -78
- data/chasqui.gemspec +1 -1
- data/lib/chasqui.rb +83 -5
- data/lib/chasqui/cli.rb +8 -2
- data/lib/chasqui/config.rb +70 -8
- data/lib/chasqui/{queue_adapters → queue_adapter}/redis_queue_adapter.rb +6 -12
- data/lib/chasqui/subscriber.rb +1 -78
- data/lib/chasqui/subscription_builder.rb +135 -0
- data/lib/chasqui/subscription_builder/resque_subscription_builder.rb +31 -0
- data/lib/chasqui/subscription_builder/sidekiq_subscription_builder.rb +27 -0
- data/lib/chasqui/subscriptions.rb +20 -38
- data/lib/chasqui/version.rb +1 -1
- data/spec/integration/pubsub_examples.rb +1 -0
- data/spec/integration/setup/resque.rb +38 -1
- data/spec/integration/setup/sidekiq.rb +40 -1
- data/spec/lib/chasqui/brokers/redis_broker_spec.rb +12 -10
- data/spec/lib/chasqui/config_spec.rb +3 -3
- data/spec/lib/chasqui/{queue_adapters → queue_adapter}/redis_queue_adapter_spec.rb +6 -19
- data/spec/lib/chasqui/subscriber_spec.rb +4 -114
- data/spec/lib/chasqui/subscription_builder/resque_subscription_builder_spec.rb +20 -0
- data/spec/lib/chasqui/subscription_builder/sidekiq_subscription_builder_spec.rb +27 -0
- data/spec/lib/chasqui/subscription_builder_spec.rb +41 -0
- data/spec/lib/chasqui/subscriptions_spec.rb +4 -4
- data/spec/lib/chasqui_spec.rb +76 -12
- data/spec/support/chasqui_spec_helpers.rb +0 -21
- data/spec/support/fake_worker.rb +2 -0
- data/spec/support/shared_examples/subscription_builder_examples.rb +99 -0
- metadata +22 -16
- data/lib/chasqui/worker.rb +0 -81
- data/spec/integration/setup/subscribers.rb +0 -30
- data/spec/lib/chasqui/worker_spec.rb +0 -96
- data/spec/support/fake_subscriber.rb +0 -13
@@ -1,22 +1,16 @@
|
|
1
1
|
module Chasqui
|
2
|
-
module
|
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
|
-
|
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.
|
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}
|
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
|
|
data/lib/chasqui/subscriber.rb
CHANGED
@@ -1,81 +1,4 @@
|
|
1
1
|
module Chasqui
|
2
|
-
|
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.
|
13
|
-
|
14
|
-
|
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.
|
19
|
-
|
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.
|
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
|
data/lib/chasqui/version.rb
CHANGED
@@ -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
|
-
|
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
|