derfred-workling 0.4.9.1 → 0.4.9.2
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/cattr_accessor.rb +51 -0
- data/lib/mattr_accessor.rb +55 -0
- data/lib/rude_q/client.rb +11 -0
- data/lib/workling/base.rb +115 -0
- data/lib/workling/clients/amqp_client.rb +38 -0
- data/lib/workling/clients/amqp_exchange_client.rb +50 -0
- data/lib/workling/clients/base.rb +57 -0
- data/lib/workling/clients/memcache_queue_client.rb +91 -0
- data/lib/workling/clients/sqs_client.rb +162 -0
- data/lib/workling/clients/xmpp_client.rb +100 -0
- data/lib/workling/discovery.rb +16 -0
- data/lib/workling/remote/invokers/amqp_single_subscriber.rb +45 -0
- data/lib/workling/remote/invokers/base.rb +124 -0
- data/lib/workling/remote/invokers/basic_poller.rb +41 -0
- data/lib/workling/remote/invokers/eventmachine_subscriber.rb +41 -0
- data/lib/workling/remote/invokers/looped_subscriber.rb +38 -0
- data/lib/workling/remote/invokers/thread_pool_poller.rb +169 -0
- data/lib/workling/remote/invokers/threaded_poller.rb +153 -0
- data/lib/workling/remote/runners/amqp_exchange_runner.rb +45 -0
- data/lib/workling/remote/runners/backgroundjob_runner.rb +35 -0
- data/lib/workling/remote/runners/base.rb +42 -0
- data/lib/workling/remote/runners/client_runner.rb +46 -0
- data/lib/workling/remote/runners/not_remote_runner.rb +23 -0
- data/lib/workling/remote/runners/not_runner.rb +17 -0
- data/lib/workling/remote/runners/rudeq_runner.rb +23 -0
- data/lib/workling/remote/runners/spawn_runner.rb +38 -0
- data/lib/workling/remote/runners/starling_runner.rb +13 -0
- data/lib/workling/remote.rb +69 -0
- data/lib/workling/return/store/base.rb +42 -0
- data/lib/workling/return/store/iterator.rb +24 -0
- data/lib/workling/return/store/memory_return_store.rb +26 -0
- data/lib/workling/return/store/rudeq_return_store.rb +24 -0
- data/lib/workling/return/store/starling_return_store.rb +31 -0
- data/lib/workling/routing/base.rb +16 -0
- data/lib/workling/routing/class_and_method_routing.rb +57 -0
- data/lib/workling/routing/static_routing.rb +47 -0
- data/lib/workling/rudeq/client.rb +17 -0
- data/lib/workling/rudeq/poller.rb +116 -0
- data/lib/workling/rudeq.rb +7 -0
- data/lib/workling.rb +195 -0
- data/lib/workling_server.rb +108 -0
- data/script/bj_invoker.rb +11 -0
- data/script/starling_status.rb +37 -0
- metadata +44 -1
@@ -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
|
@@ -0,0 +1,11 @@
|
|
1
|
+
#
|
2
|
+
# A RudeQ client that behvaes somewhat like memcache-client
|
3
|
+
#
|
4
|
+
module RudeQ
|
5
|
+
class Client
|
6
|
+
def initialize(*args); super(); end
|
7
|
+
def set(key, value); RudeQueue.set(key, value); end;
|
8
|
+
def get(key); RudeQueue.get(key); end;
|
9
|
+
def stats; ActiveRecord::Base.connection; end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
#require 'struct'
|
2
|
+
|
3
|
+
#
|
4
|
+
# All worker classes must inherit from this class, and be saved in app/workers.
|
5
|
+
#
|
6
|
+
# The Worker lifecycle:
|
7
|
+
# The Worker is loaded once, at which point the instance method 'create' is called.
|
8
|
+
#
|
9
|
+
# Invoking Workers:
|
10
|
+
# Calling async_my_method on the worker class will trigger background work.
|
11
|
+
# This means that the loaded Worker instance will receive a call to the method
|
12
|
+
# my_method(:uid => "thisjobsuid2348732947923").
|
13
|
+
#
|
14
|
+
# The Worker method must have a single hash argument. Note that the job :uid will
|
15
|
+
# be merged into the hash.
|
16
|
+
#
|
17
|
+
module Workling
|
18
|
+
class Base
|
19
|
+
cattr_writer :logger
|
20
|
+
def self.logger
|
21
|
+
@@logger ||= ::RAILS_DEFAULT_LOGGER
|
22
|
+
end
|
23
|
+
|
24
|
+
cattr_accessor :exposed_methods
|
25
|
+
@@exposed_methods ||= {}
|
26
|
+
|
27
|
+
def self.inherited(subclass)
|
28
|
+
Workling::Discovery.discovered << subclass
|
29
|
+
end
|
30
|
+
|
31
|
+
# expose a method using a custom queue name
|
32
|
+
def self.expose(method, options)
|
33
|
+
self.exposed_methods[method.to_s] = options[:as]
|
34
|
+
end
|
35
|
+
|
36
|
+
# identify the queue for a given method
|
37
|
+
def self.queue_for(method)
|
38
|
+
if self.exposed_methods.has_key?(method)
|
39
|
+
self.exposed_methods[method]
|
40
|
+
else
|
41
|
+
"#{ self.to_s.tableize }/#{ method }".split("/").join("__") # Don't split with : because it messes up memcache stats
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# identify the method linked to the queue
|
46
|
+
def self.method_for(queue)
|
47
|
+
if self.exposed_methods.invert.has_key?(queue)
|
48
|
+
self.exposed_methods.invert[queue]
|
49
|
+
else
|
50
|
+
queue.split("__").last
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def method_for(queue)
|
55
|
+
self.class.method_for(queue)
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
def initialize
|
60
|
+
super
|
61
|
+
|
62
|
+
create
|
63
|
+
end
|
64
|
+
|
65
|
+
# Put worker initialization code in here. This is good for restarting jobs that
|
66
|
+
# were interrupted.
|
67
|
+
def create
|
68
|
+
end
|
69
|
+
|
70
|
+
# override this if you want to set up exception notification
|
71
|
+
def notify_exception(exception, method, options)
|
72
|
+
logger.error "WORKLING ERROR: runner could not invoke #{ self.class }:#{ method } with #{ options.inspect }. error was: #{ exception.inspect }\n #{ exception.backtrace.join("\n") }"
|
73
|
+
end
|
74
|
+
|
75
|
+
# takes care of suppressing remote errors but raising Workling::WorklingNotFoundError
|
76
|
+
# where appropriate. swallow workling exceptions so that everything behaves like remote code.
|
77
|
+
# otherwise StarlingRunner and SpawnRunner would behave too differently to NotRemoteRunner.
|
78
|
+
def dispatch_to_worker_method(method, options = {})
|
79
|
+
begin
|
80
|
+
options = default_options.merge(options)
|
81
|
+
self.send(method, options)
|
82
|
+
rescue Workling::WorklingError => e
|
83
|
+
raise e
|
84
|
+
rescue Exception => e
|
85
|
+
notify_exception e, method, options
|
86
|
+
on_error(e) if respond_to?(:on_error)
|
87
|
+
|
88
|
+
# reraise after logging. the exception really can't go anywhere in many cases. (spawn traps the exception)
|
89
|
+
raise e if Workling.raise_exceptions?
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# supply default_options as a hash in classes that inherit Workling::Base
|
94
|
+
def default_options
|
95
|
+
{}
|
96
|
+
end
|
97
|
+
|
98
|
+
# thanks to blaine cook for this suggestion.
|
99
|
+
def self.method_missing(method, *args, &block)
|
100
|
+
if method.to_s =~ /^asynch?_(.*)/
|
101
|
+
Workling::Remote.run(self.to_s.dasherize, $1, *args)
|
102
|
+
else
|
103
|
+
super
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.logger
|
108
|
+
@logger ||= defined?(RAILS_DEFAULT_LOGGER) ? ::RAILS_DEFAULT_LOGGER : Logger.new($stdout)
|
109
|
+
end
|
110
|
+
|
111
|
+
def logger
|
112
|
+
self.class.logger
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,38 @@
|
|
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
|
+
connection = AMQP.start((Workling.config[:amqp_options] ||{}).symbolize_keys)
|
15
|
+
@amq = MQ.new connection
|
16
|
+
rescue
|
17
|
+
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.)")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# no need for explicit closing. when the event loop
|
22
|
+
# terminates, the connection is closed anyway.
|
23
|
+
def close; true; end
|
24
|
+
|
25
|
+
# subscribe to a queue
|
26
|
+
def subscribe(key)
|
27
|
+
@amq.queue(key).subscribe do |value|
|
28
|
+
data = Marshal.load(value) rescue value
|
29
|
+
yield data
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# request and retrieve work
|
34
|
+
def retrieve(key); @amq.queue(key); end
|
35
|
+
def request(key, value); @amq.queue(key).publish(Marshal.dump(value)); end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,50 @@
|
|
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 AmqpExchangeClient < 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 |header, body|
|
27
|
+
|
28
|
+
puts "***** received msg with header - #{header.inspect}"
|
29
|
+
|
30
|
+
value = YAML.load(body)
|
31
|
+
yield value
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# request and retrieve work
|
36
|
+
def retrieve(key)
|
37
|
+
@amq.queue(key)
|
38
|
+
end
|
39
|
+
|
40
|
+
# publish message to exchange
|
41
|
+
# using the specified routing key
|
42
|
+
def request(exchange_name, value)
|
43
|
+
key = value.delete(:routing_key)
|
44
|
+
msg = YAML.dump(value)
|
45
|
+
exchange = @amq.topic(exchange_name)
|
46
|
+
exchange.publish(msg, :routing_key => key)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,57 @@
|
|
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
|
+
# returns the Workling::Base.logger
|
12
|
+
def logger; Workling::Base.logger; end
|
13
|
+
|
14
|
+
#
|
15
|
+
# Requests a job on the broker.
|
16
|
+
#
|
17
|
+
# work_type:
|
18
|
+
# arguments: the argument to the worker method
|
19
|
+
#
|
20
|
+
def request(work_type, arguments)
|
21
|
+
raise NotImplementedError.new("Implement request(work_type, arguments) in your client. ")
|
22
|
+
end
|
23
|
+
|
24
|
+
#
|
25
|
+
# Gets job results off a job broker. Returns nil if there are no results.
|
26
|
+
#
|
27
|
+
# worker_uid: the uid returned by workling when the work was dispatched
|
28
|
+
#
|
29
|
+
def retrieve(work_uid)
|
30
|
+
raise NotImplementedError.new("Implement retrieve(work_uid) in your client. ")
|
31
|
+
end
|
32
|
+
|
33
|
+
#
|
34
|
+
# Subscribe to job results in a job broker.
|
35
|
+
#
|
36
|
+
# worker_type:
|
37
|
+
#
|
38
|
+
def subscribe(work_type)
|
39
|
+
raise NotImplementedError.new("Implement subscribe(work_type) in your client. ")
|
40
|
+
end
|
41
|
+
|
42
|
+
#
|
43
|
+
# Opens a connection to the job broker.
|
44
|
+
#
|
45
|
+
def connect
|
46
|
+
raise NotImplementedError.new("Implement connect() in your client. ")
|
47
|
+
end
|
48
|
+
|
49
|
+
#
|
50
|
+
# Closes the connection to the job broker.
|
51
|
+
#
|
52
|
+
def close
|
53
|
+
raise NotImplementedError.new("Implement close() in your client. ")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'workling/clients/base'
|
2
|
+
require 'memcache'
|
3
|
+
|
4
|
+
#
|
5
|
+
# This client can be used for all Queue Servers that speak Memcached, such as Starling.
|
6
|
+
#
|
7
|
+
# Wrapper for the memcache connection. The connection is made using fiveruns-memcache-client,
|
8
|
+
# or memcache-client, if this is not available. See the README for a discussion of the memcache
|
9
|
+
# clients.
|
10
|
+
#
|
11
|
+
# method_missing delegates all messages through to the underlying memcache connection.
|
12
|
+
#
|
13
|
+
module Workling
|
14
|
+
module Clients
|
15
|
+
class MemcacheQueueClient < Workling::Clients::Base
|
16
|
+
|
17
|
+
# the class with which the connection is instantiated
|
18
|
+
cattr_accessor :memcache_client_class
|
19
|
+
@@memcache_client_class ||= ::MemCache
|
20
|
+
|
21
|
+
# the url with which the memcache client expects to reach starling
|
22
|
+
attr_accessor :queueserver_urls
|
23
|
+
|
24
|
+
# the memcache connection object
|
25
|
+
attr_accessor :connection
|
26
|
+
|
27
|
+
#
|
28
|
+
# the client attempts to connect to queueserver using the configuration options found in
|
29
|
+
#
|
30
|
+
# Workling.config. this can be configured in config/workling.yml.
|
31
|
+
#
|
32
|
+
# the initialization code will raise an exception if memcache-client cannot connect
|
33
|
+
# to queueserver.
|
34
|
+
#
|
35
|
+
def connect
|
36
|
+
@queueserver_urls = Workling.config[:listens_on].split(',').map { |url| url ? url.strip : url }
|
37
|
+
options = [@queueserver_urls, Workling.config[:memcache_options]].compact
|
38
|
+
self.connection = MemcacheQueueClient.memcache_client_class.new(*options)
|
39
|
+
|
40
|
+
raise_unless_connected!
|
41
|
+
end
|
42
|
+
|
43
|
+
# closes the memcache connection
|
44
|
+
def close
|
45
|
+
self.connection.flush_all
|
46
|
+
self.connection.reset
|
47
|
+
end
|
48
|
+
|
49
|
+
# implements the client job request and retrieval
|
50
|
+
def request(key, value)
|
51
|
+
set(key, value)
|
52
|
+
end
|
53
|
+
|
54
|
+
def retrieve(key)
|
55
|
+
begin
|
56
|
+
get(key)
|
57
|
+
rescue MemCache::MemCacheError => e
|
58
|
+
# failed to enqueue, raise a workling error so that it propagates upwards
|
59
|
+
raise Workling::WorklingConnectionError.new("#{e.class.to_s} - #{e.message}")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
# make sure we can actually connect to queueserver on the given port
|
65
|
+
def raise_unless_connected!
|
66
|
+
begin
|
67
|
+
self.connection.stats
|
68
|
+
rescue
|
69
|
+
raise Workling::QueueserverNotFoundError.new
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
[:get, :set].each do |method|
|
74
|
+
class_eval <<-EOS
|
75
|
+
def #{method}(*args, &block)
|
76
|
+
self.connection.#{method}(*args, &block)
|
77
|
+
end
|
78
|
+
EOS
|
79
|
+
end
|
80
|
+
|
81
|
+
# delegates directly through to the memcache connection.
|
82
|
+
def method_missing(method, *args)
|
83
|
+
begin
|
84
|
+
self.connection.send(method, *args)
|
85
|
+
rescue MemCache::MemCacheError => e
|
86
|
+
raise Workling::WorklingConnectionError.new("#{e.class.to_s} - #{e.message}")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
require 'workling/clients/base'
|
2
|
+
require 'json'
|
3
|
+
require 'right_aws'
|
4
|
+
|
5
|
+
#
|
6
|
+
# An SQS client
|
7
|
+
#
|
8
|
+
# Requires the following configuration in workling.yml:
|
9
|
+
#
|
10
|
+
# production:
|
11
|
+
# sqs_options:
|
12
|
+
# aws_access_key_id: <your AWS access key id>
|
13
|
+
# aws_secret_access_key: <your AWS secret access key>
|
14
|
+
#
|
15
|
+
# You can also add the following optional parameters:
|
16
|
+
#
|
17
|
+
# # Queue names consist of an optional prefix, followed by the environment
|
18
|
+
# # and the name of the key.
|
19
|
+
# prefix: foo_
|
20
|
+
#
|
21
|
+
# # The number of SQS messages to retrieve at once. The maximum and default
|
22
|
+
# # value is 10.
|
23
|
+
# messages_per_req: 5
|
24
|
+
#
|
25
|
+
# # The SQS visibility timeout for retrieved messages. Defaults to 30 seconds.
|
26
|
+
# visibility_timeout: 15
|
27
|
+
#
|
28
|
+
module Workling
|
29
|
+
module Clients
|
30
|
+
class SqsClient < Workling::Clients::Base
|
31
|
+
|
32
|
+
AWS_MAX_QUEUE_NAME = 80
|
33
|
+
|
34
|
+
# Note that 10 is the maximum number of messages that can be retrieved
|
35
|
+
# in a single request.
|
36
|
+
DEFAULT_MESSAGES_PER_REQ = 10
|
37
|
+
DEFAULT_VISIBILITY_TIMEOUT = 30
|
38
|
+
DEFAULT_VISIBILITY_RESERVE = 10
|
39
|
+
|
40
|
+
# Mainly exposed for testing purposes
|
41
|
+
attr_reader :sqs_options
|
42
|
+
attr_reader :messages_per_req
|
43
|
+
attr_reader :visibility_timeout
|
44
|
+
|
45
|
+
# Starts the client.
|
46
|
+
def connect
|
47
|
+
@sqs_options = Workling.config[:sqs_options]
|
48
|
+
|
49
|
+
# Make sure that required options were specified
|
50
|
+
unless (@sqs_options.include?('aws_access_key_id') &&
|
51
|
+
@sqs_options.include?('aws_secret_access_key'))
|
52
|
+
raise WorklingError, 'Unable to start SqsClient due to missing SQS options'
|
53
|
+
end
|
54
|
+
|
55
|
+
# Optional settings
|
56
|
+
@messages_per_req = @sqs_options['messages_per_req'] || DEFAULT_MESSAGES_PER_REQ
|
57
|
+
@visibility_timeout = @sqs_options['visibility_timeout'] || DEFAULT_VISIBILITY_TIMEOUT
|
58
|
+
@visibility_reserve = @sqs_options['visibility_reserve'] || DEFAULT_VISIBILITY_RESERVE
|
59
|
+
|
60
|
+
begin
|
61
|
+
@sqs = RightAws::SqsGen2.new(
|
62
|
+
@sqs_options['aws_access_key_id'],
|
63
|
+
@sqs_options['aws_secret_access_key'],
|
64
|
+
:multi_thread => true)
|
65
|
+
rescue => e
|
66
|
+
raise WorklingError, "Unable to connect to SQS. Error: #{e}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# No need for explicit closing, since there is no persistent
|
71
|
+
# connection to SQS.
|
72
|
+
def close
|
73
|
+
true
|
74
|
+
end
|
75
|
+
|
76
|
+
# Retrieve work.
|
77
|
+
def retrieve(key)
|
78
|
+
begin
|
79
|
+
# We're using a buffer per key to retrieve several messages at once,
|
80
|
+
# then return them one at a time until the buffer is empty.
|
81
|
+
# Workling seems to create one thread per worker class, each with its own
|
82
|
+
# client. But to be sure (and to be less dependent on workling internals),
|
83
|
+
# we store each buffer in a thread local variable.
|
84
|
+
buffer = Thread.current["buffer_#{key}"]
|
85
|
+
if buffer.nil? || buffer.empty?
|
86
|
+
Thread.current["buffer_#{key}"] = buffer = queue_for_key(key).receive_messages(
|
87
|
+
@messages_per_req, @visibility_timeout)
|
88
|
+
end
|
89
|
+
|
90
|
+
if buffer.empty?
|
91
|
+
nil
|
92
|
+
else
|
93
|
+
msg = buffer.shift
|
94
|
+
|
95
|
+
# We need to protect against the case that processing one of the
|
96
|
+
# messages in the buffer took so much time that the visibility
|
97
|
+
# timeout for the remaining messages has expired. To be on the
|
98
|
+
# safe side (since we need to leave enough time to delete the
|
99
|
+
# message), we drop it if more than half of the visibility timeout
|
100
|
+
# has elapsed.
|
101
|
+
if msg.received_at < (Time.now - (@visibility_timeout - @visibility_reserve))
|
102
|
+
nil
|
103
|
+
else
|
104
|
+
# Need to wrap in HashWithIndifferentAccess, as JSON serialization
|
105
|
+
# loses symbol keys.
|
106
|
+
parsed_msg = HashWithIndifferentAccess.new(JSON.parse(msg.body))
|
107
|
+
|
108
|
+
# Delete the msg from SQS, so we don't re-retrieve it after the
|
109
|
+
# visibility timeout. Ideally we would defer deleting a msg until
|
110
|
+
# after Workling has successfully processed it, but it currently
|
111
|
+
# doesn't provide the necessary hooks for this.
|
112
|
+
msg.delete
|
113
|
+
|
114
|
+
parsed_msg
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
rescue => e
|
119
|
+
logger.error "Error retrieving msg for key: #{key}; Error: #{e}\n#{e.backtrace.join("\n")}"
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
|
124
|
+
# Request work.
|
125
|
+
def request(key, value)
|
126
|
+
begin
|
127
|
+
queue_for_key(key).send_message(value.to_json)
|
128
|
+
rescue => e
|
129
|
+
logger.error "SQS Client: Error sending msg for key: #{key}, value: #{value.inspect}; Error: #{e}"
|
130
|
+
raise WorklingError, "Error sending msg for key: #{key}, value: #{value.inspect}; Error: #{e}"
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# Returns the queue that corresponds to the specified key. Creates the
|
135
|
+
# queue if it doesn't exist yet.
|
136
|
+
def queue_for_key(key)
|
137
|
+
# Use thread local for storing queues, for the same reason as for buffers
|
138
|
+
Thread.current["queue_#{key}"] ||= @sqs.queue(queue_name(key), true, @visibility_timeout)
|
139
|
+
end
|
140
|
+
|
141
|
+
# Returns the queue name for the specified key. The name consists of an
|
142
|
+
# optional prefix, followed by the environment and the key itself. Note
|
143
|
+
# that with a long worker class / method name, the name could exceed the
|
144
|
+
# 80 character maximum for SQS queue names. We truncate the name until it
|
145
|
+
# fits, but there's still the danger of this not being unique any more.
|
146
|
+
# Might need to implement a more robust naming scheme...
|
147
|
+
def queue_name(key)
|
148
|
+
"#{@sqs_options['prefix'] || ''}#{env}_#{key}"[0, AWS_MAX_QUEUE_NAME]
|
149
|
+
end
|
150
|
+
|
151
|
+
private
|
152
|
+
|
153
|
+
def logger
|
154
|
+
Rails.logger
|
155
|
+
end
|
156
|
+
|
157
|
+
def env
|
158
|
+
Rails.env
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|