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.
@@ -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