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