did_workling 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. data/README.markdown +382 -0
  2. data/lib/rude_q/client.rb +11 -0
  3. data/lib/workling.rb +150 -0
  4. data/lib/workling/base.rb +59 -0
  5. data/lib/workling/clients/amqp_client.rb +40 -0
  6. data/lib/workling/clients/base.rb +54 -0
  7. data/lib/workling/clients/memcache_queue_client.rb +82 -0
  8. data/lib/workling/discovery.rb +14 -0
  9. data/lib/workling/remote.rb +42 -0
  10. data/lib/workling/remote/invokers/base.rb +124 -0
  11. data/lib/workling/remote/invokers/basic_poller.rb +41 -0
  12. data/lib/workling/remote/invokers/eventmachine_subscriber.rb +37 -0
  13. data/lib/workling/remote/invokers/threaded_poller.rb +149 -0
  14. data/lib/workling/remote/runners/backgroundjob_runner.rb +35 -0
  15. data/lib/workling/remote/runners/base.rb +42 -0
  16. data/lib/workling/remote/runners/client_runner.rb +45 -0
  17. data/lib/workling/remote/runners/not_remote_runner.rb +23 -0
  18. data/lib/workling/remote/runners/spawn_runner.rb +38 -0
  19. data/lib/workling/remote/runners/starling_runner.rb +13 -0
  20. data/lib/workling/return/store/base.rb +37 -0
  21. data/lib/workling/return/store/memory_return_store.rb +26 -0
  22. data/lib/workling/return/store/starling_return_store.rb +31 -0
  23. data/lib/workling/routing/base.rb +13 -0
  24. data/lib/workling/routing/class_and_method_routing.rb +55 -0
  25. data/test/class_and_method_routing_test.rb +18 -0
  26. data/test/clients/memory_queue_client.rb +36 -0
  27. data/test/discovery_test.rb +13 -0
  28. data/test/invoker_basic_poller_test.rb +29 -0
  29. data/test/invoker_eventmachine_subscription_test.rb +26 -0
  30. data/test/invoker_threaded_poller_test.rb +34 -0
  31. data/test/memcachequeue_client_test.rb +36 -0
  32. data/test/memory_return_store_test.rb +23 -0
  33. data/test/mocks/client.rb +9 -0
  34. data/test/mocks/logger.rb +5 -0
  35. data/test/mocks/spawn.rb +5 -0
  36. data/test/not_remote_runner_test.rb +11 -0
  37. data/test/remote_runner_test.rb +50 -0
  38. data/test/return_store_test.rb +18 -0
  39. data/test/runners/thread_runner.rb +22 -0
  40. data/test/spawn_runner_test.rb +10 -0
  41. data/test/starling_return_store_test.rb +29 -0
  42. data/test/starling_runner_test.rb +8 -0
  43. data/test/test_helper.rb +48 -0
  44. data/test/workers/analytics/invites.rb +10 -0
  45. data/test/workers/util.rb +15 -0
  46. 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