elecnix-workling 0.3.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 (61) hide show
  1. data/CHANGES.markdown +72 -0
  2. data/README.markdown +386 -0
  3. data/TODO.markdown +27 -0
  4. data/VERSION.yml +4 -0
  5. data/lib/rude_q/client.rb +11 -0
  6. data/lib/workling/base.rb +71 -0
  7. data/lib/workling/clients/amqp_client.rb +56 -0
  8. data/lib/workling/clients/base.rb +57 -0
  9. data/lib/workling/clients/memcache_queue_client.rb +83 -0
  10. data/lib/workling/discovery.rb +14 -0
  11. data/lib/workling/remote/invokers/base.rb +124 -0
  12. data/lib/workling/remote/invokers/basic_poller.rb +41 -0
  13. data/lib/workling/remote/invokers/eventmachine_subscriber.rb +41 -0
  14. data/lib/workling/remote/invokers/threaded_poller.rb +140 -0
  15. data/lib/workling/remote/runners/backgroundjob_runner.rb +35 -0
  16. data/lib/workling/remote/runners/base.rb +42 -0
  17. data/lib/workling/remote/runners/client_runner.rb +45 -0
  18. data/lib/workling/remote/runners/not_remote_runner.rb +23 -0
  19. data/lib/workling/remote/runners/rudeq_runner.rb +23 -0
  20. data/lib/workling/remote/runners/spawn_runner.rb +38 -0
  21. data/lib/workling/remote/runners/starling_runner.rb +13 -0
  22. data/lib/workling/remote.rb +42 -0
  23. data/lib/workling/return/store/base.rb +42 -0
  24. data/lib/workling/return/store/iterator.rb +24 -0
  25. data/lib/workling/return/store/memory_return_store.rb +26 -0
  26. data/lib/workling/return/store/rudeq_return_store.rb +24 -0
  27. data/lib/workling/return/store/starling_return_store.rb +31 -0
  28. data/lib/workling/routing/base.rb +13 -0
  29. data/lib/workling/routing/class_and_method_routing.rb +55 -0
  30. data/lib/workling/rudeq/client.rb +17 -0
  31. data/lib/workling/rudeq/poller.rb +116 -0
  32. data/lib/workling/rudeq.rb +7 -0
  33. data/lib/workling.rb +150 -0
  34. data/test/class_and_method_routing_test.rb +18 -0
  35. data/test/clients/memory_queue_client.rb +36 -0
  36. data/test/discovery_test.rb +13 -0
  37. data/test/invoker_basic_poller_test.rb +29 -0
  38. data/test/invoker_eventmachine_subscription_test.rb +26 -0
  39. data/test/invoker_threaded_poller_test.rb +34 -0
  40. data/test/memcachequeue_client_test.rb +36 -0
  41. data/test/memory_return_store_test.rb +32 -0
  42. data/test/mocks/client.rb +9 -0
  43. data/test/mocks/logger.rb +5 -0
  44. data/test/mocks/rude_queue.rb +9 -0
  45. data/test/mocks/spawn.rb +5 -0
  46. data/test/not_remote_runner_test.rb +11 -0
  47. data/test/remote_runner_test.rb +58 -0
  48. data/test/rescue_test.rb +24 -0
  49. data/test/return_store_test.rb +24 -0
  50. data/test/rudeq_client_test.rb +30 -0
  51. data/test/rudeq_poller_test.rb +14 -0
  52. data/test/rudeq_return_store_test.rb +20 -0
  53. data/test/rudeq_runner_test.rb +22 -0
  54. data/test/runners/thread_runner.rb +22 -0
  55. data/test/spawn_runner_test.rb +10 -0
  56. data/test/starling_return_store_test.rb +29 -0
  57. data/test/starling_runner_test.rb +8 -0
  58. data/test/test_helper.rb +50 -0
  59. data/test/workers/analytics/invites.rb +10 -0
  60. data/test/workers/util.rb +25 -0
  61. metadata +129 -0
@@ -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,42 @@
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
+
35
+ def iterator(key)
36
+ Workling::Return::Store::Iterator.new(key)
37
+ end
38
+
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,24 @@
1
+ #
2
+ # Iterator class for iterating over return values.
3
+ #
4
+ module Workling
5
+ module Return
6
+ module Store
7
+ class Iterator
8
+
9
+ include Enumerable
10
+
11
+ def initialize(uid)
12
+ @uid = uid
13
+ end
14
+
15
+ def each
16
+ while item = Workling.return.get(@uid)
17
+ yield item
18
+ end
19
+ end
20
+
21
+ end
22
+ end
23
+ end
24
+ 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 = Hash.new([])
14
+ end
15
+
16
+ def set(key, value)
17
+ self.sky[key] << value
18
+ end
19
+
20
+ def get(key)
21
+ self.sky[key].shift
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,24 @@
1
+ require 'workling/return/store/base'
2
+ require 'workling/rudeq/client'
3
+
4
+ module Workling
5
+ module Return
6
+ module Store
7
+ class RudeqReturnStore < Base
8
+ cattr_accessor :client
9
+
10
+ def initialize
11
+ self.class.client = Workling::Rudeq::Client.new
12
+ end
13
+
14
+ def set(key, value)
15
+ self.class.client.set(key, value)
16
+ end
17
+
18
+ def get(key)
19
+ self.class.client.get(key)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,31 @@
1
+ require 'workling/return/store/base'
2
+ require 'workling/clients/memcache_queue_client'
3
+
4
+ #
5
+ # Recommended Return Store if you are using the Starling Runner. This
6
+ # Simply sets and gets values against queues. 'key' is the name of the respective Queue.
7
+ #
8
+ module Workling
9
+ module Return
10
+ module Store
11
+ class StarlingReturnStore < Base
12
+ cattr_accessor :client
13
+
14
+ def initialize
15
+ self.client = Workling::Clients::MemcacheQueueClient.new
16
+ self.client.connect
17
+ end
18
+
19
+ # set a value in the queue 'key'.
20
+ def set(key, value)
21
+ self.class.client.set(key, value)
22
+ end
23
+
24
+ # get a value from starling queue 'key'.
25
+ def get(key)
26
+ self.class.client.get(key)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,13 @@
1
+ #
2
+ # Base Class for Routing. Routing takes the worker method TestWorker#something,
3
+ # and serializes the signature in some way.
4
+ #
5
+ module Workling
6
+ module Routing
7
+ class Base < Hash
8
+ def method_name
9
+ raise Exception.new("method_name not implemented.")
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,55 @@
1
+ require 'workling/routing/base'
2
+
3
+ #
4
+ # Holds a hash of routes. Each Worker method has a corresponding hash entry after building.
5
+ #
6
+ module Workling
7
+ module Routing
8
+ class ClassAndMethodRouting < Base
9
+
10
+ # initializes and builds routing hash.
11
+ def initialize
12
+ super
13
+
14
+ build
15
+ end
16
+
17
+ # returns the worker method name, given the routing string.
18
+ def method_name(queue)
19
+ queue.split("__").last
20
+ end
21
+
22
+ # returns the routing string, given a class and method. delegating.
23
+ def queue_for(clazz, method)
24
+ ClassAndMethodRouting.queue_for(clazz, method)
25
+ end
26
+
27
+ # returns the routing string, given a class and method.
28
+ def self.queue_for(clazz, method)
29
+ "#{ clazz.to_s.tableize }/#{ method }".split("/").join("__") # Don't split with : because it messes up memcache stats
30
+ end
31
+
32
+ # returns all routed
33
+ def queue_names
34
+ self.keys
35
+ end
36
+
37
+ # dare you to remove this! go on!
38
+ def queue_names_routing_class(clazz)
39
+ self.select { |x, y| y.is_a?(clazz) }.map { |x, y| x }
40
+ end
41
+
42
+ private
43
+ def build
44
+ Workling::Discovery.discovered.each do |clazz|
45
+ methods = clazz.instance_methods(false)
46
+ methods.each do |method|
47
+ next if method == 'create' # Skip the create method
48
+ queue = queue_for(clazz, method)
49
+ self[queue] = clazz.new
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,17 @@
1
+ require 'workling/rudeq'
2
+
3
+ module Workling
4
+ module Rudeq
5
+ class Client
6
+ attr_reader :queue
7
+
8
+ def initialize
9
+ @queue = Workling::Rudeq.config[:queue_class].constantize
10
+ end
11
+
12
+ def method_missing(method, *args)
13
+ @queue.send(method, *args)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,116 @@
1
+ require 'workling/rudeq'
2
+
3
+ module Workling
4
+ module Rudeq
5
+
6
+ class Poller
7
+
8
+ cattr_accessor :sleep_time # Seconds to sleep before looping
9
+ cattr_accessor :reset_time # Seconds to wait while resetting connection
10
+
11
+ def initialize(routing)
12
+ Poller.sleep_time = Workling::Rudeq.config[:sleep_time] || 2
13
+ Poller.reset_time = Workling::Rudeq.config[:reset_time] || 30
14
+
15
+ @routing = routing
16
+ @workers = ThreadGroup.new
17
+ end
18
+
19
+ def logger
20
+ Workling::Base.logger
21
+ end
22
+
23
+ def listen
24
+
25
+ # Allow concurrency for our tasks
26
+ ActiveRecord::Base.allow_concurrency = true
27
+
28
+ # Create a thread for each worker.
29
+ Workling::Discovery.discovered.each do |clazz|
30
+ logger.debug("Discovered listener #{clazz}")
31
+ @workers.add(Thread.new(clazz) { |c| clazz_listen(c) })
32
+ end
33
+
34
+ # Wait for all workers to complete
35
+ @workers.list.each { |t| t.join }
36
+
37
+ # Clean up all the connections.
38
+ ActiveRecord::Base.verify_active_connections!
39
+ end
40
+
41
+ # gracefully stop processing
42
+ def stop
43
+ @workers.list.each { |w| w[:shutdown] = true }
44
+ end
45
+
46
+ ##
47
+ ## Thread procs
48
+ ##
49
+
50
+ # Listen for one worker class
51
+ def clazz_listen(clazz)
52
+
53
+ logger.debug("Listener thread #{clazz.name} started")
54
+
55
+ # Read thread configuration if available
56
+ if Rudeq.config.has_key?(:listeners)
57
+ if Rudeq.config[:listeners].has_key?(clazz.to_s)
58
+ config = Rudeq.config[:listeners][clazz.to_s].symbolize_keys
59
+ thread_sleep_time = config[:sleep_time] if config.has_key?(:sleep_time)
60
+ end
61
+ end
62
+
63
+ hread_sleep_time ||= self.class.sleep_time
64
+
65
+ connection = Workling::Rudeq::Client.new
66
+ puts "** Starting Workling::Rudeq::Client for #{clazz.name} queue"
67
+
68
+ # Start dispatching those messages
69
+ while (!Thread.current[:shutdown]) do
70
+ begin
71
+
72
+ # Keep MySQL connection alive
73
+ unless ActiveRecord::Base.connection.active?
74
+ unless ActiveRecord::Base.connection.reconnect!
75
+ logger.fatal("FAILED - Database not available")
76
+ break
77
+ end
78
+ end
79
+
80
+ # Dispatch and process the messages
81
+ n = dispatch!(connection, clazz)
82
+ logger.debug("Listener thread #{clazz.name} processed #{n.to_s} queue items") if n > 0
83
+ sleep(self.class.sleep_time) unless n > 0
84
+ end
85
+ end
86
+
87
+ logger.debug("Listener thread #{clazz.name} ended")
88
+ end
89
+
90
+ # Dispatcher for one worker class.
91
+ # Returns the number of worker methods called
92
+ def dispatch!(connection, clazz)
93
+ n = 0
94
+ for queue in @routing.queue_names_routing_class(clazz)
95
+ begin
96
+ result = connection.get(queue)
97
+ if result
98
+ n += 1
99
+ handler = @routing[queue]
100
+ method_name = @routing.method_name(queue)
101
+ logger.debug("Calling #{handler.class.to_s}\##{method_name}(#{result.inspect})")
102
+ handler.send(method_name, result)
103
+ end
104
+ rescue
105
+ logger.error("FAILED to connect with queue #{ queue }: #{ e } }")
106
+ raise e
107
+ rescue Object => e
108
+ logger.error("FAILED to process queue #{ queue }. #{ @routing[queue] } could not handle invocation of #{ @routing.method_name(queue) } with #{ result.inspect }: #{ e }.\n#{ e.backtrace.join("\n") }")
109
+ end
110
+ end
111
+
112
+ return n
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,7 @@
1
+ module Workling
2
+ module Rudeq
3
+ def self.config
4
+ @@config ||= {:queue_class => "RudeQueue"}
5
+ end
6
+ end
7
+ end
data/lib/workling.rb ADDED
@@ -0,0 +1,150 @@
1
+ #
2
+ # I can haz am in your Workling are belong to us!
3
+ #
4
+ module Workling
5
+ class WorklingError < StandardError; end
6
+ class WorklingNotFoundError < WorklingError; end
7
+ class WorklingConnectionError < WorklingError; end
8
+ class QueueserverNotFoundError < WorklingError
9
+ def initialize
10
+ super "config/workling.yml configured to connect to queue server on #{ Workling.config[:listens_on] } for this environment. could not connect to queue server on this host:port. for starling users: pass starling the port with -p flag when starting it. If you don't want to use Starling, then explicitly set Workling::Remote.dispatcher (see README for an example)"
11
+ end
12
+ end
13
+
14
+ class ConfigurationError < WorklingError
15
+ def initialize
16
+ super File.exist?(File.join(RAILS_ROOT, 'config', 'starling.yml')) ?
17
+ "config/starling.yml has been depracated. rename your config file to config/workling.yml then try again!" :
18
+ "config/workling.yml could not be loaded. check out README.markdown to see what this file should contain. "
19
+ end
20
+ end
21
+
22
+ mattr_accessor :load_path
23
+ @@load_path = [ File.expand_path(File.join(File.dirname(__FILE__), '../../../../app/workers')) ]
24
+ VERSION = "0.4.2.2"
25
+
26
+ #
27
+ # determine the runner to use if nothing is specifically set. workling will try to detect
28
+ # starling, spawn, or bj, in that order. if none of these are found, notremoterunner will
29
+ # be used.
30
+ #
31
+ # this can be overridden by setting Workling::Remote.dispatcher, eg:
32
+ # Workling::Remote.dispatcher = Workling::Remote::Runners::StarlingRunner.new
33
+ #
34
+ def self.default_runner
35
+ if RAILS_ENV == "test"
36
+ Workling::Remote::Runners::NotRemoteRunner.new
37
+ elsif starling_installed?
38
+ Workling::Remote::Runners::StarlingRunner.new
39
+ elsif spawn_installed?
40
+ Workling::Remote::Runners::SpawnRunner.new
41
+ elsif bj_installed?
42
+ Workling::Remote::Runners::BackgroundjobRunner.new
43
+ else
44
+ Workling::Remote::Runners::NotRemoteRunner.new
45
+ end
46
+ end
47
+
48
+ #
49
+ # gets the worker instance, given a class. the optional method argument will cause an
50
+ # exception to be raised if the worker instance does not respoind to said method.
51
+ #
52
+ def self.find(clazz, method = nil)
53
+ begin
54
+ inst = clazz.to_s.camelize.constantize.new
55
+ rescue NameError
56
+ raise_not_found(clazz, method)
57
+ end
58
+ raise_not_found(clazz, method) if method && !inst.respond_to?(method)
59
+ inst
60
+ end
61
+
62
+ # returns Workling::Return::Store.instance.
63
+ def self.return
64
+ Workling::Return::Store.instance
65
+ end
66
+
67
+ # is spawn installed?
68
+ def self.spawn_installed?
69
+ begin
70
+ require 'spawn'
71
+ rescue LoadError
72
+ end
73
+
74
+ Object.const_defined? "Spawn"
75
+ end
76
+
77
+ # is starling installed?
78
+ def self.starling_installed?
79
+ begin
80
+ require 'starling'
81
+ rescue LoadError
82
+ end
83
+
84
+ Object.const_defined? "Starling"
85
+ end
86
+
87
+ # is bj installed?
88
+ def self.bj_installed?
89
+ Object.const_defined? "Bj"
90
+ end
91
+
92
+ # tries to load fiveruns-memcache-client. if this isn't found,
93
+ # memcache-client is searched for. if that isn't found, don't do anything.
94
+ def self.try_load_a_memcache_client
95
+ begin
96
+ gem 'fiveruns-memcache-client'
97
+ require 'memcache'
98
+ rescue Gem::LoadError
99
+ begin
100
+ gem 'memcache-client'
101
+ require 'memcache'
102
+ rescue Gem::LoadError
103
+ Workling::Base.logger.info "WORKLING: couldn't find a memcache client - you need one for the starling runner. "
104
+ end
105
+ end
106
+ end
107
+
108
+ # attempts to load amqp and writes out descriptive error message if not present
109
+ def self.try_load_an_amqp_client
110
+ begin
111
+ require 'mq'
112
+ rescue Exception => e
113
+ raise WorklingError.new(
114
+ "WORKLING: couldn't find the ruby amqp client - you need it for the amqp runner. " \
115
+ "Install from github: gem sources -a http://gems.github.com/ && sudo gem install tmm1-amqp "
116
+ )
117
+ end
118
+ end
119
+
120
+ #
121
+ # returns a config hash. reads RAILS_ROOT/config/workling.yml
122
+ #
123
+ def self.config
124
+ begin
125
+ config_path = File.join(RAILS_ROOT, 'config', 'workling.yml')
126
+ @@config ||= YAML.load_file(config_path)[RAILS_ENV || 'development'].symbolize_keys
127
+ @@config[:memcache_options].symbolize_keys! if @@config[:memcache_options]
128
+ @@config
129
+ rescue
130
+ # config files could not be read correctly
131
+ raise ConfigurationError.new
132
+ end
133
+ end
134
+
135
+ #
136
+ # Raises exceptions thrown inside of the worker. normally, these are logged to
137
+ # logger.error. it's easy to miss these log calls while developing, though.
138
+ #
139
+ mattr_accessor :raise_exceptions
140
+ @@raise_exceptions = (RAILS_ENV == "test" || RAILS_ENV == "development")
141
+
142
+ def self.raise_exceptions?
143
+ @@raise_exceptions
144
+ end
145
+
146
+ private
147
+ def self.raise_not_found(clazz, method)
148
+ raise Workling::WorklingNotFoundError.new("could not find #{ clazz }:#{ method } workling. ")
149
+ end
150
+ end