derfred-workling 0.4.9.1 → 0.4.9.2

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.
Files changed (44) hide show
  1. data/lib/cattr_accessor.rb +51 -0
  2. data/lib/mattr_accessor.rb +55 -0
  3. data/lib/rude_q/client.rb +11 -0
  4. data/lib/workling/base.rb +115 -0
  5. data/lib/workling/clients/amqp_client.rb +38 -0
  6. data/lib/workling/clients/amqp_exchange_client.rb +50 -0
  7. data/lib/workling/clients/base.rb +57 -0
  8. data/lib/workling/clients/memcache_queue_client.rb +91 -0
  9. data/lib/workling/clients/sqs_client.rb +162 -0
  10. data/lib/workling/clients/xmpp_client.rb +100 -0
  11. data/lib/workling/discovery.rb +16 -0
  12. data/lib/workling/remote/invokers/amqp_single_subscriber.rb +45 -0
  13. data/lib/workling/remote/invokers/base.rb +124 -0
  14. data/lib/workling/remote/invokers/basic_poller.rb +41 -0
  15. data/lib/workling/remote/invokers/eventmachine_subscriber.rb +41 -0
  16. data/lib/workling/remote/invokers/looped_subscriber.rb +38 -0
  17. data/lib/workling/remote/invokers/thread_pool_poller.rb +169 -0
  18. data/lib/workling/remote/invokers/threaded_poller.rb +153 -0
  19. data/lib/workling/remote/runners/amqp_exchange_runner.rb +45 -0
  20. data/lib/workling/remote/runners/backgroundjob_runner.rb +35 -0
  21. data/lib/workling/remote/runners/base.rb +42 -0
  22. data/lib/workling/remote/runners/client_runner.rb +46 -0
  23. data/lib/workling/remote/runners/not_remote_runner.rb +23 -0
  24. data/lib/workling/remote/runners/not_runner.rb +17 -0
  25. data/lib/workling/remote/runners/rudeq_runner.rb +23 -0
  26. data/lib/workling/remote/runners/spawn_runner.rb +38 -0
  27. data/lib/workling/remote/runners/starling_runner.rb +13 -0
  28. data/lib/workling/remote.rb +69 -0
  29. data/lib/workling/return/store/base.rb +42 -0
  30. data/lib/workling/return/store/iterator.rb +24 -0
  31. data/lib/workling/return/store/memory_return_store.rb +26 -0
  32. data/lib/workling/return/store/rudeq_return_store.rb +24 -0
  33. data/lib/workling/return/store/starling_return_store.rb +31 -0
  34. data/lib/workling/routing/base.rb +16 -0
  35. data/lib/workling/routing/class_and_method_routing.rb +57 -0
  36. data/lib/workling/routing/static_routing.rb +47 -0
  37. data/lib/workling/rudeq/client.rb +17 -0
  38. data/lib/workling/rudeq/poller.rb +116 -0
  39. data/lib/workling/rudeq.rb +7 -0
  40. data/lib/workling.rb +195 -0
  41. data/lib/workling_server.rb +108 -0
  42. data/script/bj_invoker.rb +11 -0
  43. data/script/starling_status.rb +37 -0
  44. metadata +44 -1
@@ -0,0 +1,100 @@
1
+ require 'workling/clients/base'
2
+ Workling.try_load_xmpp4r
3
+
4
+ #
5
+ # An XMPP client
6
+ #
7
+ # How to use: this client requires the xmpp4r gem
8
+ #
9
+ # in the config/environments/development.rb file (or production.rb etc)
10
+ #
11
+ # Workling::Remote::Runners::ClientRunner.client = Workling::Clients::XmppClient.new
12
+ # Workling::Remote.dispatcher = Workling::Remote::Runners::ClientRunner.new # dont use the standard runner
13
+ # Workling::Remote.invoker = Workling::Remote::Invokers::LoopedSubscriber # does not work with the EventmachineSubscriber Invoker
14
+ #
15
+ # furthermore in the workling.yml file you need to set up the server details for your XMPP server
16
+ #
17
+ # development:
18
+ # listens_on: "localhost:22122"
19
+ # jabber_id: "sub@localhost/laptop"
20
+ # jabber_server: "localhost"
21
+ # jabber_password: "sub"
22
+ # jabber_service: "pubsub.derfredtop.local"
23
+ #
24
+ # for details on how to configure your XMPP server (ejabberd) check out the following howto:
25
+ #
26
+ # http://keoko.wordpress.com/2008/12/17/xmpp-pubsub-with-ejabberd-and-xmpp4r/
27
+ #
28
+ #
29
+ # finally you need to expose your worker methods to XMPP nodes like so:
30
+ #
31
+ # class NotificationWorker < Workling::Base
32
+ #
33
+ # expose :receive_notification, :as => "/home/localhost/pub/sub"
34
+ #
35
+ # def receive_notification(input)
36
+ # # something here
37
+ # end
38
+ #
39
+ # end
40
+ #
41
+
42
+
43
+
44
+ module Workling
45
+ module Clients
46
+ class XmppClient < Workling::Clients::Base
47
+
48
+ # starts the client.
49
+ def connect
50
+ begin
51
+ @client = Jabber::Client.new Workling.config[:jabber_id]
52
+ @client.connect Workling.config[:jabber_server]
53
+ @client.auth Workling.config[:jabber_password]
54
+ @client.send Jabber::Presence.new.set_type(:available)
55
+ @pubsub = Jabber::PubSub::ServiceHelper.new(@client, Workling.config[:jabber_service])
56
+ unsubscribe_from_all # make sure there are no open subscriptions, could cause multiple delivery of notifications, as they are persistent
57
+ rescue
58
+ raise WorklingError.new("couldn't connect to the jabber server")
59
+ end
60
+ end
61
+
62
+ # disconnect from the server
63
+ def close
64
+ @client.close
65
+ end
66
+
67
+ # subscribe to a queue
68
+ def subscribe(key)
69
+ @pubsub.subscribe_to(key)
70
+
71
+ # filter out the subscription notification message that was generated by subscribing to the node
72
+ @pubsub.get_subscriptions_from_all_nodes()
73
+
74
+ @pubsub.add_event_callback do |event|
75
+ event.payload.each do |e|
76
+ e.children.each do |child|
77
+ yield Hash.from_xml(child.children.first.to_s) if child.name == 'item'
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ # request and retrieve work
84
+ def retrieve(key)
85
+ @pubsub.get_items_from(key, 1)
86
+ end
87
+
88
+ def request(key, value)
89
+ @pubsub.publish_item_to(key, value)
90
+ end
91
+
92
+ private
93
+ def unsubscribe_from_all
94
+ @pubsub.get_subscriptions_from_all_nodes.each do |subscription|
95
+ @pubsub.unsubscribe_from subscription.node
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,16 @@
1
+ #
2
+ # Discovery is responsible for loading workers in app/workers.
3
+ #
4
+ module Workling
5
+ class Discovery
6
+ cattr_accessor :discovered
7
+ @@discovered = []
8
+
9
+ # requires worklings so that they are added to routing.
10
+ def self.discover!
11
+ Workling.load_path.each do |p|
12
+ Dir.glob(p).each { |wling| require wling }
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,45 @@
1
+ require 'eventmachine'
2
+ require 'workling/remote/invokers/base'
3
+
4
+ #
5
+ # TODO - Subscribes a single worker to a single queue
6
+ #
7
+ module Workling
8
+ module Remote
9
+ module Invokers
10
+ class AmqpSingleSubscriber < Workling::Remote::Invokers::Base
11
+
12
+ def initialize(routing, client_class)
13
+ super
14
+ end
15
+
16
+ #
17
+ # Starts EM loop and sets up subscription callback for the worker
18
+ # Create the queue and bind to exchange using the routing key
19
+ #
20
+ def listen
21
+ EM.run do
22
+ connect do
23
+ queue_name = @routing.queue_for
24
+ routing_key = @routing.routing_key_for
25
+
26
+ # temp stuff to hook the queues and exchanges up
27
+ # wildcard routing - # (match all)
28
+ exch = MQ.topic
29
+ q = MQ.queue(queue_name)
30
+ q.bind(exch, :key => routing_key)
31
+
32
+ @client.subscribe(queue_name) do |args|
33
+ run(queue_name, args)
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ def stop
40
+ EM.stop if EM.reactor_running?
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,124 @@
1
+ #
2
+ # Invokers are responsible for
3
+ #
4
+ # 1. grabbing work off a job broker (such as a starling or rabbitmq server).
5
+ # 2. routing (mapping) that work onto the correct worker method.
6
+ # 3. invoking the worker method, passing any arguments that came off the broker.
7
+ #
8
+ # Invokers should implement their own concurrency strategies. For example,
9
+ # The there is a ThreadedPoller which starts a thread for each Worker class.
10
+ #
11
+ # This base Invoker class defines the methods an Invoker needs to implement.
12
+ #
13
+ module Workling
14
+ module Remote
15
+ module Invokers
16
+ class Base
17
+
18
+ attr_accessor :sleep_time, :reset_time
19
+
20
+ #
21
+ # call up with super in the subclass constructor.
22
+ #
23
+ def initialize(routing, client_class)
24
+ @routing = routing
25
+ @client_class = client_class
26
+ @sleep_time = Workling.config[:sleep_time] || 2
27
+ @reset_time = Workling.config[:reset_time] || 30
28
+ @@mutex ||= Mutex.new
29
+ end
30
+
31
+ #
32
+ # Starts main Invoker Loop. The invoker runs until stop() is called.
33
+ #
34
+ def listen
35
+ raise NotImplementedError.new("Implement listen() in your Invoker. ")
36
+ end
37
+
38
+ #
39
+ # Gracefully stops the Invoker. The currently executing Jobs should be allowed
40
+ # to finish.
41
+ #
42
+ def stop
43
+ raise NotImplementedError.new("Implement stop() in your Invoker. ")
44
+ end
45
+
46
+ #
47
+ # Runs the worker method, given
48
+ #
49
+ # type: the worker route
50
+ # args: the arguments to be passed into the worker method.
51
+ #
52
+ def run(type, args)
53
+ worker = @routing[type]
54
+ method = @routing.method_name(type)
55
+ worker.dispatch_to_worker_method(method, args)
56
+ end
57
+
58
+ # returns the Workling::Base.logger
59
+ def logger; Workling::Base.logger; end
60
+
61
+ protected
62
+
63
+ # handle opening and closing of client. pass code block to this method.
64
+ def connect
65
+ @client = @client_class.new
66
+ @client.connect
67
+
68
+ begin
69
+ yield
70
+ ensure
71
+ @client.close
72
+ ActiveRecord::Base.verify_active_connections!
73
+ end
74
+ end
75
+
76
+ #
77
+ # Loops through the available routes, yielding for each route.
78
+ # This continues until @shutdown is set on this instance.
79
+ #
80
+ def loop_routes
81
+ while(!@shutdown) do
82
+ ensure_activerecord_connection
83
+
84
+ routes.each do |route|
85
+ break if @shutdown
86
+ yield route
87
+ end
88
+
89
+ sleep self.sleep_time
90
+ end
91
+ end
92
+
93
+ #
94
+ # Returns the complete set of active routes
95
+ #
96
+ def routes
97
+ @active_routes ||= Workling::Discovery.discovered.map { |clazz| @routing.queue_names_routing_class(clazz) }.flatten
98
+ end
99
+
100
+ # Thanks for this Brent!
101
+ #
102
+ # ...Just a heads up, due to how rails’ MySQL adapter handles this
103
+ # call ‘ActiveRecord::Base.connection.active?’, you’ll need
104
+ # to wrap the code that checks for a connection in in a mutex.
105
+ #
106
+ # ....I noticed this while working with a multi-core machine that
107
+ # was spawning multiple workling threads. Some of my workling
108
+ # threads would hit serious issues at this block of code without
109
+ # the mutex.
110
+ #
111
+ def ensure_activerecord_connection
112
+ @@mutex.synchronize do
113
+ unless ActiveRecord::Base.connection.active? # Keep MySQL connection alive
114
+ unless ActiveRecord::Base.connection.reconnect!
115
+ logger.fatal("Failed - Database not available!")
116
+ break
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,41 @@
1
+ require 'workling/remote/invokers/base'
2
+
3
+ #
4
+ # A basic polling invoker.
5
+ #
6
+ module Workling
7
+ module Remote
8
+ module Invokers
9
+ class BasicPoller < Workling::Remote::Invokers::Base
10
+
11
+ #
12
+ # set up client, sleep time
13
+ #
14
+ def initialize(routing, client_class)
15
+ super
16
+ end
17
+
18
+ #
19
+ # Starts main Invoker Loop. The invoker runs until stop() is called.
20
+ #
21
+ def listen
22
+ connect do
23
+ loop_routes do |route|
24
+ if args = @client.retrieve(route)
25
+ run(route, args)
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ #
32
+ # Gracefully stops the Invoker. The currently executing Jobs should be allowed
33
+ # to finish.
34
+ #
35
+ def stop
36
+ @shutdown = true
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,41 @@
1
+ require 'eventmachine'
2
+ require 'workling/remote/invokers/base'
3
+
4
+ #
5
+ # Subscribes the workers to the correct queues.
6
+ #
7
+ module Workling
8
+ module Remote
9
+ module Invokers
10
+ class EventmachineSubscriber < Workling::Remote::Invokers::Base
11
+
12
+ def initialize(routing, client_class)
13
+ super
14
+ end
15
+
16
+ #
17
+ # Starts EM loop and sets up subscription callbacks for workers.
18
+ #
19
+ def listen
20
+ EM.run do
21
+ connect do
22
+ routes.each do |route|
23
+ @client.subscribe(route) do |args|
24
+ begin
25
+ run(route, args)
26
+ rescue
27
+ logger.error("EventmachineSubscriber listen error on #{route}: #{$!}")
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ def stop
36
+ EM.stop if EM.reactor_running?
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,38 @@
1
+ require 'workling/remote/invokers/base'
2
+
3
+ #
4
+ # Subscribes the workers to the correct queues.
5
+ #
6
+ module Workling
7
+ module Remote
8
+ module Invokers
9
+ class LoopedSubscriber < Workling::Remote::Invokers::Base
10
+
11
+ def initialize(routing, client_class)
12
+ super
13
+ end
14
+
15
+ #
16
+ # Starts EM loop and sets up subscription callbacks for workers.
17
+ #
18
+ def listen
19
+ connect do
20
+ routes.each do |route|
21
+ @client.subscribe(route) do |args|
22
+ run(route, args)
23
+ end
24
+ end
25
+
26
+ loop do
27
+ sleep 1
28
+ end
29
+ end
30
+ end
31
+
32
+ def stop
33
+
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,169 @@
1
+ require 'workling/remote/invokers/base'
2
+
3
+ require 'thread'
4
+ require 'mutex_m'
5
+
6
+ #
7
+ # A polling invoker that executes the jobs using a thread pool.
8
+ #
9
+ # This invoker was designed for long running tasks and Rails 2.2. It is expected
10
+ # that each worker will manage it's own database connections using ActiveRecord::Base.connection_pool.
11
+ #
12
+ # This implementation isn't using most of the features provided by Invokers::Base
13
+ # because of it's assumptions about database connections
14
+ #
15
+ # Example:
16
+ # user = nil
17
+ # ActiveRecord::Base.connection_pool.with_connection do
18
+ # user = User.find(options[:id])
19
+ # end
20
+ #
21
+ # # Do long running stuff here
22
+ # user.wait_until_birthday
23
+ # user.buy_gift 'BRAWNDO!'
24
+ #
25
+ # ActiveRecord::Base.connection_pool.with_connection do
26
+ # user.save
27
+ # end
28
+ #
29
+ module Workling
30
+ module Remote
31
+ module Invokers
32
+ class ThreadPoolPoller < Workling::Remote::Invokers::Base
33
+ attr_reader :sleep_time, :reset_time, :pool_capacity
34
+
35
+ def initialize(routing, client_class)
36
+ @routing = routing
37
+ @client_class = client_class
38
+
39
+ # Grab settings out of the config file
40
+ @sleep_time = (Workling.config[:sleep_time] || 2).to_f
41
+ @reset_time = (Workling.config[:reset_time] || 30).to_f
42
+
43
+ # Pool of polling threads
44
+ @pollers = []
45
+ @pollers.extend(Mutex_m)
46
+
47
+ # Pool of worker threads
48
+ @workers = []
49
+ @workers.extend(Mutex_m)
50
+
51
+ # Connection to the job queue
52
+ @pool_capacity = (Workling.config[:pool_size] || 25).to_i
53
+ end
54
+
55
+ # Start up the checking for items on the queue. Will block until stop is called and all pollers
56
+ # and workers have finished execution.
57
+ def listen
58
+ logger.info("Starting ThreadPoolPoller...")
59
+
60
+ # Determine which queues need checking
61
+ Workling::Discovery.discovered.map do |klass|
62
+ @pollers.synchronize do
63
+ # Polls the backing queue for jobs to be done
64
+ @pollers << Thread.new do
65
+ poller_thread(@routing.queue_names_routing_class(klass))
66
+ end
67
+ end
68
+ end
69
+
70
+ # Wait for the poller and all outstanding workers to finish.
71
+ #
72
+ # This is a little tricky because we're doing some synchronization on pollers... but
73
+ # the list of pollers is never modified after being setup above.
74
+ @pollers.synchronize { @pollers.dup }.each { |p| p.join }
75
+ @pollers.synchronize { @pollers.clear }
76
+ logger.info("Pollers have all finished")
77
+
78
+ @workers.synchronize { @workers.dup }.each { |w| w.join }
79
+ logger.info("Worker threads have all finished")
80
+ end
81
+
82
+ # Instructs the thread pool poller to stop checking for new jobs on the backing queue.
83
+ def stop
84
+ logger.info("Stopping thread pool invoker pollers and workers...")
85
+ @pollers.synchronize { @pollers.each { |p| p[:shutdown] = true } }
86
+ end
87
+
88
+ # Set pool_size in workling config to adjust the maximum number of threads in the pool
89
+ def workers_available?
90
+ worker_threads < @pool_capacity
91
+ end
92
+
93
+ # Number of correctly active worker threads
94
+ def worker_threads
95
+ @workers.synchronize { @workers.size }
96
+ end
97
+
98
+ # Number of currently active polling threads
99
+ def poller_threads
100
+ @pollers.synchronize { @pollers.size }
101
+ end
102
+
103
+ private
104
+
105
+ def poller_thread(queues)
106
+ # Make sure queues is an array
107
+ queues = [queues].flatten!
108
+
109
+ # Connect our client to the backing queue
110
+ client = @client_class.new
111
+ client.connect
112
+ logger.info("** Starting client #{ client.class } for #{ queues.inspect }") if logger.info?
113
+
114
+ # Poll each queue for new items
115
+ while(!Thread.current[:shutdown]) do
116
+ # Check each queue for a job posting
117
+ queues.each do |queue|
118
+ break if Thread.current[:shutdown]
119
+
120
+ begin
121
+ # Take a job off the queue and execute it in a new worker thread.
122
+ #
123
+ # Don't pop any jobs off the backing queue if the thread pool
124
+ # is full. This keeps them on the master queue so other instances
125
+ # can process them.
126
+ while(workers_available? && (options = client.retrieve(queue)))
127
+ logger.debug("#{queue} received job #{ options.inspect }") if logger.debug?
128
+
129
+ @workers.synchronize do
130
+ @workers << Thread.new do
131
+ begin
132
+ # Execute the job
133
+ run(queue, options)
134
+ rescue Exception => e
135
+ # Log the exception since there isn't much else we can do about it at this point
136
+ logger.error(e) if logger.error?
137
+ ensure
138
+ # Make sure the current thread's connection gets released
139
+ if(ActiveRecord::Base.connection_pool)
140
+ ActiveRecord::Base.connection_pool.release_connection
141
+ end
142
+
143
+ # Remove this thread from the list of active workers
144
+ @workers.synchronize do
145
+ @workers.delete(Thread.current)
146
+ end
147
+ end
148
+ end
149
+ end
150
+
151
+ # Break out of the checks early if shutdown was called
152
+ break if Thread.current[:shutdown]
153
+ end
154
+ rescue Workling::WorklingError => e
155
+ logger.error("FAILED to connect with queue #{ queue }: #{ e } }") if logger.error?
156
+ sleep(@reset_time)
157
+
158
+ # FIXME: This will _definitely_ blow up with AMQP since there is no reset call
159
+ client.reset
160
+ end
161
+ end
162
+
163
+ sleep(@sleep_time) unless Thread.current[:shutdown]
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end