derfred-workling 0.4.9.1 → 0.4.9.2
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.
- 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
|