resque-bus 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,81 @@
1
+ module ResqueBus
2
+ class Matcher
3
+ SPECIAL_PREPEND = "bus_special_value_"
4
+ attr_reader :filters
5
+ def initialize(hash)
6
+ @filters = encode(hash)
7
+ end
8
+
9
+ def to_redis
10
+ @filters
11
+ end
12
+
13
+ def match?(attribute_name, attributes)
14
+ mine = filters[attribute_name].to_s
15
+ return false if mine.size == 0
16
+
17
+ given = attributes[attribute_name]
18
+ case mine
19
+ when "#{SPECIAL_PREPEND}key"
20
+ return true if attributes.has_key?(attribute_name)
21
+ return false
22
+ when "#{SPECIAL_PREPEND}blank"
23
+ return true if given.to_s.strip.size == 0
24
+ return false
25
+ when "#{SPECIAL_PREPEND}empty"
26
+ return false if given == nil
27
+ return true if given.to_s.size == 0
28
+ return false
29
+ when "#{SPECIAL_PREPEND}nil"
30
+ return true if given == nil
31
+ return false
32
+ when "#{SPECIAL_PREPEND}value"
33
+ return false if given == nil
34
+ return true
35
+ when "#{SPECIAL_PREPEND}present"
36
+ return true if given.to_s.strip.size > 0
37
+ return false
38
+ end
39
+
40
+ given = given.to_s
41
+
42
+ return true if mine == given
43
+ begin
44
+ # if it's already a regex, don't mess with it
45
+ # otherwise, it should have start and end line situation
46
+ if mine[0..6] == "(?-mix:"
47
+ regex = Regexp.new(mine)
48
+ else
49
+ regex = Regexp.new("^#{mine}$")
50
+ end
51
+ return !!regex.match(given)
52
+ rescue
53
+ return false
54
+ end
55
+ end
56
+
57
+ def matches?(attributes)
58
+ return false if filters.empty?
59
+ return false if attributes == nil
60
+
61
+ filters.keys.each do |key|
62
+ return false unless match?(key, attributes)
63
+ end
64
+
65
+ true
66
+ end
67
+
68
+ def encode(hash)
69
+ out = {}
70
+ hash.each do |key, value|
71
+ case value
72
+ when :key, :blank, :nil, :present, :empty, :value
73
+ value = "#{SPECIAL_PREPEND}#{value}"
74
+ end
75
+ out[key.to_s] = value.to_s
76
+ end
77
+ out
78
+ end
79
+ end
80
+ end
81
+
@@ -0,0 +1,9 @@
1
+ module ResqueBus
2
+ # publishes on a delay
3
+ class Publisher
4
+ def self.perform(event_type, attributes = {})
5
+ ResqueBus.log_worker("Publisher running: #{event_type} - #{attributes.inspect}")
6
+ ResqueBus.publish(event_type, attributes)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,52 @@
1
+ require 'resque-retry'
2
+
3
+ module ResqueBus
4
+ # queue'd in each
5
+ class Rider
6
+ extend Resque::Plugins::ExponentialBackoff
7
+
8
+ def self.perform(attributes = {})
9
+ sub_key = attributes["bus_rider_sub_key"]
10
+ app_key = attributes["bus_rider_app_key"]
11
+ raise "No application key passed" if app_key.to_s == ""
12
+ raise "No subcription key passed" if sub_key.to_s == ""
13
+
14
+ attributes ||= {}
15
+
16
+ ResqueBus.log_worker("Rider received: #{app_key} #{sub_key} #{attributes.inspect}")
17
+
18
+ # attributes that should be available
19
+ # attributes["bus_event_type"]
20
+ # attributes["bus_app_key"]
21
+ # attributes["bus_published_at"]
22
+ # attributes["bus_driven_at"]
23
+
24
+ # allow the real Reqsue to be used inside the callback while in a worker
25
+ Resque.redis = ResqueBus.original_redis if ResqueBus.original_redis
26
+
27
+ # (now running with the real app that subscribed)
28
+ ResqueBus.dispatcher_execute(app_key, sub_key, attributes.merge("bus_executed_at" => Time.now.to_i))
29
+ ensure
30
+ # put this back if running in the thread
31
+ Resque.redis = ResqueBus.redis if ResqueBus.original_redis
32
+ end
33
+
34
+ # @failure_hooks_already_ran on https://github.com/defunkt/resque/tree/1-x-stable
35
+ # to prevent running twice
36
+ def self.queue
37
+ @my_queue
38
+ end
39
+
40
+ def self.on_failure_aaa(exception, *args)
41
+ # note: sorted alphabetically
42
+ # queue needs to be set for rety to work (know what queue in Requeue.class_to_queue)
43
+ @my_queue = args[0]["bus_rider_queue"]
44
+ end
45
+
46
+ def self.on_failure_zzz(exception, *args)
47
+ # note: sorted alphabetically
48
+ @my_queue = nil
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,101 @@
1
+
2
+ <script LANGUAGE="JavaScript">
3
+ <!--
4
+ function confirmSubmit()
5
+ {
6
+ var agree=confirm("Are you sure you wish to continue?");
7
+ if (agree)
8
+ return true ;
9
+ else
10
+ return false ;
11
+ }
12
+ // -->
13
+ </script>
14
+
15
+ <%
16
+ app_hash = {}
17
+ event_hash = {}
18
+
19
+ # collect each differently
20
+ ResqueBus::Application.all.each do |app|
21
+ app_key = app.app_key
22
+
23
+ app_hash[app_key] ||= []
24
+ app.event_display_tuples.each do |tuple|
25
+ event, queue = tuple
26
+ app_hash[app_key] << [event, queue]
27
+
28
+ event_hash[event] ||= []
29
+ event_hash[event] << [app_key, queue]
30
+ end
31
+ end
32
+
33
+ # sort each list item by secondary label
34
+ event_hash.each do |_, array|
35
+ array.sort!{ |a,b| a.first <=> b.first }
36
+ end
37
+ event_hash.each do |_, array|
38
+ array.sort!{ |a,b| a.first <=> b.first }
39
+ end
40
+
41
+ # helper to display either
42
+ def display_row(name, val, button=nil, first=false)
43
+ form = ""
44
+ if button
45
+ text, url = button
46
+ form = "<form method='POST' action='#{u url}' style='float:left; padding:0 5px 0 0;margin:0;'><input type='submit' name='' value='#{text}' style='padding:0;margin:0;' onclick=\"return confirmSubmit();\"/><input type='hidden' name='name' value='#{h(name)}' /></form>"
47
+ end
48
+
49
+ if !val
50
+ out = "<td>&nbsp;</td><td>&nbsp;</td>"
51
+ else
52
+ detail, queue = val
53
+ out = "<td>#{h(detail)}</td><td><a href='#{u("queues/#{queue}")}'>#{h(queue)}</a></td>"
54
+ end
55
+
56
+ if first
57
+ "<tr><td>#{h(name)}#{form}</td>#{out}</tr>\n"
58
+ else
59
+ "<tr><td>&nbsp;</td>#{out}</tr>\n"
60
+ end
61
+ end
62
+
63
+ def output_hash(hash, action=nil)
64
+ out = ""
65
+ hash.keys.sort.each do |item|
66
+ display = hash[item]
67
+ first = display.shift
68
+ out << display_row(item, first, action, true)
69
+ display.each do |val|
70
+ out << display_row(item, val, action)
71
+ end
72
+ end
73
+ out
74
+ end
75
+ %>
76
+
77
+
78
+
79
+ <h1 class='wi'>Applications</h1>
80
+ <p class='intro'>The apps below have registered the given event types and queues.</p>
81
+ <table class='queues'>
82
+ <tr>
83
+ <th>App Key</th>
84
+ <th>Event Type</th>
85
+ <th>Queue</th>
86
+ </tr>
87
+ <%= output_hash(app_hash, ["Unsubscribe", "bus/unsubscribe"]) %>
88
+ </table>
89
+
90
+ <p>&nbsp;</p>
91
+
92
+ <h1 class='wi'>Events</h1>
93
+ <p class='intro'>The event types below have been registered by the given applications and queues.</p>
94
+ <table class='queues'>
95
+ <tr>
96
+ <th>Event Type</th>
97
+ <th>App Key</th>
98
+ <th>Queue</th>
99
+ </tr>
100
+ <%= output_hash(event_hash, false) %>
101
+ </table>
@@ -0,0 +1,29 @@
1
+ require 'resque-bus'
2
+ require 'resque/server'
3
+
4
+ # Extend Resque::Server to add tabs.
5
+ module ResqueBus
6
+ module Server
7
+
8
+ def self.included(base)
9
+ base.class_eval {
10
+
11
+ get "/bus" do
12
+ erb File.read(File.join(File.dirname(__FILE__), "server/views/bus.erb"))
13
+ end
14
+
15
+
16
+ post '/bus/unsubscribe' do
17
+ app = Application.new(params[:name]).unsubscribe
18
+ redirect u('bus')
19
+ end
20
+
21
+ }
22
+ end
23
+ end
24
+ end
25
+
26
+ Resque::Server.tabs << 'Bus'
27
+ Resque::Server.class_eval do
28
+ include ResqueBus::Server
29
+ end
@@ -0,0 +1,60 @@
1
+ module ResqueBus
2
+ module Subscriber
3
+
4
+ def self.included(base)
5
+ base.extend ClassMethods
6
+ end
7
+
8
+ module ClassMethods
9
+
10
+ def application(app_key)
11
+ @app_key = ::ResqueBus::Application.normalize(app_key)
12
+ end
13
+
14
+ def app_key
15
+ return @app_key if @app_key
16
+ @app_key = ::ResqueBus.default_app_key
17
+ return @app_key if @app_key
18
+ # module or class_name
19
+ val = self.name.to_s.split("::").first
20
+ @app_key = ::ResqueBus::Util.underscore(val)
21
+ end
22
+
23
+ def subscribe(method_name, matcher_hash = nil)
24
+ queue_name = ::Resque.queue_from_class(self)
25
+ queue_name ||= ::ResqueBus.default_queue
26
+ queue_name ||= "#{app_key}_default"
27
+ subscribe_queue(queue_name, method_name, matcher_hash)
28
+ end
29
+
30
+ def subscribe_queue(queue_name, method_name, matcher_hash = nil)
31
+ klass = self
32
+ matcher_hash ||= {"bus_event_type" => method_name}
33
+ sub_key = "#{self.name}.#{method_name}"
34
+ dispatcher = ::ResqueBus.dispatcher_by_key(app_key)
35
+ dispatcher.add_subscription(queue_name, sub_key, klass.name.to_s, matcher_hash, lambda{ |att| klass.perform(att) })
36
+ end
37
+
38
+ def transform(method_name)
39
+ @transform = method_name
40
+ end
41
+ def perform(attributes)
42
+ sub_key = attributes["bus_rider_sub_key"]
43
+ meth_key = sub_key.split(".").last
44
+ resque_bus_execute(meth_key, attributes)
45
+ end
46
+
47
+ def resque_bus_execute(key, attributes)
48
+ args = attributes
49
+ args = send(@transform, attributes) if @transform
50
+ args = [args] unless args.is_a?(Array)
51
+ if self.respond_to?(:subscriber_with_attributes)
52
+ me = self.subscriber_with_attributes(attributes)
53
+ else
54
+ me = self.new
55
+ end
56
+ me.send(key, *args)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,50 @@
1
+ module ResqueBus
2
+ class Subscription
3
+ def self.register(queue, key, class_name, matcher, block)
4
+ Subscription.new(queue, key, class_name, matcher, block)
5
+ end
6
+
7
+ def self.from_redis(hash)
8
+ queue_name = hash["queue_name"].to_s
9
+ key = hash["key"].to_s
10
+ class_name = hash["class"].to_s
11
+ matcher = hash["matcher"]
12
+ return nil if key.length == 0 || queue_name.length == 0
13
+ Subscription.new(queue_name, key, class_name, matcher, nil)
14
+ end
15
+
16
+ def to_redis
17
+ out = {}
18
+ out["queue_name"] = queue_name
19
+ out["key"] = key
20
+ out["class"] = class_name
21
+ out["matcher"] = matcher.to_redis
22
+ out
23
+ end
24
+
25
+ attr_reader :matcher, :executor, :queue_name, :key, :class_name
26
+ attr_accessor :app_key # dyanmically set on return from subscription_matches
27
+ def initialize(queue_name, key, class_name, filters, executor=nil)
28
+ @queue_name = self.class.normalize(queue_name)
29
+ @key = key.to_s
30
+ @class_name = class_name.to_s
31
+ @matcher = Matcher.new(filters)
32
+ @executor = executor
33
+ end
34
+
35
+ def execute!(attributes)
36
+ attributes = attributes.with_indifferent_access if attributes.respond_to?(:with_indifferent_access)
37
+ executor.call(attributes)
38
+ end
39
+
40
+ def matches?(attributes)
41
+ @matcher.matches?(attributes)
42
+ end
43
+
44
+ protected
45
+
46
+ def self.normalize(val)
47
+ val.to_s.gsub(/\W/, "_").downcase
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,49 @@
1
+ module ResqueBus
2
+ class SubscriptionList
3
+ def self.from_redis(redis_hash)
4
+ out = SubscriptionList.new
5
+
6
+ redis_hash.each do |key, value|
7
+ sub = Subscription.from_redis(value)
8
+ out.add(sub) if sub
9
+ end
10
+ out
11
+ end
12
+
13
+ def to_redis
14
+ out = {}
15
+ @subscriptions.values.each do |sub|
16
+ out[sub.key] = sub.to_redis
17
+ end
18
+ out
19
+ end
20
+
21
+ def initialize
22
+ @subscriptions = {}
23
+ end
24
+
25
+ def add(sub)
26
+ @subscriptions[sub.key] = sub
27
+ end
28
+
29
+ def size
30
+ @subscriptions.size
31
+ end
32
+
33
+ def key(key)
34
+ @subscriptions[key.to_s]
35
+ end
36
+
37
+ def all
38
+ @subscriptions.values
39
+ end
40
+
41
+ def matches(attributes)
42
+ out = []
43
+ all.each do |sub|
44
+ out << sub if sub.matches?(attributes)
45
+ end
46
+ out
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,50 @@
1
+ module ResqueBus
2
+ class TaskManager
3
+ attr_reader :logging
4
+ def initialize(logging)
5
+ @logging = logging
6
+ end
7
+
8
+ def subscribe!
9
+ count = 0
10
+ ResqueBus.dispatchers.each do |dispatcher|
11
+ subscriptions = dispatcher.subscriptions
12
+ if subscriptions.size > 0
13
+ count += subscriptions.size
14
+ log "Subscribing #{dispatcher.app_key} to #{subscriptions.size} subscriptions"
15
+ app = ResqueBus::Application.new(dispatcher.app_key)
16
+ app.subscribe(subscriptions, logging)
17
+ log " ...done"
18
+ end
19
+ end
20
+ count
21
+ end
22
+
23
+ def unsubscribe!
24
+ count = 0
25
+ ResqueBus.dispatchers.each do |dispatcher|
26
+ log "Unsubcribing from #{dispatcher.app_key}"
27
+ app = ResqueBus::Application.new(dispatcher.app_key)
28
+ app.unsubscribe
29
+ count += 1
30
+ log " ...done"
31
+ end
32
+ end
33
+
34
+ def queue_names
35
+ # let's not talk to redis in here. Seems to screw things up
36
+ queues = []
37
+ ResqueBus.dispatchers.each do |dispatcher|
38
+ dispatcher.subscriptions.all.each do |sub|
39
+ queues << sub.queue_name
40
+ end
41
+ end
42
+
43
+ queues.uniq
44
+ end
45
+
46
+ def log(message)
47
+ puts(message) if logging
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,105 @@
1
+ # require 'resquebus/tasks'
2
+ # will give you the resquebus tasks
3
+
4
+
5
+ require "resque/tasks"
6
+ namespace :resquebus do
7
+
8
+ desc "Setup will configure a resque task to run before resque:work"
9
+ task :setup => [ :preload ] do
10
+
11
+ if ENV['QUEUES'].nil?
12
+ manager = ::ResqueBus::TaskManager.new(true)
13
+ queues = manager.queue_names
14
+ ENV['QUEUES'] = queues.join(",")
15
+ else
16
+ queues = ENV['QUEUES'].split(",")
17
+ end
18
+
19
+ if queues.size == 1
20
+ puts " >> Working Queue : #{queues.first}"
21
+ else
22
+ puts " >> Working Queues: #{queues.join(", ")}"
23
+ end
24
+ end
25
+
26
+ desc "Subscribes this application to ResqueBus events"
27
+ task :subscribe => [ :preload ] do
28
+ manager = ::ResqueBus::TaskManager.new(true)
29
+ count = manager.subscribe!
30
+ raise "No subscriptions created" if count == 0
31
+ end
32
+
33
+ desc "Unsubscribes this application from ResqueBus events"
34
+ task :unsubscribe => [ :preload ] do
35
+ require 'resque-bus'
36
+ manager = ::ResqueBus::TaskManager.new(true)
37
+ count = manager.unsubscribe!
38
+ puts "No subscriptions unsubscribed" if count == 0
39
+ end
40
+
41
+ desc "Sets the queue to work the driver Use: `rake resquebus:driver resque:work`"
42
+ task :driver => [ :preload ] do
43
+ ENV['QUEUES'] = "resquebus_incoming"
44
+ end
45
+
46
+ # Preload app files if this is Rails
47
+ task :preload do
48
+ require "resque"
49
+ require "resque-bus"
50
+ require "resque/failure/redis"
51
+
52
+ # change the namespace to be the ones used by ResqueBus
53
+ # save the old one for handling later
54
+ ResqueBus.original_redis = Resque.redis
55
+ Resque.redis = ResqueBus.redis
56
+
57
+ Resque::Failure::MultipleWithRetrySuppression.classes = [Resque::Failure::Redis]
58
+ Resque::Failure.backend = Resque::Failure::MultipleWithRetrySuppression
59
+
60
+ Rake::Task["resque:setup"].invoke # loads the environment and such if defined
61
+ end
62
+
63
+
64
+ # examples to test out the system
65
+ namespace :example do
66
+ desc "Publishes events to example applications"
67
+ task :publish => [ "resquebus:preload", "resquebus:setup" ] do
68
+ which = ["one", "two", "three", "other"][rand(4)]
69
+ ResqueBus.publish("event_#{which}", { "rand" => rand(99999)})
70
+ ResqueBus.publish("event_all", { "rand" => rand(99999)})
71
+ ResqueBus.publish("none_subscribed", { "rand" => rand(99999)})
72
+ puts "published event_#{which}, event_all, none_subscribed"
73
+ end
74
+
75
+ desc "Sets up an example config"
76
+ task :register => [ "resquebus:preload"] do
77
+ ResqueBus.dispatch("example") do
78
+ subscribe "event_one" do
79
+ puts "event1 happened"
80
+ end
81
+
82
+ subscribe "event_two" do
83
+ puts "event2 happened"
84
+ end
85
+
86
+ high "event_three" do
87
+ puts "event3 happened (high)"
88
+ end
89
+
90
+ low "event_.*" do |attributes|
91
+ puts "LOG ALL: #{attributes.inspect}"
92
+ end
93
+ end
94
+ end
95
+
96
+ desc "Subscribes this application to ResqueBus example events"
97
+ task :subscribe => [ :register, "resquebus:subscribe" ]
98
+
99
+ desc "Start a ResqueBus example worker"
100
+ task :work => [ :register, "resquebus:setup", "resque:work" ]
101
+
102
+ desc "Start a ResqueBus example worker"
103
+ task :driver => [ :register, "resquebus:driver", "resque:work" ]
104
+ end
105
+ end
@@ -0,0 +1,42 @@
1
+ module ResqueBus
2
+ module Util
3
+ extend self
4
+
5
+ def underscore(camel_cased_word)
6
+ word = camel_cased_word.to_s.dup
7
+ word.gsub!('::', '/')
8
+ # word.gsub!(/(?:([A-Za-z\d])|^)(#{inflections.acronym_regex})(?=\b|[^a-z])/) { "#{$1}#{$1 && '_'}#{$2.downcase}" }
9
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
10
+ word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
11
+ word.tr!("-", "_")
12
+ word.downcase!
13
+ word
14
+ end
15
+
16
+ def constantize(camel_cased_word)
17
+ names = camel_cased_word.split('::')
18
+ names.shift if names.empty? || names.first.empty?
19
+
20
+ names.inject(Object) do |constant, name|
21
+ if constant == Object
22
+ constant.const_get(name)
23
+ else
24
+ candidate = constant.const_get(name)
25
+ next candidate if constant.const_defined?(name, false)
26
+ next candidate unless Object.const_defined?(name)
27
+
28
+ # Go down the ancestors to check it it's owned
29
+ # directly before we reach Object or the end of ancestors.
30
+ constant = constant.ancestors.inject do |const, ancestor|
31
+ break const if ancestor == Object
32
+ break ancestor if ancestor.const_defined?(name, false)
33
+ const
34
+ end
35
+
36
+ # owner is in Object, so raise
37
+ constant.const_get(name, false)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,5 @@
1
+ module Resque
2
+ module Bus
3
+ VERSION = "0.2.3"
4
+ end
5
+ end
@@ -0,0 +1,2 @@
1
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../../lib'
2
+ require 'resque_bus/tasks'
@@ -0,0 +1,34 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "resque_bus/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "resque-bus"
7
+ s.version = Resque::Bus::VERSION
8
+ s.authors = ["Brian Leonard"]
9
+ s.email = ["brian@bleonard.com"]
10
+ s.homepage = ""
11
+ s.summary = %q{A simple event bus on top of Resque}
12
+ s.description = %q{A simple event bus on top of Resque.
13
+ Publish and subscribe to events as they occur through a queue.}
14
+
15
+ s.rubyforge_project = "resque-bus"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+
22
+ # specify any dependencies here; for example:
23
+ # s.add_development_dependency "rspec"
24
+ # s.add_runtime_dependency "rest-client"
25
+ s.add_dependency('resque', ['>= 1.10.0', '< 2.0'])
26
+ s.add_dependency('resque-scheduler', '>= 2.0.1')
27
+ s.add_dependency('resque-retry')
28
+ s.add_dependency("redis-namespace")
29
+ s.add_dependency("redis")
30
+
31
+ s.add_development_dependency("rspec")
32
+ s.add_development_dependency("timecop")
33
+ s.add_development_dependency("json_pure")
34
+ end