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,37 @@
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
+ run(route, args)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ def stop
32
+ EM.stop if EM.reactor_running?
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,149 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'workling/remote/invokers/base'
3
+
4
+ #
5
+ # A threaded polling Invoker.
6
+ #
7
+ # TODO: refactor this to make use of the base class.
8
+ #
9
+ module Workling
10
+ module Remote
11
+ module Invokers
12
+ class ThreadedPoller < Workling::Remote::Invokers::Base
13
+
14
+ cattr_accessor :sleep_time, :reset_time
15
+
16
+ def initialize(routing, client_class)
17
+ super
18
+
19
+ ThreadedPoller.sleep_time = Workling.config[:sleep_time] || 2
20
+ ThreadedPoller.reset_time = Workling.config[:reset_time] || 30
21
+
22
+ @workers = ThreadGroup.new
23
+ @mutex = Mutex.new
24
+ end
25
+
26
+ def listen
27
+ # Allow concurrency for our tasks
28
+ ActiveRecord::Base.allow_concurrency = true
29
+
30
+ # Create a thread for each worker.
31
+ Workling::Discovery.discovered.each do |clazz|
32
+ logger.debug("Discovered listener #{clazz}")
33
+ @workers.add(Thread.new(clazz) { |c| clazz_listen(c) })
34
+ end
35
+
36
+ # Wait for all workers to complete
37
+ @workers.list.each { |t| t.join }
38
+
39
+ logger.debug("Reaped listener threads. ")
40
+
41
+ # Clean up all the connections.
42
+ ActiveRecord::Base.verify_active_connections!
43
+ logger.debug("Cleaned up connection: out!")
44
+ end
45
+
46
+ # Check if all Worker threads have been started.
47
+ def started?
48
+ logger.debug("checking if started... list size is #{ worker_threads }")
49
+ Workling::Discovery.discovered.size == worker_threads
50
+ end
51
+
52
+ # number of worker threads running
53
+ def worker_threads
54
+ @workers.list.size
55
+ end
56
+
57
+ # Gracefully stop processing
58
+ def stop
59
+ logger.info("stopping threaded poller...")
60
+ sleep 1 until started? # give it a chance to start up before shutting down.
61
+ logger.info("Giving Listener Threads a chance to shut down. This may take a while... ")
62
+ @workers.list.each { |w| w[:shutdown] = true }
63
+ logger.info("Listener threads were shut down. ")
64
+ end
65
+
66
+ # Listen for one worker class
67
+ def clazz_listen(clazz)
68
+ logger.debug("Listener thread #{clazz.name} started")
69
+
70
+ # Read thread configuration if available
71
+ if Workling.config.has_key?(:listeners)
72
+ if Workling.config[:listeners].has_key?(clazz.to_s)
73
+ config = Workling.config[:listeners][clazz.to_s].symbolize_keys
74
+ thread_sleep_time = config[:sleep_time] if config.has_key?(:sleep_time)
75
+ end
76
+ end
77
+
78
+ hread_sleep_time ||= self.class.sleep_time
79
+
80
+ # Setup connection to client (one per thread)
81
+ connection = @client_class.new
82
+ connection.connect
83
+ logger.info("** Starting client #{ connection.class } for #{clazz.name} queue")
84
+
85
+ # Start dispatching those messages
86
+ while (!Thread.current[:shutdown]) do
87
+ begin
88
+
89
+ # Thanks for this Brent!
90
+ #
91
+ # ...Just a heads up, due to how rails’ MySQL adapter handles this
92
+ # call ‘ActiveRecord::Base.connection.active?’, you’ll need
93
+ # to wrap the code that checks for a connection in in a mutex.
94
+ #
95
+ # ....I noticed this while working with a multi-core machine that
96
+ # was spawning multiple workling threads. Some of my workling
97
+ # threads would hit serious issues at this block of code without
98
+ # the mutex.
99
+ #
100
+ @mutex.synchronize do
101
+ ActiveRecord::Base.connection.verify! # Keep MySQL connection alive
102
+ unless ActiveRecord::Base.connection.active?
103
+ logger.fatal("Failed - Database not available!")
104
+ end
105
+ end
106
+
107
+ # Dispatch and process the messages
108
+ n = dispatch!(connection, clazz)
109
+ logger.debug("Listener thread #{clazz.name} processed #{n.to_s} queue items") if n > 0
110
+ sleep(self.class.sleep_time) unless n > 0
111
+
112
+ # If there is a memcache error, hang for a bit to give it a chance to fire up again
113
+ # and reset the connection.
114
+ rescue Workling::WorklingConnectionError
115
+ logger.warn("Listener thread #{clazz.name} failed to connect. Resetting connection.")
116
+ sleep(self.class.reset_time)
117
+ connection.reset
118
+ end
119
+ end
120
+
121
+ logger.debug("Listener thread #{clazz.name} ended")
122
+ end
123
+
124
+ # Dispatcher for one worker class. Will throw MemCacheError if unable to connect.
125
+ # Returns the number of worker methods called
126
+ def dispatch!(connection, clazz)
127
+ n = 0
128
+ for queue in @routing.queue_names_routing_class(clazz)
129
+ begin
130
+ result = connection.retrieve(queue)
131
+ if result
132
+ n += 1
133
+ handler = @routing[queue]
134
+ method_name = @routing.method_name(queue)
135
+ logger.debug("Calling #{handler.class.to_s}\##{method_name}(#{result.inspect})")
136
+ handler.dispatch_to_worker_method(method_name, result)
137
+ end
138
+ rescue MemCache::MemCacheError => e
139
+ logger.error("FAILED to connect with queue #{ queue }: #{ e } }")
140
+ raise e
141
+ end
142
+ end
143
+
144
+ return n
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,35 @@
1
+ require 'workling/remote/runners/base'
2
+
3
+ #
4
+ # Use Ara Howards BackgroundJob to run the work. BackgroundJob loads Rails once per requested Job.
5
+ # It persists over the database, and there is no requirement for separate processes to be started.
6
+ # Since rails has to load before each request, it takes a moment for the job to run.
7
+ #
8
+ module Workling
9
+ module Remote
10
+ module Runners
11
+ class BackgroundjobRunner < Workling::Remote::Runners::Base
12
+ cattr_accessor :routing
13
+
14
+ def initialize
15
+ BackgroundjobRunner.routing =
16
+ Workling::Routing::ClassAndMethodRouting.new
17
+ end
18
+
19
+ # passes the job to bj by serializing the options to xml and passing them to
20
+ # ./script/bj_invoker.rb, which in turn routes the deserialized args to the
21
+ # appropriate worker.
22
+ def run(clazz, method, options = {})
23
+ stdin = @@routing.queue_for(clazz, method) +
24
+ " " +
25
+ options.to_xml(:indent => 0, :skip_instruct => true)
26
+
27
+ Bj.submit "./script/runner ./script/bj_invoker.rb",
28
+ :stdin => stdin
29
+
30
+ return nil # that means nothing!
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,42 @@
1
+ #
2
+ # Base class for Workling Runners.
3
+ #
4
+ # Runners must subclass this and implement the method
5
+ #
6
+ # Workling::Remote::Runners::Base#run(clazz, method, options = {})
7
+ #
8
+ # which is responsible for pushing the requested job into the background. Depending
9
+ # on the Runner, this may require other code to dequeue the job. The actual
10
+ # invocation of the runner should be done like this:
11
+ #
12
+ # Workling.find(clazz, method).dispatch_to_worker_method(method, options)
13
+ #
14
+ # This ensures for consistent logging and handling of propagated exceptions. You can
15
+ # also call the convenience method
16
+ #
17
+ # Workling::Remote::Runners::Base#dispatch!(clazz, method, options)
18
+ #
19
+ # which invokes this for you.
20
+ #
21
+ module Workling
22
+ module Remote
23
+ module Runners
24
+ class Base
25
+
26
+ # runner uses this to connect to a job broker
27
+ cattr_accessor :client
28
+
29
+ # default logger defined in Workling::Base.logger
30
+ def logger
31
+ Workling::Base.logger
32
+ end
33
+
34
+ # find the worker instance and invoke it. Invoking the worker method like this ensures for
35
+ # consistent logging and handling of propagated exceptions.
36
+ def dispatch!(clazz, method, options)
37
+ Workling.find(clazz, method).dispatch_to_worker_method(method, options)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,45 @@
1
+ require 'workling/remote/runners/base'
2
+ require 'workling/clients/memcache_queue_client'
3
+
4
+ #
5
+ # Runs Jobs over a Client. The client should be a subclass of Workling::Client::Base.
6
+ # Set the client like this:
7
+ #
8
+ # Workling::Remote::Runners::ClientRunner.client = Workling::Clients::AmqpClient.new
9
+ #
10
+ # Jobs are dispatched by requesting them on the Client. The Runner takes care of mapping of queue names to worker code.
11
+ # this is done with Workling::ClassAndMethodRouting, but you can use your own by sublassing Workling::Routing.
12
+ # Don’t worry about any of this if you’re not dealing directly with the queues.
13
+ #
14
+ # There’s a workling-client daemon that uses the configured invoker to retrieve work and dispatching these to the
15
+ # responsible workers. If you intend to run this on a remote machine, then just check out your rails project
16
+ # there and start up the workling client like this: ruby script/workling_client run.
17
+ #
18
+ module Workling
19
+ module Remote
20
+ module Runners
21
+ class ClientRunner < Workling::Remote::Runners::Base
22
+
23
+ # Routing class. Workling::Routing::ClassAndMethodRouting.new by default.
24
+ cattr_accessor :routing
25
+ @@routing ||= Workling::Routing::ClassAndMethodRouting.new
26
+
27
+ # The workling Client class. Workling::Clients::MemcacheQueueClient.new by default.
28
+ cattr_accessor :client
29
+ @@client ||= Workling::Clients::MemcacheQueueClient.new
30
+
31
+ # enqueues the job onto the client
32
+ def run(clazz, method, options = {})
33
+
34
+ # neet to connect in here as opposed to the constructor, since the EM loop is
35
+ # not available there.
36
+ @connected ||= self.class.client.connect
37
+
38
+ self.class.client.request(@@routing.queue_for(clazz, method), options)
39
+
40
+ return nil
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,23 @@
1
+ require 'workling/remote/runners/base'
2
+
3
+ #
4
+ # directly dispatches to the worker method, in-process. options are first marshalled then dumped
5
+ # in order to simulate the sideeffects of a remote call.
6
+ #
7
+ module Workling
8
+ module Remote
9
+ module Runners
10
+ class NotRemoteRunner < Workling::Remote::Runners::Base
11
+
12
+ # directly dispatches to the worker method, in-process. options are first marshalled then dumped
13
+ # in order to simulate the sideeffects of a remote call.
14
+ def run(clazz, method, options = {})
15
+ options = Marshal.load(Marshal.dump(options)) # get this to behave more like the remote runners
16
+ dispatch!(clazz, method, options)
17
+
18
+ return nil # nada. niente.
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,38 @@
1
+ require 'workling/remote/runners/base'
2
+
3
+ #
4
+ # Run the job over the spawn plugin. Refer to the README for instructions on
5
+ # installing Spawn.
6
+ #
7
+ # Spawn forks the entire process once for each job. This means that the job starts
8
+ # with a very low latency, but takes up more memory for each job.
9
+ #
10
+ # It's also possible to configure Spawn to start a Thread for each job. Do this
11
+ # by setting
12
+ #
13
+ # Workling::Remote::Runners::SpawnRunner.options = { :method => :thread }
14
+ #
15
+ # Have a look at the Spawn README to find out more about the characteristics of this.
16
+ #
17
+ module Workling
18
+ module Remote
19
+ module Runners
20
+ class SpawnRunner < Workling::Remote::Runners::Base
21
+ cattr_accessor :options
22
+
23
+ # use thread for development and test modes. easier to hunt down exceptions that way.
24
+ @@options = { :method => (RAILS_ENV == "test" || RAILS_ENV == "development" ? :thread : :fork) }
25
+ include Spawn if Workling.spawn_installed?
26
+
27
+ # dispatches to Spawn, using the :fork option.
28
+ def run(clazz, method, options = {})
29
+ spawn(SpawnRunner.options) do # exceptions are trapped in here.
30
+ dispatch!(clazz, method, options)
31
+ end
32
+
33
+ return nil # that means nothing!
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,13 @@
1
+ require 'workling/remote/runners/client_runner'
2
+
3
+ #
4
+ # DEPRECATED. Should use ClientRunner instead.
5
+ #
6
+ module Workling
7
+ module Remote
8
+ module Runners
9
+ class StarlingRunner < Workling::Remote::Runners::ClientRunner
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,37 @@
1
+ #
2
+ # Basic interface for getting and setting Data which needs to be passed between Workers and
3
+ # client code.
4
+ #
5
+ module Workling
6
+ module Return
7
+ module Store
8
+ mattr_accessor :instance
9
+
10
+ # set a value in the store with the given key. delegates to the returnstore.
11
+ def self.set(key, value)
12
+ self.instance.set(key, value)
13
+ end
14
+
15
+ # get a value from the store. this should be destructive. delegates to the returnstore.
16
+ def self.get(key)
17
+ self.instance.get(key)
18
+ end
19
+
20
+ #
21
+ # Base Class for Return Stores. Subclasses need to implement set and get.
22
+ #
23
+ class Base
24
+
25
+ # set a value in the store with the given key.
26
+ def set(key, value)
27
+ raise NotImplementedError.new("set(key, value) not implemented in #{ self.class }")
28
+ end
29
+
30
+ # get a value from the store. this should be destructive.
31
+ def get(key)
32
+ raise NotImplementedError.new("get(key) not implemented in #{ self.class }")
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,26 @@
1
+ require 'workling/return/store/base'
2
+
3
+ #
4
+ # Stores directly into memory. This is for tests only - not for production use. aight?
5
+ #
6
+ module Workling
7
+ module Return
8
+ module Store
9
+ class MemoryReturnStore < Base
10
+ attr_accessor :sky
11
+
12
+ def initialize
13
+ self.sky = {}
14
+ end
15
+
16
+ def set(key, value)
17
+ self.sky[key] = value
18
+ end
19
+
20
+ def get(key)
21
+ self.sky.delete(key)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end