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