workling 0.4.9.7

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 (43) hide show
  1. data/CHANGES.markdown +82 -0
  2. data/README.markdown +543 -0
  3. data/TODO.markdown +27 -0
  4. data/VERSION.yml +4 -0
  5. data/bin/workling_client +29 -0
  6. data/contrib/bj_invoker.rb +11 -0
  7. data/contrib/starling_status.rb +37 -0
  8. data/lib/extensions/cattr_accessor.rb +51 -0
  9. data/lib/extensions/mattr_accessor.rb +55 -0
  10. data/lib/workling.rb +213 -0
  11. data/lib/workling/base.rb +110 -0
  12. data/lib/workling/clients/amqp_client.rb +51 -0
  13. data/lib/workling/clients/amqp_exchange_client.rb +58 -0
  14. data/lib/workling/clients/backgroundjob_client.rb +25 -0
  15. data/lib/workling/clients/base.rb +89 -0
  16. data/lib/workling/clients/broker_base.rb +63 -0
  17. data/lib/workling/clients/memcache_queue_client.rb +104 -0
  18. data/lib/workling/clients/memory_queue_client.rb +34 -0
  19. data/lib/workling/clients/not_client.rb +14 -0
  20. data/lib/workling/clients/not_remote_client.rb +17 -0
  21. data/lib/workling/clients/rude_q_client.rb +47 -0
  22. data/lib/workling/clients/spawn_client.rb +46 -0
  23. data/lib/workling/clients/sqs_client.rb +163 -0
  24. data/lib/workling/clients/thread_client.rb +18 -0
  25. data/lib/workling/clients/xmpp_client.rb +110 -0
  26. data/lib/workling/discovery.rb +16 -0
  27. data/lib/workling/invokers/amqp_single_subscriber.rb +42 -0
  28. data/lib/workling/invokers/base.rb +124 -0
  29. data/lib/workling/invokers/basic_poller.rb +38 -0
  30. data/lib/workling/invokers/eventmachine_subscriber.rb +38 -0
  31. data/lib/workling/invokers/looped_subscriber.rb +34 -0
  32. data/lib/workling/invokers/thread_pool_poller.rb +165 -0
  33. data/lib/workling/invokers/threaded_poller.rb +149 -0
  34. data/lib/workling/remote.rb +38 -0
  35. data/lib/workling/return/store/base.rb +42 -0
  36. data/lib/workling/return/store/iterator.rb +24 -0
  37. data/lib/workling/return/store/memory_return_store.rb +24 -0
  38. data/lib/workling/return/store/starling_return_store.rb +30 -0
  39. data/lib/workling/routing/base.rb +13 -0
  40. data/lib/workling/routing/class_and_method_routing.rb +55 -0
  41. data/lib/workling/routing/static_routing.rb +43 -0
  42. data/lib/workling_daemon.rb +111 -0
  43. metadata +96 -0
data/TODO.markdown ADDED
@@ -0,0 +1,27 @@
1
+ # Todos for 0.5.0
2
+
3
+ * add a linting runner for tests. should check that no ar objects are being passed around
4
+ * add a mechanism for requiring models, for those people who insist on passing models across the wire
5
+ * add reloading of workers if Rails.reload?
6
+ * write some code that knows if the client should be started, and gives out a warning
7
+ * add a configuration option for SERVER/CLIENT
8
+ * add phusion daemon starter option so that workling_client doesn't need to be started manually on SERVER
9
+ * write some more documentation on the above issues and on standard remote setup.
10
+ * create a public forum, rdoc site
11
+ * try to reduce user error in setting environments correctly
12
+ * add beanstalkd runner
13
+ * refactor starling* to be memcache*. add aliased classes into deprecated.rb.
14
+ * look into create method. is this being called more often than intended?
15
+ * add some monit and god scripts as starters
16
+ * try to catch more user setup errors which lead to worker code not being called
17
+ * add json as a marshaling option for the amqp client.
18
+
19
+ # Todos for 1.0
20
+
21
+ * gemify
22
+ * move all runner/invoker implementations out of workling
23
+ * move backend discovery code out of workling
24
+ * decide on a single backend to include in workling
25
+ * merb support
26
+ * test on jruby
27
+ * more runners: sqs
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :minor: 4
3
+ :patch: 2
4
+ :major: 0
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rubygems'
3
+ require 'daemons'
4
+ require File.join(File.dirname(__FILE__), '../lib/workling_daemon')
5
+
6
+ daemon_options = {
7
+ :app_name => "workling",
8
+ :dir_mode => :normal,
9
+ :dir => File.join(Dir.pwd, 'log'),
10
+ :log_output => true,
11
+ :multiple => false,
12
+ :backtrace => true,
13
+ :monitor => false
14
+ }.merge(WorklingDaemon.parse_daemon_options(ARGV))
15
+
16
+ workling_options = {
17
+ :client_class => "memcache_queue",
18
+ :invoker_class => "threaded_poller",
19
+ :routing_class => "class_and_method",
20
+ :rails_root => Dir.pwd,
21
+ :load_path => ['app/workers/**/*.rb'],
22
+ :rails_env => (ENV['RAILS_ENV'] || "development").dup,
23
+ :no_rails => false
24
+ }.merge(WorklingDaemon.parse_workling_options(ARGV))
25
+
26
+ Daemons.run_proc(daemon_options[:app_name], daemon_options) do
27
+ Dir.chdir(workling_options[:rails_root])
28
+ WorklingDaemon.run(workling_options)
29
+ end
@@ -0,0 +1,11 @@
1
+ @routing = Workling::Routing::ClassAndMethodRouting.new
2
+ unnormalized = REXML::Text::unnormalize(STDIN.read)
3
+ message, command, args = *unnormalized.match(/(^[^ ]*) (.*)/)
4
+ options = Hash.from_xml(args)["hash"]
5
+
6
+ if workling = @routing[command]
7
+ options = options.symbolize_keys
8
+ method_name = @routing.method_name(command)
9
+
10
+ workling.dispatch_to_worker_method(method_name, options)
11
+ end
@@ -0,0 +1,37 @@
1
+ require 'pp'
2
+
3
+ puts '=> Loading Rails...'
4
+
5
+ require File.dirname(__FILE__) + '/../config/environment'
6
+ require File.dirname(__FILE__) + '/../vendor/plugins/workling/lib/workling/remote/invokers/basic_poller'
7
+ require File.dirname(__FILE__) + '/../vendor/plugins/workling/lib/workling/routing/class_and_method_routing'
8
+
9
+ puts '** Rails loaded.'
10
+
11
+ trap(:INT) { exit }
12
+
13
+ client = Workling::Clients::MemcacheQueueClient.new
14
+
15
+ begin
16
+ client.connect
17
+ client.reset
18
+
19
+ client.stats # do this so that connection is shown as established below.
20
+
21
+ puts "Queue state:"
22
+ pp client.inspect
23
+ pp "Active?: #{client.active?}"
24
+ pp "Read Only?: #{client.readonly?}"
25
+ puts ""
26
+ puts "Servers:"
27
+ pp client.servers
28
+ puts ""
29
+ puts "Queue stats:"
30
+ pp client.stats
31
+
32
+ puts "\nThread Stats:"
33
+ pp Thread.list
34
+ ensure
35
+ puts '** Exiting'
36
+ client.close
37
+ end
@@ -0,0 +1,51 @@
1
+ # Extends the class object with class and instance accessors for class attributes,
2
+ # just like the native attr* accessors for instance attributes.
3
+ #
4
+ # class Person
5
+ # cattr_accessor :hair_colors
6
+ # end
7
+ #
8
+ # Person.hair_colors = [:brown, :black, :blonde, :red]
9
+ class Class
10
+ def cattr_reader(*syms)
11
+ syms.flatten.each do |sym|
12
+ next if sym.is_a?(Hash)
13
+ class_eval(<<-EOS, __FILE__, __LINE__)
14
+ unless defined? @@#{sym} # unless defined? @@hair_colors
15
+ @@#{sym} = nil # @@hair_colors = nil
16
+ end # end
17
+ #
18
+ def self.#{sym} # def self.hair_colors
19
+ @@#{sym} # @@hair_colors
20
+ end # end
21
+ #
22
+ def #{sym} # def hair_colors
23
+ @@#{sym} # @@hair_colors
24
+ end # end
25
+ EOS
26
+ end
27
+ end
28
+
29
+ def cattr_writer(*syms)
30
+ syms.flatten.each do |sym|
31
+ class_eval(<<-EOS, __FILE__, __LINE__)
32
+ unless defined? @@#{sym} # unless defined? @@hair_colors
33
+ @@#{sym} = nil # @@hair_colors = nil
34
+ end # end
35
+ #
36
+ def self.#{sym}=(obj) # def self.hair_colors=(obj)
37
+ @@#{sym} = obj # @@hair_colors = obj
38
+ end # end
39
+ #
40
+ def #{sym}=(obj) # def hair_colors=(obj)
41
+ @@#{sym} = obj # @@hair_colors = obj
42
+ end # end
43
+ EOS
44
+ end
45
+ end
46
+
47
+ def cattr_accessor(*syms)
48
+ cattr_reader(*syms)
49
+ cattr_writer(*syms)
50
+ end
51
+ end
@@ -0,0 +1,55 @@
1
+ # Extends the module object with module and instance accessors for class attributes,
2
+ # just like the native attr* accessors for instance attributes.
3
+ #
4
+ # module AppConfiguration
5
+ # mattr_accessor :google_api_key
6
+ # self.google_api_key = "123456789"
7
+ #
8
+ # mattr_accessor :paypal_url
9
+ # self.paypal_url = "www.sandbox.paypal.com"
10
+ # end
11
+ #
12
+ # AppConfiguration.google_api_key = "overriding the api key!"
13
+ class Module
14
+ def mattr_reader(*syms)
15
+ syms.each do |sym|
16
+ next if sym.is_a?(Hash)
17
+ class_eval(<<-EOS, __FILE__, __LINE__)
18
+ unless defined? @@#{sym} # unless defined? @@pagination_options
19
+ @@#{sym} = nil # @@pagination_options = nil
20
+ end # end
21
+ #
22
+ def self.#{sym} # def self.pagination_options
23
+ @@#{sym} # @@pagination_options
24
+ end # end
25
+ #
26
+ def #{sym} # def pagination_options
27
+ @@#{sym} # @@pagination_options
28
+ end # end
29
+ EOS
30
+ end
31
+ end
32
+
33
+ def mattr_writer(*syms)
34
+ syms.each do |sym|
35
+ class_eval(<<-EOS, __FILE__, __LINE__)
36
+ unless defined? @@#{sym} # unless defined? @@pagination_options
37
+ @@#{sym} = nil # @@pagination_options = nil
38
+ end # end
39
+ #
40
+ def self.#{sym}=(obj) # def self.pagination_options=(obj)
41
+ @@#{sym} = obj # @@pagination_options = obj
42
+ end # end
43
+ #
44
+ def #{sym}=(obj) # def pagination_options=(obj)
45
+ @@#{sym} = obj # @@pagination_options = obj
46
+ end # end
47
+ EOS
48
+ end
49
+ end
50
+
51
+ def mattr_accessor(*syms)
52
+ mattr_reader(*syms)
53
+ mattr_writer(*syms)
54
+ end
55
+ end
data/lib/workling.rb ADDED
@@ -0,0 +1,213 @@
1
+ #
2
+ # I can haz am in your Workling are belong to us!
3
+ #
4
+ def require_in_tree(name)
5
+ require File.join(File.dirname(__FILE__), name)
6
+ end
7
+
8
+ require_in_tree 'extensions/mattr_accessor' unless Module.respond_to?(:mattr_accessor)
9
+ require_in_tree 'extensions/cattr_accessor' unless Class.respond_to?(:cattr_accessor)
10
+
11
+ gem 'activesupport'
12
+ require 'active_support/inflector'
13
+ require 'active_support/core_ext/hash/keys'
14
+
15
+ class Hash #:nodoc:
16
+ include ActiveSupport::CoreExtensions::Hash::Keys
17
+ end
18
+
19
+ require 'yaml'
20
+
21
+ module Workling
22
+ class WorklingError < StandardError; end
23
+ class WorklingNotFoundError < WorklingError; end
24
+ class WorklingConnectionError < WorklingError; end
25
+ class QueueserverNotFoundError < WorklingError
26
+ def initialize
27
+ 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)"
28
+ end
29
+ end
30
+ class ConfigurationError < WorklingError
31
+ def initialize
32
+ super File.exist?(Workling.path('config', 'starling.yml')) ?
33
+ "config/starling.yml has been depracated. rename your config file to config/workling.yml then try again!" :
34
+ "config/workling.yml could not be loaded. check out README.markdown to see what this file should contain. "
35
+ end
36
+ end
37
+
38
+ def self.path(*args)
39
+ if defined?(RAILS_ROOT)
40
+ File.join(RAILS_ROOT, *args)
41
+ else
42
+ File.join(Dir.pwd, *args)
43
+ end
44
+ end
45
+
46
+ def self.env
47
+ @env ||= if defined?(RAILS_ENV)
48
+ RAILS_ENV.to_s
49
+ elsif defined?(RACK_ENV)
50
+ RACK_ENV.to_s
51
+ end
52
+ end
53
+
54
+ mattr_accessor :load_path
55
+ @@load_path = [ File.expand_path(path('app', 'workers')) ]
56
+
57
+ VERSION = "0.4.9" unless defined?(VERSION)
58
+
59
+ #
60
+ # determine the client to use if nothing is specifically set. workling will try to detect
61
+ # starling, spawn, or bj, in that order. if none of these are found, notremoterunner will
62
+ # be used.
63
+ #
64
+ def self.select_default_client
65
+ if env == "test"
66
+ Workling::Clients::NotRemoteClient
67
+ elsif Workling::Clients::SpawnClient.installed?
68
+ Workling::Clients::SpawnClient
69
+ elsif Workling::Clients::BackgroundjobClient.installed?
70
+ Workling::Clients::BackgroundjobClient
71
+ else
72
+ Workling::Clients::NotRemoteClient
73
+ end
74
+ end
75
+
76
+ def self.clients
77
+ {
78
+ 'amqp' => Workling::Clients::AmqpClient,
79
+ 'amqp_exchange' => Workling::Clients::AmqpExchangeClient,
80
+ 'memcache' => Workling::Clients::MemcacheQueueClient,
81
+ 'starling' => Workling::Clients::MemcacheQueueClient,
82
+ 'memory_queue' => Workling::Clients::MemoryQueueClient,
83
+ 'sqs' => Workling::Clients::SqsClient,
84
+ 'xmpp' => Workling::Clients::XmppClient,
85
+ 'backgroundjob' => Workling::Clients::BackgroundjobClient,
86
+ 'not_remote' => Workling::Clients::NotRemoteClient,
87
+ 'not' => Workling::Clients::NotClient,
88
+ 'spawn' => Workling::Clients::SpawnClient,
89
+ 'thread' => Workling::Clients::ThreadClient,
90
+ 'rudeq' => Workling::Clients::RudeQClient
91
+ }
92
+ end
93
+
94
+ def self.select_client
95
+ client_class = clients[Workling.config[:client]] || select_default_client
96
+ client_class.load
97
+ client_class
98
+ end
99
+
100
+ #
101
+ # this will build the client to use for job dispatching and retrieval
102
+ # The look up does the following:
103
+ # 1. if Workling::Remote.client is set explicitly then that client is used
104
+ # 2. if workling.yml (or whatever file is used) contains a client section then that is used
105
+ # 3. otherwise the default client is built using the Workling.select_and_build_default_client method
106
+ #
107
+ def self.select_and_build_client
108
+ select_client.new
109
+ end
110
+
111
+ #
112
+ # this will select the routing class
113
+ #
114
+ def self.select_and_build_routing
115
+ routing_class = {
116
+ 'class_and_method' => Workling::Routing::ClassAndMethodRouting,
117
+ 'static' => Workling::Routing::StaticRouting
118
+ }[Workling.config[:routing]] || Workling::Routing::ClassAndMethodRouting
119
+ routing_class.new
120
+ end
121
+
122
+ #
123
+ # this will build the invoker which will run the daemon
124
+ #
125
+ def self.select_and_build_invoker
126
+ invoker_class = {
127
+ 'basic_poller' => Workling::Invokers::BasicPoller,
128
+ 'thread_pool_poller' => Workling::Invokers::ThreadPoolPoller,
129
+ 'threaded_poller' => Workling::Invokers::ThreadedPoller,
130
+
131
+ 'eventmachine_subscriber' => Workling::Invokers::EventmachineSubscriber,
132
+ 'looped_subscriber' => Workling::Invokers::LoopedSubscriber,
133
+ 'amqp_single_subscriber' => Workling::Invokers::AmqpSingleSubscriber,
134
+ }[Workling.config[:invoker]] || Workling::Invokers::BasicPoller
135
+ invoker_class.new(select_and_build_routing, select_client)
136
+ end
137
+
138
+ #
139
+ # gets the worker instance, given a class. the optional method argument will cause an
140
+ # exception to be raised if the worker instance does not respoind to said method.
141
+ #
142
+ def self.find(clazz, method = nil)
143
+ begin
144
+ inst = clazz.to_s.camelize.constantize.new
145
+ rescue NameError
146
+ raise_not_found(clazz, method)
147
+ end
148
+ raise_not_found(clazz, method) if method && !inst.respond_to?(method)
149
+ inst
150
+ end
151
+
152
+ # returns Workling::Return::Store.instance.
153
+ def self.return
154
+ Workling::Return::Store.instance
155
+ end
156
+
157
+ #
158
+ # returns a config hash. reads ./config/workling.yml
159
+ #
160
+ mattr_writer :config
161
+ def self.config
162
+ return @@config if defined?(@@config) && @@config
163
+
164
+ return nil unless File.exists?(config_path)
165
+
166
+ @@config ||= YAML.load_file(config_path)[env || 'development'].symbolize_keys
167
+ @@config[:memcache_options].symbolize_keys! if @@config[:memcache_options]
168
+ @@config
169
+ end
170
+
171
+ mattr_writer :config_path
172
+ def self.config_path
173
+ return @@config_path if defined?(@@config_path) && @@config_path
174
+ @@config_path = File.join(RAILS_ROOT, 'config', 'workling.yml')
175
+ end
176
+
177
+ #
178
+ # Raises exceptions thrown inside of the worker. normally, these are logged to
179
+ # logger.error. it's easy to miss these log calls while developing, though.
180
+ #
181
+ mattr_writer :raise_exceptions
182
+ def raise_exceptions
183
+ return @@raise_exceptions if defined?(@@raise_exceptions)
184
+ @@raise_exceptions = (RAILS_ENV == "test" || RAILS_ENV == "development")
185
+ end
186
+
187
+ def self.raise_exceptions?
188
+ @@raise_exceptions
189
+ end
190
+
191
+ private
192
+ def self.raise_not_found(clazz, method)
193
+ raise Workling::WorklingNotFoundError.new("could not find #{ clazz }:#{ method } workling. ")
194
+ end
195
+
196
+ end
197
+
198
+ require_in_tree "workling/discovery"
199
+ require_in_tree "workling/base"
200
+ require_in_tree "workling/remote"
201
+
202
+ require_in_tree "workling/clients/base"
203
+ require_in_tree "workling/clients/broker_base"
204
+ require_in_tree "workling/invokers/base"
205
+ require_in_tree "workling/return/store/base"
206
+ require_in_tree "workling/routing/base"
207
+
208
+ # load all possible extension classes
209
+ ["clients", "invokers", "return/store", "routing"].each do |e_dirs|
210
+ Dir.glob(File.join(File.dirname(__FILE__), "workling", e_dirs, "*.rb")).each do |rb_file|
211
+ require File.join(File.dirname(rb_file), File.basename(rb_file, ".rb"))
212
+ end
213
+ end
@@ -0,0 +1,110 @@
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
+
18
+ cattr_writer :logger
19
+ def self.logger
20
+ @@logger ||= defined?(RAILS_DEFAULT_LOGGER) ? ::RAILS_DEFAULT_LOGGER : Logger.new($stdout)
21
+ end
22
+
23
+ def logger
24
+ self.class.logger
25
+ end
26
+
27
+ cattr_accessor :exposed_methods
28
+ @@exposed_methods ||= {}
29
+
30
+ def self.inherited(subclass)
31
+ Workling::Discovery.discovered << subclass
32
+ end
33
+
34
+ # expose a method using a custom queue name
35
+ def self.expose(method, options)
36
+ self.exposed_methods[method.to_s] = options[:as]
37
+ end
38
+
39
+ # identify the queue for a given method
40
+ def self.queue_for(method)
41
+ if self.exposed_methods.has_key?(method)
42
+ self.exposed_methods[method]
43
+ else
44
+ "#{ self.to_s.tableize }/#{ method }".split("/").join("__") # Don't split with : because it messes up memcache stats
45
+ end
46
+ end
47
+
48
+ # identify the method linked to the queue
49
+ def self.method_for(queue)
50
+ if self.exposed_methods.invert.has_key?(queue)
51
+ self.exposed_methods.invert[queue]
52
+ else
53
+ queue.split("__").last
54
+ end
55
+ end
56
+
57
+ def method_for(queue)
58
+ self.class.method_for(queue)
59
+ end
60
+
61
+
62
+ def initialize
63
+ super
64
+
65
+ create
66
+ end
67
+
68
+ # Put worker initialization code in here. This is good for restarting jobs that
69
+ # were interrupted.
70
+ def create
71
+ end
72
+
73
+ # override this if you want to set up exception notification
74
+ def notify_exception(exception, method, options)
75
+ logger.error "WORKLING ERROR: runner could not invoke #{ self.class }:#{ method } with #{ options.inspect }. error was: #{ exception.inspect }\n #{ exception.backtrace.join("\n") }"
76
+ end
77
+
78
+ # takes care of suppressing remote errors but raising Workling::WorklingNotFoundError
79
+ # where appropriate. swallow workling exceptions so that everything behaves like remote code.
80
+ # otherwise StarlingRunner and SpawnRunner would behave too differently to NotRemoteRunner.
81
+ def dispatch_to_worker_method(method, options = {})
82
+ begin
83
+ options = default_options.merge(options)
84
+ self.send(method, options)
85
+ rescue Workling::WorklingError => e
86
+ raise e
87
+ rescue Exception => e
88
+ notify_exception e, method, options
89
+
90
+ # reraise after logging. the exception really can't go anywhere in many cases. (spawn traps the exception)
91
+ raise e if Workling.raise_exceptions?
92
+ end
93
+ end
94
+
95
+ # supply default_options as a hash in classes that inherit Workling::Base
96
+ def default_options
97
+ {}
98
+ end
99
+
100
+ # thanks to blaine cook for this suggestion.
101
+ def self.method_missing(method, *args, &block)
102
+ if method.to_s =~ /^asynch?_(.*)/
103
+ Workling::Remote.run(self.to_s.dasherize, $1, *args)
104
+ else
105
+ super
106
+ end
107
+ end
108
+
109
+ end
110
+ end