did_workling 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +382 -0
- data/lib/rude_q/client.rb +11 -0
- data/lib/workling.rb +150 -0
- data/lib/workling/base.rb +59 -0
- data/lib/workling/clients/amqp_client.rb +40 -0
- data/lib/workling/clients/base.rb +54 -0
- data/lib/workling/clients/memcache_queue_client.rb +82 -0
- data/lib/workling/discovery.rb +14 -0
- data/lib/workling/remote.rb +42 -0
- data/lib/workling/remote/invokers/base.rb +124 -0
- data/lib/workling/remote/invokers/basic_poller.rb +41 -0
- data/lib/workling/remote/invokers/eventmachine_subscriber.rb +37 -0
- data/lib/workling/remote/invokers/threaded_poller.rb +149 -0
- data/lib/workling/remote/runners/backgroundjob_runner.rb +35 -0
- data/lib/workling/remote/runners/base.rb +42 -0
- data/lib/workling/remote/runners/client_runner.rb +45 -0
- data/lib/workling/remote/runners/not_remote_runner.rb +23 -0
- data/lib/workling/remote/runners/spawn_runner.rb +38 -0
- data/lib/workling/remote/runners/starling_runner.rb +13 -0
- data/lib/workling/return/store/base.rb +37 -0
- data/lib/workling/return/store/memory_return_store.rb +26 -0
- data/lib/workling/return/store/starling_return_store.rb +31 -0
- data/lib/workling/routing/base.rb +13 -0
- data/lib/workling/routing/class_and_method_routing.rb +55 -0
- data/test/class_and_method_routing_test.rb +18 -0
- data/test/clients/memory_queue_client.rb +36 -0
- data/test/discovery_test.rb +13 -0
- data/test/invoker_basic_poller_test.rb +29 -0
- data/test/invoker_eventmachine_subscription_test.rb +26 -0
- data/test/invoker_threaded_poller_test.rb +34 -0
- data/test/memcachequeue_client_test.rb +36 -0
- data/test/memory_return_store_test.rb +23 -0
- data/test/mocks/client.rb +9 -0
- data/test/mocks/logger.rb +5 -0
- data/test/mocks/spawn.rb +5 -0
- data/test/not_remote_runner_test.rb +11 -0
- data/test/remote_runner_test.rb +50 -0
- data/test/return_store_test.rb +18 -0
- data/test/runners/thread_runner.rb +22 -0
- data/test/spawn_runner_test.rb +10 -0
- data/test/starling_return_store_test.rb +29 -0
- data/test/starling_runner_test.rb +8 -0
- data/test/test_helper.rb +48 -0
- data/test/workers/analytics/invites.rb +10 -0
- data/test/workers/util.rb +15 -0
- metadata +132 -0
@@ -0,0 +1,59 @@
|
|
1
|
+
#
|
2
|
+
# All worker classes must inherit from this class, and be saved in app/workers.
|
3
|
+
#
|
4
|
+
# The Worker lifecycle:
|
5
|
+
# The Worker is loaded once, at which point the instance method 'create' is called.
|
6
|
+
#
|
7
|
+
# Invoking Workers:
|
8
|
+
# Calling async_my_method on the worker class will trigger background work.
|
9
|
+
# This means that the loaded Worker instance will receive a call to the method
|
10
|
+
# my_method(:uid => "thisjobsuid2348732947923").
|
11
|
+
#
|
12
|
+
# The Worker method must have a single hash argument. Note that the job :uid will
|
13
|
+
# be merged into the hash.
|
14
|
+
#
|
15
|
+
module Workling
|
16
|
+
class Base
|
17
|
+
cattr_accessor :logger
|
18
|
+
@@logger ||= ::RAILS_DEFAULT_LOGGER
|
19
|
+
|
20
|
+
def self.inherited(subclass)
|
21
|
+
Workling::Discovery.discovered << subclass
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize
|
25
|
+
super
|
26
|
+
|
27
|
+
create
|
28
|
+
end
|
29
|
+
|
30
|
+
# Put worker initialization code in here. This is good for restarting jobs that
|
31
|
+
# were interrupted.
|
32
|
+
def create
|
33
|
+
end
|
34
|
+
|
35
|
+
# takes care of suppressing remote errors but raising Workling::WorklingNotFoundError
|
36
|
+
# where appropriate. swallow workling exceptions so that everything behaves like remote code.
|
37
|
+
# otherwise StarlingRunner and SpawnRunner would behave too differently to NotRemoteRunner.
|
38
|
+
def dispatch_to_worker_method(method, options)
|
39
|
+
begin
|
40
|
+
self.send(method, options)
|
41
|
+
rescue Exception => e
|
42
|
+
raise e if e.kind_of?(Workling::WorklingError)
|
43
|
+
logger.error "WORKLING ERROR: runner could not invoke #{ self.class }:#{ method } with #{ options.inspect }. error was: #{ e.inspect }\n #{ e.backtrace.join("\n") }"
|
44
|
+
|
45
|
+
# reraise after logging. the exception really can't go anywhere in many cases. (spawn traps the exception)
|
46
|
+
raise e if Workling.raise_exceptions?
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# thanks to blaine cook for this suggestion.
|
51
|
+
def self.method_missing(method, *args, &block)
|
52
|
+
if method.to_s =~ /^asynch?_(.*)/
|
53
|
+
Workling::Remote.run(self.to_s.dasherize, $1, *args)
|
54
|
+
else
|
55
|
+
super
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'workling/clients/base'
|
2
|
+
Workling.try_load_an_amqp_client
|
3
|
+
|
4
|
+
#
|
5
|
+
# An Ampq client
|
6
|
+
#
|
7
|
+
module Workling
|
8
|
+
module Clients
|
9
|
+
class AmqpClient < Workling::Clients::Base
|
10
|
+
|
11
|
+
# starts the client.
|
12
|
+
def connect
|
13
|
+
begin
|
14
|
+
@amq = MQ.new
|
15
|
+
rescue
|
16
|
+
raise WorklingError.new("couldn't start amq client. if you're running this in a server environment, then make sure the server is evented (ie use thin or evented mongrel, not normal mongrel.)")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# no need for explicit closing. when the event loop
|
21
|
+
# terminates, the connection is closed anyway.
|
22
|
+
def close; true; end
|
23
|
+
|
24
|
+
# subscribe to a queue
|
25
|
+
def subscribe(key)
|
26
|
+
@amq.queue(key).subscribe do |data|
|
27
|
+
value = Marshal.load(data)
|
28
|
+
yield value
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# request and retrieve work
|
33
|
+
def retrieve(key); @amq.queue(key); end
|
34
|
+
def request(key, value)
|
35
|
+
data = Marshal.dump(value)
|
36
|
+
@amq.queue(key).publish(data)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
#
|
2
|
+
# Clients are responsible for communicating with a job broker (ie connecting to starling or rabbitmq.)
|
3
|
+
#
|
4
|
+
# Clients are used to request jobs on a broker, get results for a job from a broker, and subscribe to results
|
5
|
+
# from a specific type of job.
|
6
|
+
#
|
7
|
+
module Workling
|
8
|
+
module Clients
|
9
|
+
class Base
|
10
|
+
|
11
|
+
#
|
12
|
+
# Requests a job on the broker.
|
13
|
+
#
|
14
|
+
# work_type:
|
15
|
+
# arguments: the argument to the worker method
|
16
|
+
#
|
17
|
+
def request(work_type, arguments)
|
18
|
+
raise NotImplementedError.new("Implement request(work_type, arguments) in your client. ")
|
19
|
+
end
|
20
|
+
|
21
|
+
#
|
22
|
+
# Gets job results off a job broker. Returns nil if there are no results.
|
23
|
+
#
|
24
|
+
# worker_uid: the uid returned by workling when the work was dispatched
|
25
|
+
#
|
26
|
+
def retrieve(work_uid)
|
27
|
+
raise NotImplementedError.new("Implement retrieve(work_uid) in your client. ")
|
28
|
+
end
|
29
|
+
|
30
|
+
#
|
31
|
+
# Subscribe to job results in a job broker.
|
32
|
+
#
|
33
|
+
# worker_type:
|
34
|
+
#
|
35
|
+
def subscribe(work_type)
|
36
|
+
raise NotImplementedError.new("Implement subscribe(work_type) in your client. ")
|
37
|
+
end
|
38
|
+
|
39
|
+
#
|
40
|
+
# Opens a connection to the job broker.
|
41
|
+
#
|
42
|
+
def connect
|
43
|
+
raise NotImplementedError.new("Implement connect() in your client. ")
|
44
|
+
end
|
45
|
+
|
46
|
+
#
|
47
|
+
# Closes the connection to the job broker.
|
48
|
+
#
|
49
|
+
def close
|
50
|
+
raise NotImplementedError.new("Implement close() in your client. ")
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'workling/clients/base'
|
2
|
+
|
3
|
+
#
|
4
|
+
# This client can be used for all Queue Servers that speak Memcached, such as Starling.
|
5
|
+
#
|
6
|
+
# Wrapper for the memcache connection. The connection is made using fiveruns-memcache-client,
|
7
|
+
# or memcache-client, if this is not available. See the README for a discussion of the memcache
|
8
|
+
# clients.
|
9
|
+
#
|
10
|
+
# method_missing delegates all messages through to the underlying memcache connection.
|
11
|
+
#
|
12
|
+
module Workling
|
13
|
+
module Clients
|
14
|
+
class MemcacheQueueClient < Workling::Clients::Base
|
15
|
+
|
16
|
+
# the class with which the connection is instantiated
|
17
|
+
cattr_accessor :memcache_client_class
|
18
|
+
@@memcache_client_class ||= ::MemCache
|
19
|
+
|
20
|
+
# the url with which the memcache client expects to reach starling
|
21
|
+
attr_accessor :queueserver_urls
|
22
|
+
|
23
|
+
# the memcache connection object
|
24
|
+
attr_accessor :connection
|
25
|
+
|
26
|
+
#
|
27
|
+
# the client attempts to connect to queueserver using the configuration options found in
|
28
|
+
#
|
29
|
+
# Workling.config. this can be configured in config/workling.yml.
|
30
|
+
#
|
31
|
+
# the initialization code will raise an exception if memcache-client cannot connect
|
32
|
+
# to queueserver.
|
33
|
+
#
|
34
|
+
def connect
|
35
|
+
@queueserver_urls = Workling.config[:listens_on].split(',').map { |url| url ? url.strip : url }
|
36
|
+
options = [@queueserver_urls, Workling.config[:memcache_options]].compact
|
37
|
+
self.connection = MemcacheQueueClient.memcache_client_class.new(*options)
|
38
|
+
|
39
|
+
raise_unless_connected!
|
40
|
+
end
|
41
|
+
|
42
|
+
# closes the memcache connection
|
43
|
+
def close
|
44
|
+
self.connection.flush_all
|
45
|
+
self.connection.reset
|
46
|
+
end
|
47
|
+
|
48
|
+
# implements the client job request and retrieval
|
49
|
+
def request(key, value)
|
50
|
+
set(key, value)
|
51
|
+
end
|
52
|
+
|
53
|
+
def retrieve(key)
|
54
|
+
begin
|
55
|
+
get(key)
|
56
|
+
rescue MemCache::MemCacheError => e
|
57
|
+
# failed to enqueue, raise a workling error so that it propagates upwards
|
58
|
+
raise Workling::WorklingError.new("#{e.class.to_s} - #{e.message}")
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
# make sure we can actually connect to queueserver on the given port
|
64
|
+
def raise_unless_connected!
|
65
|
+
begin
|
66
|
+
self.connection.stats
|
67
|
+
rescue
|
68
|
+
raise Workling::QueueserverNotFoundError.new
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# delegates directly through to the memcache connection.
|
73
|
+
def method_missing(method, *args)
|
74
|
+
begin
|
75
|
+
self.connection.send(method, *args)
|
76
|
+
rescue MemCache::MemCacheError => e
|
77
|
+
raise Workling::WorklingConnectionError.new("#{e.class.to_s} - #{e.message}")
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,14 @@
|
|
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
|
+
Dir.glob(Workling.load_path.map { |p| "#{ p }/**/*.rb" }).each { |wling| require wling }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require "workling/remote/runners/not_remote_runner"
|
2
|
+
require "workling/remote/runners/spawn_runner"
|
3
|
+
require "workling/remote/runners/starling_runner"
|
4
|
+
require "workling/remote/runners/backgroundjob_runner"
|
5
|
+
|
6
|
+
require 'digest/md5'
|
7
|
+
|
8
|
+
#
|
9
|
+
# Scoping Module for Runners.
|
10
|
+
#
|
11
|
+
module Workling
|
12
|
+
module Remote
|
13
|
+
|
14
|
+
# set the desired runner here. this is initialized with Workling.default_runner.
|
15
|
+
mattr_accessor :dispatcher
|
16
|
+
|
17
|
+
# set the desired invoker. this class grabs work from the job broker and executes it.
|
18
|
+
mattr_accessor :invoker
|
19
|
+
@@invoker ||= Workling::Remote::Invokers::ThreadedPoller
|
20
|
+
|
21
|
+
# retrieve the dispatcher or instantiate it using the defaults
|
22
|
+
def self.dispatcher
|
23
|
+
@@dispatcher ||= Workling.default_runner
|
24
|
+
end
|
25
|
+
|
26
|
+
# generates a unique identifier for this particular job.
|
27
|
+
def self.generate_uid(clazz, method)
|
28
|
+
uid = ::Digest::MD5.hexdigest("#{ clazz }:#{ method }:#{ rand(1 << 64) }:#{ Time.now }")
|
29
|
+
"#{ clazz.to_s.tableize }/#{ method }/#{ uid }".split("/").join(":")
|
30
|
+
end
|
31
|
+
|
32
|
+
# dispatches to a workling. writes the :uid for this work into the options hash, so make
|
33
|
+
# sure you pass in a hash if you want write to a return store in your workling.
|
34
|
+
def self.run(clazz, method, options = {})
|
35
|
+
uid = Workling::Remote.generate_uid(clazz, method)
|
36
|
+
options[:uid] = uid if options.kind_of?(Hash) && !options[:uid]
|
37
|
+
Workling.find(clazz, method) # this line raises a WorklingError if the method does not exist.
|
38
|
+
dispatcher.run(clazz, method, options)
|
39
|
+
uid
|
40
|
+
end
|
41
|
+
end
|
42
|
+
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
|