resque-bus 0.2.3
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.
- data/.gitignore +4 -0
- data/.rbenv-version +1 -0
- data/.rvmrc +2 -0
- data/Gemfile +6 -0
- data/MIT-LICENSE +20 -0
- data/README.mdown +152 -0
- data/Rakefile +3 -0
- data/lib/resque-bus.rb +250 -0
- data/lib/resque_bus/application.rb +110 -0
- data/lib/resque_bus/dispatch.rb +59 -0
- data/lib/resque_bus/driver.rb +28 -0
- data/lib/resque_bus/local.rb +32 -0
- data/lib/resque_bus/matcher.rb +81 -0
- data/lib/resque_bus/publisher.rb +9 -0
- data/lib/resque_bus/rider.rb +52 -0
- data/lib/resque_bus/server/views/bus.erb +101 -0
- data/lib/resque_bus/server.rb +29 -0
- data/lib/resque_bus/subscriber.rb +60 -0
- data/lib/resque_bus/subscription.rb +50 -0
- data/lib/resque_bus/subscription_list.rb +49 -0
- data/lib/resque_bus/task_manager.rb +50 -0
- data/lib/resque_bus/tasks.rb +105 -0
- data/lib/resque_bus/util.rb +42 -0
- data/lib/resque_bus/version.rb +5 -0
- data/lib/tasks/resquebus.rake +2 -0
- data/resque-bus.gemspec +34 -0
- data/spec/application_spec.rb +152 -0
- data/spec/dispatch_spec.rb +76 -0
- data/spec/driver_spec.rb +100 -0
- data/spec/integration_spec.rb +53 -0
- data/spec/matcher_spec.rb +143 -0
- data/spec/publish_at_spec.rb +75 -0
- data/spec/publish_spec.rb +48 -0
- data/spec/redis_spec.rb +13 -0
- data/spec/rider_spec.rb +83 -0
- data/spec/spec_helper.rb +75 -0
- data/spec/subscriber_spec.rb +233 -0
- data/spec/subscription_list_spec.rb +43 -0
- data/spec/subscription_spec.rb +53 -0
- metadata +232 -0
data/.gitignore
ADDED
data/.rbenv-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.9.3-p194
|
data/.rvmrc
ADDED
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2012 Brian Leonard
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.mdown
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
## Resque Bus
|
2
|
+
|
3
|
+
This gem uses Redis and Resque to allow simple asynchronous communication between apps.
|
4
|
+
|
5
|
+
### Example
|
6
|
+
|
7
|
+
Application A can publish an event
|
8
|
+
|
9
|
+
# config
|
10
|
+
ResqueBus.redis = "192.168.1.1:6379"
|
11
|
+
|
12
|
+
# business logic
|
13
|
+
ResqueBus.publish("user_created", "id" => 42, "first_name" => "John", "last_name" => "Smith")
|
14
|
+
|
15
|
+
# or do it later
|
16
|
+
ResqueBus.publish_at(1.hour.from_now, "user_created", "id" => 42, "first_name" => "John", "last_name" => "Smith")
|
17
|
+
|
18
|
+
Application B is subscribed to events
|
19
|
+
|
20
|
+
# config
|
21
|
+
ResqueBus.redis = "192.168.1.1:6379"
|
22
|
+
|
23
|
+
# initializer
|
24
|
+
ResqueBus.dispatch("app_b") do
|
25
|
+
# processes event on app_b_default queue
|
26
|
+
# subscribe is short-hand to subscribe to your 'default' queue and this block with process events with the name "user_created"
|
27
|
+
subscribe "user_created" do |attributes|
|
28
|
+
NameCount.find_or_create_by_name(attributes["last_name"]).increment!
|
29
|
+
end
|
30
|
+
|
31
|
+
# processes event on app_b_critical queue
|
32
|
+
# critical is short-hand to subscribe to your 'critical' queue and this block with process events with the name "user_paid"
|
33
|
+
critical "user_paid" do |attributes|
|
34
|
+
CreditCard.charge!(attributes)
|
35
|
+
end
|
36
|
+
|
37
|
+
# you can pass any queue name you would like to process from as well IE: `banana "peeled" do |attributes|`
|
38
|
+
|
39
|
+
# and regexes work as well. note that with the above configuration along with this regex,
|
40
|
+
# the following as well as the corresponding block above would both be executed
|
41
|
+
subscribe /^user_/ do |attributes|
|
42
|
+
Metrics.record_user_action(attributes["bus_event_type"], attributes["id"])
|
43
|
+
end
|
44
|
+
|
45
|
+
# the above all filter on just the event_type, but you can filter on anything
|
46
|
+
# this would be _any_ event that has a user_id and the page value of homepage regardless of bus_event_type
|
47
|
+
subscribe "my_key", { "user_id" => :present, "page" => "homepage"} do
|
48
|
+
Mixpanel.homepage_action!(attributes["action"])
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
Applications can also subscribe within classes using the provided `Subscriber` module.
|
53
|
+
|
54
|
+
class SimpleSubscriber
|
55
|
+
include ResqueBus::Subscriber
|
56
|
+
subscribe :my_method
|
57
|
+
|
58
|
+
def my_method(attributes)
|
59
|
+
# heavy lifting
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
The following is equivalent to the original initializer and shows more options:
|
64
|
+
|
65
|
+
class OtherSubscriber
|
66
|
+
include ResqueBus::Subscriber
|
67
|
+
application :app_b
|
68
|
+
|
69
|
+
subscribe :user_created
|
70
|
+
subscribe_queue :app_b_critical, :user_paid
|
71
|
+
subscribe_queue :app_b_default, :user_action, :bus_event_type => /^user_/
|
72
|
+
subscribe :homepage_method, :user_id => :present, :page => "homepage"
|
73
|
+
|
74
|
+
def user_created(attributes)
|
75
|
+
NameCount.find_or_create_by_name(attributes["last_name"]).increment!
|
76
|
+
end
|
77
|
+
|
78
|
+
def user_paid(attributes)
|
79
|
+
CreditCard.charge!(attributes)
|
80
|
+
end
|
81
|
+
|
82
|
+
def user_action(attributes)
|
83
|
+
Metrics.record_user_action(attributes["bus_event_type"], attributes["id"])
|
84
|
+
end
|
85
|
+
|
86
|
+
def homepage_method
|
87
|
+
Mixpanel.homepage_action!(attributes["action"])
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
|
92
|
+
The subscription block is run inside a Resque worker which needs to be started for each app.
|
93
|
+
|
94
|
+
$ rake resquebus:setup resque:work
|
95
|
+
|
96
|
+
The incoming queue also needs to be processed on a dedicated or all the app servers.
|
97
|
+
|
98
|
+
$ rake resquebus:driver resque:work
|
99
|
+
|
100
|
+
If you want retry to work for subscribing apps, you should run resque-scheduler
|
101
|
+
|
102
|
+
$ rake resquebus:driver resque:scheduler
|
103
|
+
|
104
|
+
### Compatibility
|
105
|
+
|
106
|
+
ResqueBus can live along side another instance of Resque that points at a different Redis server.
|
107
|
+
|
108
|
+
# config
|
109
|
+
Resque.redis = "192.168.1.0:6379"
|
110
|
+
ResqueBus.redis = "192.168.1.1:6379"
|
111
|
+
|
112
|
+
If no Redis instance is given specifically, ResqueBus will use the Resque one.
|
113
|
+
|
114
|
+
# config
|
115
|
+
Resque.redis = "192.168.1.0:6379"
|
116
|
+
|
117
|
+
That will use the default (resque) namespace which can be helpful for using the tooling. Conflict with queue names are unlikely. You can change the namespace if you like though.
|
118
|
+
|
119
|
+
# config
|
120
|
+
Resque.redis = "192.168.1.0:6379"
|
121
|
+
ResqusBus.redis.namespace = :get_on_the_bus
|
122
|
+
|
123
|
+
|
124
|
+
### Local Mode
|
125
|
+
|
126
|
+
For development, a local mode is also provided and is specified in the
|
127
|
+
configuration.
|
128
|
+
|
129
|
+
# config
|
130
|
+
ResqueBus.local_mode = :standalone
|
131
|
+
or
|
132
|
+
ResqueBus.local_mode = :inline
|
133
|
+
|
134
|
+
Standalone mode does not require a separate resquebus:driver task to be running to process the
|
135
|
+
incoming queue. Simply publishing to the bus will distribute the incoming events
|
136
|
+
to the appropriate application specific queue. A separate resquebus:work task does
|
137
|
+
still need to be run to process these events
|
138
|
+
|
139
|
+
Inline mode skips queue processing entirely and directly dispatches the
|
140
|
+
event to the appropriate code block.
|
141
|
+
|
142
|
+
|
143
|
+
### TODO
|
144
|
+
|
145
|
+
* There are a few spots in the code with TODO notes
|
146
|
+
* Make this not freak out in development without Redis or when Redis is down
|
147
|
+
* We might not actually need to publish in tests
|
148
|
+
* Add some rspec helpers for the apps to use: should_ post an event_publish or something along those lines
|
149
|
+
* A synchronous version will be needed for several use cases. Make it so that an event can go in real-time to one subscriber and still be async to the rest.
|
150
|
+
* Should this use resque-retry or should they jsut go into the failure queue?
|
151
|
+
|
152
|
+
Copyright (c) 2011 Brian Leonard, released under the MIT license
|
data/Rakefile
ADDED
data/lib/resque-bus.rb
ADDED
@@ -0,0 +1,250 @@
|
|
1
|
+
require 'redis/namespace'
|
2
|
+
require 'resque'
|
3
|
+
|
4
|
+
require "resque_bus/version"
|
5
|
+
require 'resque_bus/util'
|
6
|
+
require 'resque_bus/matcher'
|
7
|
+
require 'resque_bus/subscription'
|
8
|
+
require 'resque_bus/subscription_list'
|
9
|
+
require 'resque_bus/subscriber'
|
10
|
+
require 'resque_bus/application'
|
11
|
+
require 'resque_bus/publisher'
|
12
|
+
require 'resque_bus/driver'
|
13
|
+
require 'resque_bus/local'
|
14
|
+
require 'resque_bus/rider'
|
15
|
+
require 'resque_bus/dispatch'
|
16
|
+
require 'resque_bus/task_manager'
|
17
|
+
|
18
|
+
module ResqueBus
|
19
|
+
extend self
|
20
|
+
|
21
|
+
def default_app_key=val
|
22
|
+
@default_app_key = Application.normalize(val)
|
23
|
+
end
|
24
|
+
|
25
|
+
def default_app_key
|
26
|
+
@default_app_key
|
27
|
+
end
|
28
|
+
|
29
|
+
def default_queue=val
|
30
|
+
@default_queue = val
|
31
|
+
end
|
32
|
+
|
33
|
+
def default_queue
|
34
|
+
@default_queue
|
35
|
+
end
|
36
|
+
|
37
|
+
def hostname
|
38
|
+
@hostname ||= `hostname 2>&1`.strip.sub(/.local/,'')
|
39
|
+
end
|
40
|
+
|
41
|
+
def dispatch(app_key=nil, &block)
|
42
|
+
dispatcher = dispatcher_by_key(app_key)
|
43
|
+
dispatcher.instance_eval(&block)
|
44
|
+
dispatcher
|
45
|
+
end
|
46
|
+
|
47
|
+
def dispatchers
|
48
|
+
@dispatchers ||= {}
|
49
|
+
@dispatchers.values
|
50
|
+
end
|
51
|
+
|
52
|
+
def dispatcher_by_key(app_key)
|
53
|
+
app_key = Application.normalize(app_key || default_app_key)
|
54
|
+
@dispatchers ||= {}
|
55
|
+
@dispatchers[app_key] ||= Dispatch.new(app_key)
|
56
|
+
end
|
57
|
+
|
58
|
+
def dispatcher_execute(app_key, key, attributes)
|
59
|
+
@dispatchers ||= {}
|
60
|
+
dispatcher = @dispatchers[app_key]
|
61
|
+
dispatcher.execute(key, attributes) if dispatcher
|
62
|
+
end
|
63
|
+
|
64
|
+
def local_mode=value
|
65
|
+
@local_mode = value
|
66
|
+
end
|
67
|
+
|
68
|
+
def local_mode
|
69
|
+
@local_mode
|
70
|
+
end
|
71
|
+
|
72
|
+
# Accepts:
|
73
|
+
# 1. A 'hostname:port' String
|
74
|
+
# 2. A 'hostname:port:db' String (to select the Redis db)
|
75
|
+
# 3. A 'hostname:port/namespace' String (to set the Redis namespace)
|
76
|
+
# 4. A Redis URL String 'redis://host:port'
|
77
|
+
# 5. An instance of `Redis`, `Redis::Client`, `Redis::DistRedis`,
|
78
|
+
# or `Redis::Namespace`.
|
79
|
+
def redis=(server)
|
80
|
+
case server
|
81
|
+
when String
|
82
|
+
if server =~ /redis\:\/\//
|
83
|
+
redis = Redis.connect(:url => server, :thread_safe => true)
|
84
|
+
else
|
85
|
+
server, namespace = server.split('/', 2)
|
86
|
+
host, port, db = server.split(':')
|
87
|
+
redis = Redis.new(:host => host, :port => port,
|
88
|
+
:thread_safe => true, :db => db)
|
89
|
+
end
|
90
|
+
namespace ||= default_namespace
|
91
|
+
|
92
|
+
@redis = Redis::Namespace.new(namespace, :redis => redis)
|
93
|
+
when Redis::Namespace
|
94
|
+
@redis = server
|
95
|
+
else
|
96
|
+
@redis = Redis::Namespace.new(default_namespace, :redis => server)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Returns the current Redis connection. If none has been created, will
|
101
|
+
# create a new one from the Reqsue one (with a different namespace)
|
102
|
+
def redis
|
103
|
+
return @redis if @redis
|
104
|
+
copy = Resque.redis.clone
|
105
|
+
copy.namespace = default_namespace
|
106
|
+
self.redis = copy
|
107
|
+
self.redis
|
108
|
+
end
|
109
|
+
|
110
|
+
def original_redis=(server)
|
111
|
+
@original_redis = server
|
112
|
+
end
|
113
|
+
def original_redis
|
114
|
+
@original_redis
|
115
|
+
end
|
116
|
+
|
117
|
+
def publish_metadata(event_type, attributes={})
|
118
|
+
# TODO: "bus_app_key" => application.app_key ?
|
119
|
+
bus_attr = {"bus_published_at" => Time.now.to_i, "created_at" => Time.now.to_i, "bus_event_type" => event_type}
|
120
|
+
bus_attr["bus_id"] ||= "#{Time.now.to_i}-#{generate_uuid}"
|
121
|
+
bus_attr["bus_app_hostname"] = hostname
|
122
|
+
bus_attr.merge(attributes || {})
|
123
|
+
end
|
124
|
+
|
125
|
+
def generate_uuid
|
126
|
+
require 'securerandom' unless defined?(SecureRandom)
|
127
|
+
return SecureRandom.uuid
|
128
|
+
|
129
|
+
rescue Exception => e
|
130
|
+
# secure random not there
|
131
|
+
# big random number a few times
|
132
|
+
n_bytes = [42].pack('i').size
|
133
|
+
n_bits = n_bytes * 8
|
134
|
+
max = 2 ** (n_bits - 2) - 1
|
135
|
+
return "#{rand(max)}-#{rand(max)}-#{rand(max)}"
|
136
|
+
end
|
137
|
+
|
138
|
+
def publish(event_type, attributes = {})
|
139
|
+
to_publish = publish_metadata(event_type, attributes)
|
140
|
+
ResqueBus.log_application("Event published: #{event_type} #{to_publish.inspect}")
|
141
|
+
if local_mode
|
142
|
+
ResqueBus::Local.perform(to_publish)
|
143
|
+
else
|
144
|
+
enqueue_to(incoming_queue, Driver, to_publish)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def publish_at(timestamp_or_epoch, event_type, attributes = {})
|
149
|
+
to_publish = publish_metadata(event_type, attributes)
|
150
|
+
to_publish["bus_delayed_until"] ||= timestamp_or_epoch.to_i
|
151
|
+
to_publish.delete("bus_published_at") unless attributes["bus_published_at"] # will be put on when it actually does it
|
152
|
+
|
153
|
+
ResqueBus.log_application("Event published:#{event_type} #{to_publish.inspect} publish_at: #{timestamp_or_epoch.to_i}")
|
154
|
+
item = delayed_job_to_hash_with_queue(incoming_queue, Publisher, [event_type, to_publish])
|
155
|
+
delayed_push(timestamp_or_epoch, item)
|
156
|
+
end
|
157
|
+
|
158
|
+
def enqueue_to(queue, klass, *args)
|
159
|
+
push(queue, :class => klass.to_s, :args => args)
|
160
|
+
end
|
161
|
+
|
162
|
+
def logger
|
163
|
+
@logger
|
164
|
+
end
|
165
|
+
|
166
|
+
def logger=val
|
167
|
+
@logger = val
|
168
|
+
end
|
169
|
+
|
170
|
+
def log_application(message)
|
171
|
+
if logger
|
172
|
+
time = Time.now.strftime('%H:%M:%S %Y-%m-%d')
|
173
|
+
logger.info("** [#{time}] #$$: ResqueBus #{message}")
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def log_worker(message)
|
178
|
+
if ENV['LOGGING'] || ENV['VERBOSE'] || ENV['VVERBOSE']
|
179
|
+
time = Time.now.strftime('%H:%M:%S %Y-%m-%d')
|
180
|
+
puts "** [#{time}] #$$: #{message}"
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
protected
|
185
|
+
|
186
|
+
def reset
|
187
|
+
# used by tests
|
188
|
+
@redis = nil # clear instance of redis
|
189
|
+
@dispatcher = nil
|
190
|
+
@default_app_key = nil
|
191
|
+
@default_queue = nil
|
192
|
+
end
|
193
|
+
|
194
|
+
def incoming_queue
|
195
|
+
"resquebus_incoming"
|
196
|
+
end
|
197
|
+
|
198
|
+
def default_namespace
|
199
|
+
# It might play better on the same server, but overall life is more complicated
|
200
|
+
:resque
|
201
|
+
end
|
202
|
+
|
203
|
+
## From Resque, but using a (possibly) different instance of Redis
|
204
|
+
|
205
|
+
# Pushes a job onto a queue. Queue name should be a string and the
|
206
|
+
# item should be any JSON-able Ruby object.
|
207
|
+
#
|
208
|
+
# Resque works generally expect the `item` to be a hash with the following
|
209
|
+
# keys:
|
210
|
+
#
|
211
|
+
# class - The String name of the job to run.
|
212
|
+
# args - An Array of arguments to pass the job. Usually passed
|
213
|
+
# via `class.to_class.perform(*args)`.
|
214
|
+
#
|
215
|
+
# Example
|
216
|
+
#
|
217
|
+
# Resque.push('archive', :class => 'Archive', :args => [ 35, 'tar' ])
|
218
|
+
#
|
219
|
+
# Returns nothing
|
220
|
+
def push(queue, item)
|
221
|
+
watch_queue(queue)
|
222
|
+
redis.rpush "queue:#{queue}", Resque.encode(item)
|
223
|
+
end
|
224
|
+
|
225
|
+
# Used internally to keep track of which queues we've created.
|
226
|
+
# Don't call this directly.
|
227
|
+
def watch_queue(queue)
|
228
|
+
redis.sadd(:queues, queue.to_s)
|
229
|
+
end
|
230
|
+
|
231
|
+
### From Resque Scheduler
|
232
|
+
# Used internally to stuff the item into the schedule sorted list.
|
233
|
+
# +timestamp+ can be either in seconds or a datetime object
|
234
|
+
# Insertion if O(log(n)).
|
235
|
+
# Returns true if it's the first job to be scheduled at that time, else false
|
236
|
+
def delayed_push(timestamp, item)
|
237
|
+
# First add this item to the list for this timestamp
|
238
|
+
redis.rpush("delayed:#{timestamp.to_i}", Resque.encode(item))
|
239
|
+
|
240
|
+
# Now, add this timestamp to the zsets. The score and the value are
|
241
|
+
# the same since we'll be querying by timestamp, and we don't have
|
242
|
+
# anything else to store.
|
243
|
+
redis.zadd :delayed_queue_schedule, timestamp.to_i, timestamp.to_i
|
244
|
+
end
|
245
|
+
|
246
|
+
def delayed_job_to_hash_with_queue(queue, klass, args)
|
247
|
+
{:class => klass.to_s, :args => args, :queue => queue}
|
248
|
+
end
|
249
|
+
|
250
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module ResqueBus
|
2
|
+
class Application
|
3
|
+
attr_reader :app_key, :redis_key
|
4
|
+
|
5
|
+
def self.all
|
6
|
+
# note the names arent the same as we started with
|
7
|
+
ResqueBus.redis.smembers(app_list_key).collect{ |val| new(val) }
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(app_key)
|
11
|
+
@app_key = self.class.normalize(app_key)
|
12
|
+
@redis_key = "#{self.class.app_single_key}:#{@app_key}"
|
13
|
+
# raise error if only other chars
|
14
|
+
raise "Invalid application name" if @app_key.gsub("_", "").size == 0
|
15
|
+
end
|
16
|
+
|
17
|
+
def subscribe(subscription_list, log = false)
|
18
|
+
@subscriptions = nil
|
19
|
+
|
20
|
+
if subscription_list == nil || subscription_list.size == 0
|
21
|
+
unsubscribe
|
22
|
+
return true
|
23
|
+
end
|
24
|
+
|
25
|
+
temp_key = "temp_#{redis_key}:#{rand(999999999)}"
|
26
|
+
|
27
|
+
redis_hash = subscription_list.to_redis
|
28
|
+
redis_hash.each do |key, hash|
|
29
|
+
ResqueBus.redis.hset(temp_key, key, Resque.encode(hash))
|
30
|
+
end
|
31
|
+
|
32
|
+
# make it the real one
|
33
|
+
ResqueBus.redis.rename(temp_key, redis_key)
|
34
|
+
ResqueBus.redis.sadd(self.class.app_list_key, app_key)
|
35
|
+
|
36
|
+
if log
|
37
|
+
puts ResqueBus.redis.hgetall(redis_key).inspect
|
38
|
+
end
|
39
|
+
|
40
|
+
true
|
41
|
+
end
|
42
|
+
|
43
|
+
def unsubscribe
|
44
|
+
# TODO: clean up known queues?
|
45
|
+
ResqueBus.redis.srem(self.class.app_list_key, app_key)
|
46
|
+
ResqueBus.redis.del(redis_key)
|
47
|
+
end
|
48
|
+
|
49
|
+
def no_connect_queue_names_for(subscriptions)
|
50
|
+
out = []
|
51
|
+
subscriptions.all.each do |sub|
|
52
|
+
queue = "#{app_key}_#{sub.queue_name}"
|
53
|
+
out << queue
|
54
|
+
end
|
55
|
+
out << "#{app_key}_default"
|
56
|
+
out.uniq
|
57
|
+
end
|
58
|
+
|
59
|
+
def subscription_matches(attributes)
|
60
|
+
out = subscriptions.matches(attributes)
|
61
|
+
out.each do |sub|
|
62
|
+
sub.app_key = self.app_key
|
63
|
+
end
|
64
|
+
out
|
65
|
+
end
|
66
|
+
|
67
|
+
def event_display_tuples
|
68
|
+
out = []
|
69
|
+
subscriptions.all.each do |sub|
|
70
|
+
out << [sub.event_name, sub.queue_name]
|
71
|
+
end
|
72
|
+
out
|
73
|
+
end
|
74
|
+
|
75
|
+
protected
|
76
|
+
|
77
|
+
def self.normalize(val)
|
78
|
+
val.to_s.gsub(/\W/, "_").downcase
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.app_list_key
|
82
|
+
"resquebus_apps"
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.app_single_key
|
86
|
+
"resquebus_app"
|
87
|
+
end
|
88
|
+
|
89
|
+
def event_queues
|
90
|
+
ResqueBus.redis.hgetall(redis_key)
|
91
|
+
end
|
92
|
+
|
93
|
+
def subscriptions
|
94
|
+
@subscriptions ||= SubscriptionList.from_redis(read_redis_hash)
|
95
|
+
end
|
96
|
+
|
97
|
+
def read_redis_hash
|
98
|
+
out = {}
|
99
|
+
ResqueBus.redis.hgetall(redis_key).each do |key, val|
|
100
|
+
begin
|
101
|
+
out[key] = Resque.decode(val)
|
102
|
+
rescue Resque::Helpers::DecodeException
|
103
|
+
out[key] = val
|
104
|
+
end
|
105
|
+
end
|
106
|
+
out
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# Creates a DSL for apps to define their blocks to run for event_types
|
2
|
+
|
3
|
+
module ResqueBus
|
4
|
+
class Dispatch
|
5
|
+
attr_reader :app_key, :subscriptions
|
6
|
+
def initialize(app_key)
|
7
|
+
@app_key = Application.normalize(app_key)
|
8
|
+
@subscriptions = SubscriptionList.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def size
|
12
|
+
@subscriptions.size
|
13
|
+
end
|
14
|
+
|
15
|
+
def subscribe(key, matcher_hash = nil, &block)
|
16
|
+
dispatch_event("default", key, matcher_hash, block)
|
17
|
+
end
|
18
|
+
|
19
|
+
# allows definitions of other queues
|
20
|
+
def method_missing(method_name, *args, &block)
|
21
|
+
if args.size == 1 and block
|
22
|
+
dispatch_event(method_name, args[0], nil, block)
|
23
|
+
elsif args.size == 2 and block
|
24
|
+
dispatch_event(method_name, args[0], args[1], block)
|
25
|
+
else
|
26
|
+
super
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def execute(key, attributes)
|
31
|
+
sub = subscriptions.key(key)
|
32
|
+
if sub
|
33
|
+
sub.execute!(attributes)
|
34
|
+
else
|
35
|
+
# TODO: log that it's not there
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def subscription_matches(attributes)
|
40
|
+
out = subscriptions.matches(attributes)
|
41
|
+
out.each do |sub|
|
42
|
+
sub.app_key = self.app_key
|
43
|
+
end
|
44
|
+
out
|
45
|
+
end
|
46
|
+
|
47
|
+
def dispatch_event(queue, key, matcher_hash, block)
|
48
|
+
# if not matcher_hash, assume key is a event_type regex
|
49
|
+
matcher_hash ||= { "bus_event_type" => key }
|
50
|
+
add_subscription("#{app_key}_#{queue}", key, "::ResqueBus::Rider", matcher_hash, block)
|
51
|
+
end
|
52
|
+
|
53
|
+
def add_subscription(queue_name, key, class_name, matcher_hash = nil, block)
|
54
|
+
sub = Subscription.register(queue_name, key, class_name, matcher_hash, block)
|
55
|
+
subscriptions.add(sub)
|
56
|
+
sub
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module ResqueBus
|
2
|
+
# fans out an event to multiple queues
|
3
|
+
class Driver
|
4
|
+
|
5
|
+
def self.subscription_matches(attributes)
|
6
|
+
out = []
|
7
|
+
Application.all.each do |app|
|
8
|
+
subs = app.subscription_matches(attributes)
|
9
|
+
out.concat(subs)
|
10
|
+
end
|
11
|
+
out
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.perform(attributes={})
|
15
|
+
raise "No attribiutes passed" if attributes.empty?
|
16
|
+
|
17
|
+
ResqueBus.log_worker("Driver running: #{attributes.inspect}")
|
18
|
+
|
19
|
+
subscription_matches(attributes).each do |sub|
|
20
|
+
ResqueBus.log_worker(" ...sending to #{sub.queue_name} queue with class #{sub.class_name} for app #{sub.app_key} because of subscription: #{sub.key}")
|
21
|
+
|
22
|
+
bus_attr = {"bus_driven_at" => Time.now.to_i, "bus_rider_queue" => sub.queue_name, "bus_rider_app_key" => sub.app_key, "bus_rider_sub_key" => sub.key, "bus_rider_class_name" => sub.class_name}
|
23
|
+
ResqueBus.enqueue_to(sub.queue_name, sub.class_name, bus_attr.merge(attributes || {}))
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module ResqueBus
|
2
|
+
# only process local queues
|
3
|
+
class Local
|
4
|
+
|
5
|
+
def self.perform(attributes = {})
|
6
|
+
ResqueBus.log_worker("Local running: #{attributes.inspect}")
|
7
|
+
|
8
|
+
# looking for subscriptions, not queues
|
9
|
+
subscription_matches(attributes).each do |sub|
|
10
|
+
bus_attr = {"bus_driven_at" => Time.now.to_i, "bus_rider_queue" => sub.queue_name, "bus_rider_app_key" => sub.app_key, "bus_rider_sub_key" => sub.key, "bus_rider_class_name" => sub.class_name}
|
11
|
+
to_publish = bus_attr.merge(attributes || {})
|
12
|
+
if ResqueBus.local_mode == :standalone
|
13
|
+
ResqueBus.enqueue_to(sub.queue_name, sub.class_name, bus_attr.merge(attributes || {}))
|
14
|
+
# defaults to inline mode
|
15
|
+
else ResqueBus.local_mode == :inline
|
16
|
+
sub.execute!(to_publish)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# looking directly at subscriptions loaded into dispatcher
|
22
|
+
# so we don't need redis server up
|
23
|
+
def self.subscription_matches(attributes)
|
24
|
+
out = []
|
25
|
+
ResqueBus.dispatchers.each do |dispatcher|
|
26
|
+
out.concat(dispatcher.subscription_matches(attributes))
|
27
|
+
end
|
28
|
+
out
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|