resque-fanout 0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +21 -0
- data/README.md +110 -0
- data/Rakefile +9 -0
- data/lib/resque-fanout/fanout_server.rb +54 -0
- data/lib/resque-fanout/job_extension.rb +17 -0
- data/lib/resque-fanout/resque_extension.rb +57 -0
- data/lib/resque-fanout/views/index.erb +98 -0
- data/lib/resque-fanout.rb +8 -0
- metadata +120 -0
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
Copyright (c) Frederik Fix
|
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.
|
21
|
+
|
data/README.md
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
Resque Plugin for pubsub routing between queues
|
2
|
+
===============================================
|
3
|
+
|
4
|
+
This plugin allows you to define "exchanges" which look like regular Resque queues, but rather than processing jobs they distribute them to one or more other queues. The mapping between exchanges and queues can change at runtime. A code based configuration method as well as a web frontend are provided.
|
5
|
+
|
6
|
+
This is useful for loosely coupled asynchronous communication between multiple applications.
|
7
|
+
|
8
|
+
|
9
|
+
Consider a system consisting of three applications. The main frontend application, an internal one processing billing and another handling user accounts. When a new user registers work needs to be performed in the last two.
|
10
|
+
|
11
|
+
|
12
|
+
In the billing application define the following worker:
|
13
|
+
|
14
|
+
``` ruby
|
15
|
+
class BillingListener
|
16
|
+
|
17
|
+
@queue = :payroll
|
18
|
+
|
19
|
+
def self.perform(user_hash)
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
Resque.subscribe :new_user, :class => BillingListener
|
26
|
+
```
|
27
|
+
|
28
|
+
In the user accounts application define this worker:
|
29
|
+
|
30
|
+
``` ruby
|
31
|
+
class AccountListener
|
32
|
+
|
33
|
+
@queue = :accounts
|
34
|
+
|
35
|
+
def self.perform(user_hash)
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
Resque.subscribe :new_user, :class => AccountListener
|
42
|
+
```
|
43
|
+
|
44
|
+
When a new user registers in the frontend application execute:
|
45
|
+
|
46
|
+
``` ruby
|
47
|
+
Resque.publish :new_user, :user_name => :feynman, :account_type => :qed
|
48
|
+
```
|
49
|
+
|
50
|
+
The job will then be processed by both the BillingListener and the AccountListener in the context of the respective applications.
|
51
|
+
|
52
|
+
|
53
|
+
PubSub semantics
|
54
|
+
================
|
55
|
+
|
56
|
+
The mapping between exchange and queues is maintained in the Redis server and can therefore change at runtime. The job distribution is performed upon job submission. Exchanges override queues with the same name.
|
57
|
+
|
58
|
+
The mapping is written to Redis by the following call:
|
59
|
+
|
60
|
+
``` ruby
|
61
|
+
Resque.subscribe :new_user, :class => AccountListener
|
62
|
+
```
|
63
|
+
|
64
|
+
This call will do two things
|
65
|
+
|
66
|
+
1. Add the queue defined for AccountListener to the outbound list for the exchange "new_user"
|
67
|
+
2. Set the class AccountListener as the class handling the request on the queue defined in step 1
|
68
|
+
|
69
|
+
The second step is necessary because the class handling the jobs sent to the exchange "new_user" might be different in the respective receiving applications. The receiving class is set by setting the "class" attribute in the Job definition when it is pushed to the queue.
|
70
|
+
|
71
|
+
The above semantics allow you to reroute an existing queue. Consider the above example but the frontend application also defines the following worker:
|
72
|
+
|
73
|
+
``` ruby
|
74
|
+
class NewUserListener
|
75
|
+
|
76
|
+
@queue = :new_user
|
77
|
+
|
78
|
+
def self.perform(user_hash)
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
Resque.subscribe :new_user, :class => NewUserListener
|
85
|
+
```
|
86
|
+
|
87
|
+
If you then do the following in the frontend application,
|
88
|
+
|
89
|
+
``` ruby
|
90
|
+
Resque.enqueue(NewUserListener, :user_name => :feynman, :account_type => :qed)
|
91
|
+
```
|
92
|
+
|
93
|
+
the job will be performed by all three workers in their respective application contexts.
|
94
|
+
|
95
|
+
|
96
|
+
The `Resque.subscribe` method provides another mode which does not set the handling class:
|
97
|
+
|
98
|
+
``` ruby
|
99
|
+
Resque.subscribe :new_user, :queue => :accounts
|
100
|
+
```
|
101
|
+
|
102
|
+
This will of course only work if the receiving applications have a worker called `NewUserListener`. Using `Resque.publish` with this exchange will cause an exception.
|
103
|
+
|
104
|
+
|
105
|
+
Managing the Exchange -> Queue mapping
|
106
|
+
======================================
|
107
|
+
|
108
|
+
The mapping can be defined either by the `Resque.subscribe` method or through the web frontend. A relevant mapping is created by the `Resque.subscribe` call. Therefore if you delete a mapping in the web frontend but rerun `Resque.subscribe` it will reappear. Depending on your circumstances this may or may not be what you expect. It is recommended to place your mapping definitions in a Rake task similar to `rake db:migrate`
|
109
|
+
|
110
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
module Resque
|
2
|
+
module FanoutServer
|
3
|
+
|
4
|
+
VIEW_PATH = File.join(File.dirname(__FILE__), 'views')
|
5
|
+
|
6
|
+
def self.registered(app)
|
7
|
+
# index
|
8
|
+
app.get "/fanout/?" do
|
9
|
+
@exchanges = Resque.exchanges
|
10
|
+
@queues = Resque.queues
|
11
|
+
erb(File.read(File.join(::Resque::FanoutServer::VIEW_PATH, "index.erb")))
|
12
|
+
end
|
13
|
+
|
14
|
+
# create new mapping
|
15
|
+
app.post '/fanout/?' do
|
16
|
+
if valid_mapping? params
|
17
|
+
Resque.subscribe params[:exchange], :queue => params[:queue], :class => params[:class]
|
18
|
+
redirect u(:fanout, "?notice=#{URI.escape 'Mapping created'}")
|
19
|
+
else
|
20
|
+
redirect u(:fanout, "?error=#{URI.escape 'Invalid Mapping'}")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# delete mapping
|
25
|
+
app.post '/fanout/:exchange/:queue/remove' do
|
26
|
+
if valid_mapping? params
|
27
|
+
Resque.unsubscribe params[:exchange], :queue => params[:queue]
|
28
|
+
redirect u(:fanout, "?notice=#{URI.escape 'Mapping removed'}")
|
29
|
+
else
|
30
|
+
redirect u(:fanout, "?error=#{URI.escape 'Invalid Parameters'}")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
app.helpers do
|
35
|
+
def valid_mapping?(params)
|
36
|
+
params[:exchange].to_s != "" and params[:queue].to_s != ""
|
37
|
+
end
|
38
|
+
|
39
|
+
def display_flash
|
40
|
+
[:error, :notice].map do |field|
|
41
|
+
if params[field].to_s != ""
|
42
|
+
"<p class='flash #{field}'>#{params[field]}</p>"
|
43
|
+
end
|
44
|
+
end.compact.join
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
app.tabs << "Fanout"
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
Resque::Server.register Resque::FanoutServer
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class Resque::Job
|
2
|
+
|
3
|
+
def self.create(queue, klass, *args)
|
4
|
+
Resque.validate(klass, queue)
|
5
|
+
|
6
|
+
if Resque.inline?
|
7
|
+
constantize(klass).perform(*decode(encode(args)))
|
8
|
+
elsif queues = Resque.queues_for(queue)
|
9
|
+
queues.each do |_queue|
|
10
|
+
Resque.push(_queue[:queue], :class => (_queue[:class] || klass.to_s), :args => args)
|
11
|
+
end
|
12
|
+
else
|
13
|
+
Resque.push(queue, :class => klass.to_s, :args => args)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module ResqueExtension
|
2
|
+
|
3
|
+
def self.apply
|
4
|
+
Resque.extend ResqueExtension
|
5
|
+
end
|
6
|
+
|
7
|
+
|
8
|
+
def subscribe(exchange, options={})
|
9
|
+
raise ArgumentError, "either class or queue param must be supplier" unless options[:queue] || options[:class]
|
10
|
+
|
11
|
+
queue = options[:queue] || queue_from_class(options[:class])
|
12
|
+
redis.sadd("exchanges:#{exchange.to_s}", queue.to_s)
|
13
|
+
redis.hset("exchanges:class:#{exchange.to_s}", queue.to_s, options[:class].to_s) if options[:class]
|
14
|
+
redis.sadd(:exchanges, exchange.to_s)
|
15
|
+
end
|
16
|
+
|
17
|
+
def unsubscribe(exchange, options={})
|
18
|
+
raise ArgumentError, "either class or queue param must be supplier" unless options[:queue] || options[:class]
|
19
|
+
|
20
|
+
queue = options[:queue] || queue_from_class(options[:class])
|
21
|
+
redis.srem("exchanges:#{exchange.to_s}", queue.to_s)
|
22
|
+
redis.hdel("exchanges:class:#{exchange.to_s}", queue.to_s)
|
23
|
+
if redis.scard("exchanges:#{exchange.to_s}") == 0
|
24
|
+
redis.srem(:exchanges, exchange.to_s)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def queues_for(exchange)
|
29
|
+
if redis.sismember(:exchanges, exchange.to_s)
|
30
|
+
klasses = Hash[redis.hgetall("exchanges:class:#{exchange.to_s}")]
|
31
|
+
redis.smembers("exchanges:#{exchange.to_s}").map do |queue|
|
32
|
+
res = { :queue => queue }
|
33
|
+
res[:class] = klasses[queue] if klasses[queue]
|
34
|
+
res
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def exchanges
|
40
|
+
redis.smembers(:exchanges).map do |exchange|
|
41
|
+
{
|
42
|
+
:exchange => exchange,
|
43
|
+
:queues => queues_for(exchange)
|
44
|
+
}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
def publish(exchange, *args)
|
50
|
+
(queues_for(exchange) || []).each do |queue|
|
51
|
+
push queue[:queue], :class => queue[:class], :args => args
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
ResqueExtension.apply
|
@@ -0,0 +1,98 @@
|
|
1
|
+
<style type="text/css" media="screen">
|
2
|
+
.flash {
|
3
|
+
font-size: 190%;
|
4
|
+
font-weight: bold;
|
5
|
+
text-align: center;
|
6
|
+
padding: 20px;
|
7
|
+
}
|
8
|
+
|
9
|
+
.flash.notice {
|
10
|
+
background: #61BF55;
|
11
|
+
}
|
12
|
+
|
13
|
+
.flash.error {
|
14
|
+
background: #E47E74;
|
15
|
+
}
|
16
|
+
|
17
|
+
#main form.fanout-remove {
|
18
|
+
float: left;
|
19
|
+
margin-left: 20px;
|
20
|
+
margin-top: -5px;
|
21
|
+
}
|
22
|
+
.mapping span {
|
23
|
+
float: left;
|
24
|
+
}
|
25
|
+
|
26
|
+
#main #new-mapping {
|
27
|
+
width: 100%;
|
28
|
+
margin-top: 10px;
|
29
|
+
}
|
30
|
+
|
31
|
+
#new-mapping p {
|
32
|
+
clear: left;
|
33
|
+
}
|
34
|
+
|
35
|
+
#new-mapping label {
|
36
|
+
width: 15em;
|
37
|
+
display: block;
|
38
|
+
float: left;
|
39
|
+
}
|
40
|
+
</style>
|
41
|
+
|
42
|
+
<%= display_flash %>
|
43
|
+
|
44
|
+
<h1 class='wi'>Fanout Exchanges</h1>
|
45
|
+
|
46
|
+
<% if @exchanges.empty? %>
|
47
|
+
<p class='intro'>There are no exchanges defined</p>
|
48
|
+
<% else %>
|
49
|
+
<p class='intro'>There are <%= @exchanges.size %> exchanges defined</p>
|
50
|
+
|
51
|
+
<table>
|
52
|
+
<tr>
|
53
|
+
<th>Exchange</th>
|
54
|
+
<th>Queues</th>
|
55
|
+
</tr>
|
56
|
+
<% for exchange in @exchanges %>
|
57
|
+
<tr>
|
58
|
+
<td><%= exchange[:exchange] %></td>
|
59
|
+
<td>
|
60
|
+
<ul>
|
61
|
+
<% for queue in exchange[:queues] %>
|
62
|
+
<li class="mapping">
|
63
|
+
<span><%= queue[:queue] %><%= "(=> #{queue[:class]})" if queue[:class] %></span>
|
64
|
+
<form method="POST" action="<%= u(:fanout) %>/<%= exchange[:exchange] %>/<%= queue[:queue] %>/remove" class='fanout-remove'>
|
65
|
+
<input type='submit' name='' value='Remove Mapping' onclick='return confirm("Are you absolutely sure? This cannot be undone.");' />
|
66
|
+
</form>
|
67
|
+
</li>
|
68
|
+
<% end %>
|
69
|
+
</ul>
|
70
|
+
</td>
|
71
|
+
</tr>
|
72
|
+
<% end %>
|
73
|
+
</table>
|
74
|
+
<% end %>
|
75
|
+
|
76
|
+
<h2>Add new mapping</h2>
|
77
|
+
|
78
|
+
<form method="POST" action="<%= u(:pubsub) %>" id='new-mapping'>
|
79
|
+
<p>
|
80
|
+
<label>Exchange Name</label>
|
81
|
+
<input type="text" name="exchange">
|
82
|
+
</p>
|
83
|
+
<p>
|
84
|
+
<label>Queue Name</label>
|
85
|
+
<select name="queue">
|
86
|
+
<% for queue in @queues %>
|
87
|
+
<option><%= queue[:name] %></option>
|
88
|
+
<% end %>
|
89
|
+
</select>
|
90
|
+
</p>
|
91
|
+
<p>
|
92
|
+
<label>Class Name (optional)</label>
|
93
|
+
<input type="text" name="class">
|
94
|
+
</p>
|
95
|
+
<input type='submit' name='' value='Create Mapping' />
|
96
|
+
</form>
|
97
|
+
|
98
|
+
|
metadata
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: resque-fanout
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.5'
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Frederik Fix
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-04-04 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 2.3.0
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 2.3.0
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: bundler
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 1.0.0
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 1.0.0
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: redis
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 2.0.1
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 2.0.1
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: resque
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 1.19.0
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 1.19.0
|
78
|
+
description: ! 'A Resque plugin that provides endpoints which distributes Jobs submitted
|
79
|
+
to them to (multiple) subscribing queues. Useful for loosely coupled inter-application
|
80
|
+
communication.
|
81
|
+
|
82
|
+
'
|
83
|
+
email: ich@derfred.com
|
84
|
+
executables: []
|
85
|
+
extensions: []
|
86
|
+
extra_rdoc_files: []
|
87
|
+
files:
|
88
|
+
- README.md
|
89
|
+
- Rakefile
|
90
|
+
- LICENSE
|
91
|
+
- lib/resque-fanout/fanout_server.rb
|
92
|
+
- lib/resque-fanout/job_extension.rb
|
93
|
+
- lib/resque-fanout/resque_extension.rb
|
94
|
+
- lib/resque-fanout/views/index.erb
|
95
|
+
- lib/resque-fanout.rb
|
96
|
+
homepage: http://github.com/derfred/resque-fanout
|
97
|
+
licenses: []
|
98
|
+
post_install_message:
|
99
|
+
rdoc_options: []
|
100
|
+
require_paths:
|
101
|
+
- lib
|
102
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
103
|
+
none: false
|
104
|
+
requirements:
|
105
|
+
- - ! '>='
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: '0'
|
108
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
109
|
+
none: false
|
110
|
+
requirements:
|
111
|
+
- - ! '>='
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
version: '0'
|
114
|
+
requirements: []
|
115
|
+
rubyforge_project:
|
116
|
+
rubygems_version: 1.8.24
|
117
|
+
signing_key:
|
118
|
+
specification_version: 3
|
119
|
+
summary: Resque Plugin for fanout routing to multiple queues
|
120
|
+
test_files: []
|