did_workling 0.0.1
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.
- 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
|