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.
- data/CHANGES.markdown +82 -0
- data/README.markdown +543 -0
- data/TODO.markdown +27 -0
- data/VERSION.yml +4 -0
- data/bin/workling_client +29 -0
- data/contrib/bj_invoker.rb +11 -0
- data/contrib/starling_status.rb +37 -0
- data/lib/extensions/cattr_accessor.rb +51 -0
- data/lib/extensions/mattr_accessor.rb +55 -0
- data/lib/workling.rb +213 -0
- data/lib/workling/base.rb +110 -0
- data/lib/workling/clients/amqp_client.rb +51 -0
- data/lib/workling/clients/amqp_exchange_client.rb +58 -0
- data/lib/workling/clients/backgroundjob_client.rb +25 -0
- data/lib/workling/clients/base.rb +89 -0
- data/lib/workling/clients/broker_base.rb +63 -0
- data/lib/workling/clients/memcache_queue_client.rb +104 -0
- data/lib/workling/clients/memory_queue_client.rb +34 -0
- data/lib/workling/clients/not_client.rb +14 -0
- data/lib/workling/clients/not_remote_client.rb +17 -0
- data/lib/workling/clients/rude_q_client.rb +47 -0
- data/lib/workling/clients/spawn_client.rb +46 -0
- data/lib/workling/clients/sqs_client.rb +163 -0
- data/lib/workling/clients/thread_client.rb +18 -0
- data/lib/workling/clients/xmpp_client.rb +110 -0
- data/lib/workling/discovery.rb +16 -0
- data/lib/workling/invokers/amqp_single_subscriber.rb +42 -0
- data/lib/workling/invokers/base.rb +124 -0
- data/lib/workling/invokers/basic_poller.rb +38 -0
- data/lib/workling/invokers/eventmachine_subscriber.rb +38 -0
- data/lib/workling/invokers/looped_subscriber.rb +34 -0
- data/lib/workling/invokers/thread_pool_poller.rb +165 -0
- data/lib/workling/invokers/threaded_poller.rb +149 -0
- data/lib/workling/remote.rb +38 -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 +24 -0
- data/lib/workling/return/store/starling_return_store.rb +30 -0
- data/lib/workling/routing/base.rb +13 -0
- data/lib/workling/routing/class_and_method_routing.rb +55 -0
- data/lib/workling/routing/static_routing.rb +43 -0
- data/lib/workling_daemon.rb +111 -0
- metadata +96 -0
@@ -0,0 +1,17 @@
|
|
1
|
+
#
|
2
|
+
# Run the job inline
|
3
|
+
#
|
4
|
+
module Workling
|
5
|
+
module Clients
|
6
|
+
class NotRemoteClient < Workling::Clients::Base
|
7
|
+
|
8
|
+
def dispatch(clazz, method, options = {})
|
9
|
+
options = Marshal.load(Marshal.dump(options)) # get this to behave more like the remote runners
|
10
|
+
Workling.find(clazz, method).dispatch_to_worker_method(method, options)
|
11
|
+
|
12
|
+
return nil # nada. niente.
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
#
|
2
|
+
# This client interfaces with RudeQ a databased queue system
|
3
|
+
# http://github.com/matthewrudy/rudeq
|
4
|
+
#
|
5
|
+
module Workling
|
6
|
+
module Clients
|
7
|
+
class RudeQClient < Workling::Clients::BrokerBase
|
8
|
+
|
9
|
+
def self.installed?
|
10
|
+
begin
|
11
|
+
gem 'rudeq'
|
12
|
+
require 'rudeq'
|
13
|
+
rescue LoadError
|
14
|
+
end
|
15
|
+
|
16
|
+
Object.const_defined? "RudeQueue"
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.load
|
20
|
+
begin
|
21
|
+
gem 'rudeq'
|
22
|
+
require 'rudeq'
|
23
|
+
rescue Gem::LoadError
|
24
|
+
Workling::Base.logger.info "WORKLING: couldn't find rudeq library. Install: \"gem install matthewrudy-rudeq\". "
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# no-op as the db connection should exists always
|
29
|
+
def connect
|
30
|
+
end
|
31
|
+
|
32
|
+
# again a no-op as we would want to yank out the db connection behind the apps back
|
33
|
+
def close
|
34
|
+
end
|
35
|
+
|
36
|
+
# implements the client job request and retrieval
|
37
|
+
def request(key, value)
|
38
|
+
RudeQueue.set(key, value)
|
39
|
+
end
|
40
|
+
|
41
|
+
def retrieve(key)
|
42
|
+
RudeQueue.get(key)
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
#
|
2
|
+
# Run the job over the spawn plugin. Refer to the README for instructions on
|
3
|
+
# installing Spawn.
|
4
|
+
#
|
5
|
+
# Spawn forks the entire process once for each job. This means that the job starts
|
6
|
+
# with a very low latency, but takes up more memory for each job.
|
7
|
+
#
|
8
|
+
# It's also possible to configure Spawn to start a Thread for each job. Do this
|
9
|
+
# by setting
|
10
|
+
#
|
11
|
+
# Workling::Clients::SpawnClient.options = { :method => :thread }
|
12
|
+
#
|
13
|
+
# Have a look at the Spawn README to find out more about the characteristics of this.
|
14
|
+
#
|
15
|
+
module Workling
|
16
|
+
module Clients
|
17
|
+
class SpawnClient < Workling::Clients::Base
|
18
|
+
|
19
|
+
def self.installed?
|
20
|
+
begin
|
21
|
+
require 'spawn'
|
22
|
+
rescue LoadError
|
23
|
+
end
|
24
|
+
|
25
|
+
Object.const_defined? "Spawn"
|
26
|
+
end
|
27
|
+
|
28
|
+
cattr_writer :options
|
29
|
+
def self.options
|
30
|
+
# use thread for development and test modes. easier to hunt down exceptions that way.
|
31
|
+
@@options ||= { :method => (RAILS_ENV == "test" || RAILS_ENV == "development" ? :fork : :thread) }
|
32
|
+
end
|
33
|
+
|
34
|
+
include Spawn if installed?
|
35
|
+
|
36
|
+
def dispatch(clazz, method, options = {})
|
37
|
+
spawn(SpawnClient.options) do # exceptions are trapped in here.
|
38
|
+
Workling.find(clazz, method).dispatch_to_worker_method(method, options)
|
39
|
+
end
|
40
|
+
|
41
|
+
return nil # that means nothing!
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'right_aws'
|
3
|
+
|
4
|
+
#
|
5
|
+
# An SQS client
|
6
|
+
#
|
7
|
+
# Requires the following configuration in workling.yml:
|
8
|
+
#
|
9
|
+
# production:
|
10
|
+
# sqs_options:
|
11
|
+
# aws_access_key_id: <your AWS access key id>
|
12
|
+
# aws_secret_access_key: <your AWS secret access key>
|
13
|
+
#
|
14
|
+
# You can also add the following optional parameters:
|
15
|
+
#
|
16
|
+
# # Queue names consist of an optional prefix, followed by the environment
|
17
|
+
# # and the name of the key.
|
18
|
+
# prefix: foo_
|
19
|
+
#
|
20
|
+
# # The number of SQS messages to retrieve at once. The maximum and default
|
21
|
+
# # value is 10.
|
22
|
+
# messages_per_req: 5
|
23
|
+
#
|
24
|
+
# # The SQS visibility timeout for retrieved messages. Defaults to 30 seconds.
|
25
|
+
# visibility_timeout: 15
|
26
|
+
#
|
27
|
+
module Workling
|
28
|
+
module Clients
|
29
|
+
class SqsClient < Workling::Clients::BrokerBase
|
30
|
+
|
31
|
+
unless defined?(AWS_MAX_QUEUE_NAME)
|
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
|
+
end
|
40
|
+
|
41
|
+
# Mainly exposed for testing purposes
|
42
|
+
attr_reader :sqs_options
|
43
|
+
attr_reader :messages_per_req
|
44
|
+
attr_reader :visibility_timeout
|
45
|
+
|
46
|
+
# Starts the client.
|
47
|
+
def connect
|
48
|
+
@sqs_options = Workling.config[:sqs_options]
|
49
|
+
|
50
|
+
# Make sure that required options were specified
|
51
|
+
unless (@sqs_options.include?('aws_access_key_id') &&
|
52
|
+
@sqs_options.include?('aws_secret_access_key'))
|
53
|
+
raise WorklingError, 'Unable to start SqsClient due to missing SQS options'
|
54
|
+
end
|
55
|
+
|
56
|
+
# Optional settings
|
57
|
+
@messages_per_req = @sqs_options['messages_per_req'] || DEFAULT_MESSAGES_PER_REQ
|
58
|
+
@visibility_timeout = @sqs_options['visibility_timeout'] || DEFAULT_VISIBILITY_TIMEOUT
|
59
|
+
@visibility_reserve = @sqs_options['visibility_reserve'] || DEFAULT_VISIBILITY_RESERVE
|
60
|
+
|
61
|
+
begin
|
62
|
+
@sqs = RightAws::SqsGen2.new(
|
63
|
+
@sqs_options['aws_access_key_id'],
|
64
|
+
@sqs_options['aws_secret_access_key'],
|
65
|
+
:multi_thread => true)
|
66
|
+
rescue => e
|
67
|
+
raise WorklingError, "Unable to connect to SQS. Error: #{e}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# No need for explicit closing, since there is no persistent
|
72
|
+
# connection to SQS.
|
73
|
+
def close
|
74
|
+
true
|
75
|
+
end
|
76
|
+
|
77
|
+
# Retrieve work.
|
78
|
+
def retrieve(key)
|
79
|
+
begin
|
80
|
+
# We're using a buffer per key to retrieve several messages at once,
|
81
|
+
# then return them one at a time until the buffer is empty.
|
82
|
+
# Workling seems to create one thread per worker class, each with its own
|
83
|
+
# client. But to be sure (and to be less dependent on workling internals),
|
84
|
+
# we store each buffer in a thread local variable.
|
85
|
+
buffer = Thread.current["buffer_#{key}"]
|
86
|
+
if buffer.nil? || buffer.empty?
|
87
|
+
Thread.current["buffer_#{key}"] = buffer = queue_for_key(key).receive_messages(
|
88
|
+
@messages_per_req, @visibility_timeout)
|
89
|
+
end
|
90
|
+
|
91
|
+
if buffer.empty?
|
92
|
+
nil
|
93
|
+
else
|
94
|
+
msg = buffer.shift
|
95
|
+
|
96
|
+
# We need to protect against the case that processing one of the
|
97
|
+
# messages in the buffer took so much time that the visibility
|
98
|
+
# timeout for the remaining messages has expired. To be on the
|
99
|
+
# safe side (since we need to leave enough time to delete the
|
100
|
+
# message), we drop it if more than half of the visibility timeout
|
101
|
+
# has elapsed.
|
102
|
+
if msg.received_at < (Time.now - (@visibility_timeout - @visibility_reserve))
|
103
|
+
nil
|
104
|
+
else
|
105
|
+
# Need to wrap in HashWithIndifferentAccess, as JSON serialization
|
106
|
+
# loses symbol keys.
|
107
|
+
parsed_msg = HashWithIndifferentAccess.new(JSON.parse(msg.body))
|
108
|
+
|
109
|
+
# Delete the msg from SQS, so we don't re-retrieve it after the
|
110
|
+
# visibility timeout. Ideally we would defer deleting a msg until
|
111
|
+
# after Workling has successfully processed it, but it currently
|
112
|
+
# doesn't provide the necessary hooks for this.
|
113
|
+
msg.delete
|
114
|
+
|
115
|
+
parsed_msg
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
rescue => e
|
120
|
+
logger.error "Error retrieving msg for key: #{key}; Error: #{e}\n#{e.backtrace.join("\n")}"
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
|
125
|
+
# Request work.
|
126
|
+
def request(key, value)
|
127
|
+
begin
|
128
|
+
queue_for_key(key).send_message(value.to_json)
|
129
|
+
rescue => e
|
130
|
+
logger.error "SQS Client: Error sending msg for key: #{key}, value: #{value.inspect}; Error: #{e}"
|
131
|
+
raise WorklingError, "Error sending msg for key: #{key}, value: #{value.inspect}; Error: #{e}"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Returns the queue that corresponds to the specified key. Creates the
|
136
|
+
# queue if it doesn't exist yet.
|
137
|
+
def queue_for_key(key)
|
138
|
+
# Use thread local for storing queues, for the same reason as for buffers
|
139
|
+
Thread.current["queue_#{key}"] ||= @sqs.queue(queue_name(key), true, @visibility_timeout)
|
140
|
+
end
|
141
|
+
|
142
|
+
# Returns the queue name for the specified key. The name consists of an
|
143
|
+
# optional prefix, followed by the environment and the key itself. Note
|
144
|
+
# that with a long worker class / method name, the name could exceed the
|
145
|
+
# 80 character maximum for SQS queue names. We truncate the name until it
|
146
|
+
# fits, but there's still the danger of this not being unique any more.
|
147
|
+
# Might need to implement a more robust naming scheme...
|
148
|
+
def queue_name(key)
|
149
|
+
"#{@sqs_options['prefix'] || ''}#{env}_#{key}"[0, AWS_MAX_QUEUE_NAME]
|
150
|
+
end
|
151
|
+
|
152
|
+
private
|
153
|
+
|
154
|
+
def logger
|
155
|
+
Rails.logger
|
156
|
+
end
|
157
|
+
|
158
|
+
def env
|
159
|
+
Rails.env
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
#
|
2
|
+
# Spawns a Thread. Used for Tests only, to simulate a remote runner more realistically.
|
3
|
+
#
|
4
|
+
module Workling
|
5
|
+
module Clients
|
6
|
+
class ThreadClient < Workling::Clients::Base
|
7
|
+
|
8
|
+
def dispatch(clazz, method, options = {})
|
9
|
+
Thread.new {
|
10
|
+
Workling.find(clazz, method).dispatch_to_worker_method(method, options)
|
11
|
+
}
|
12
|
+
|
13
|
+
return nil
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
#
|
2
|
+
# An XMPP client
|
3
|
+
#
|
4
|
+
# How to use: this client requires the xmpp4r gem
|
5
|
+
#
|
6
|
+
# in the config/environments/development.rb file (or production.rb etc)
|
7
|
+
#
|
8
|
+
# Workling::Remote::Runners::ClientRunner.client = Workling::Clients::XmppClient.new
|
9
|
+
# Workling::Remote.dispatcher = Workling::Remote::Runners::ClientRunner.new # dont use the standard runner
|
10
|
+
# Workling::Remote.invoker = Workling::Remote::Invokers::LoopedSubscriber # does not work with the EventmachineSubscriber Invoker
|
11
|
+
#
|
12
|
+
# furthermore in the workling.yml file you need to set up the server details for your XMPP server
|
13
|
+
#
|
14
|
+
# development:
|
15
|
+
# listens_on: "localhost:22122"
|
16
|
+
# jabber_id: "sub@localhost/laptop"
|
17
|
+
# jabber_server: "localhost"
|
18
|
+
# jabber_password: "sub"
|
19
|
+
# jabber_service: "pubsub.derfredtop.local"
|
20
|
+
#
|
21
|
+
# for details on how to configure your XMPP server (ejabberd) check out the following howto:
|
22
|
+
#
|
23
|
+
# http://keoko.wordpress.com/2008/12/17/xmpp-pubsub-with-ejabberd-and-xmpp4r/
|
24
|
+
#
|
25
|
+
#
|
26
|
+
# finally you need to expose your worker methods to XMPP nodes like so:
|
27
|
+
#
|
28
|
+
# class NotificationWorker < Workling::Base
|
29
|
+
#
|
30
|
+
# expose :receive_notification, :as => "/home/localhost/pub/sub"
|
31
|
+
#
|
32
|
+
# def receive_notification(input)
|
33
|
+
# # something here
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
|
39
|
+
|
40
|
+
|
41
|
+
module Workling
|
42
|
+
module Clients
|
43
|
+
class XmppClient < Workling::Clients::BrokerBase
|
44
|
+
|
45
|
+
def self.load
|
46
|
+
begin
|
47
|
+
gem "xmpp4r"
|
48
|
+
require 'xmpp4r'
|
49
|
+
require "xmpp4r/pubsub"
|
50
|
+
require "xmpp4r/pubsub/helper/servicehelper.rb"
|
51
|
+
require "xmpp4r/pubsub/helper/nodebrowser.rb"
|
52
|
+
require "xmpp4r/pubsub/helper/nodehelper.rb"
|
53
|
+
rescue Exception => e
|
54
|
+
raise WorklingError.new("Couldnt load the XMPP library. check that you have the xmpp4r gem installed")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# starts the client.
|
59
|
+
def connect
|
60
|
+
begin
|
61
|
+
@client = Jabber::Client.new Workling.config[:jabber_id]
|
62
|
+
@client.connect Workling.config[:jabber_server]
|
63
|
+
@client.auth Workling.config[:jabber_password]
|
64
|
+
@client.send Jabber::Presence.new.set_type(:available)
|
65
|
+
@pubsub = Jabber::PubSub::ServiceHelper.new(@client, Workling.config[:jabber_service])
|
66
|
+
unsubscribe_from_all # make sure there are no open subscriptions, could cause multiple delivery of notifications, as they are persistent
|
67
|
+
rescue
|
68
|
+
raise WorklingError.new("couldn't connect to the jabber server")
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# disconnect from the server
|
73
|
+
def close
|
74
|
+
@client.close
|
75
|
+
end
|
76
|
+
|
77
|
+
# subscribe to a queue
|
78
|
+
def subscribe(key)
|
79
|
+
@pubsub.subscribe_to(key)
|
80
|
+
|
81
|
+
# filter out the subscription notification message that was generated by subscribing to the node
|
82
|
+
@pubsub.get_subscriptions_from_all_nodes()
|
83
|
+
|
84
|
+
@pubsub.add_event_callback do |event|
|
85
|
+
event.payload.each do |e|
|
86
|
+
e.children.each do |child|
|
87
|
+
yield Hash.from_xml(child.children.first.to_s) if child.name == 'item'
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# request and retrieve work
|
94
|
+
def retrieve(key)
|
95
|
+
@pubsub.get_items_from(key, 1)
|
96
|
+
end
|
97
|
+
|
98
|
+
def request(key, value)
|
99
|
+
@pubsub.publish_item_to(key, value)
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
def unsubscribe_from_all
|
104
|
+
@pubsub.get_subscriptions_from_all_nodes.each do |subscription|
|
105
|
+
@pubsub.unsubscribe_from subscription.node
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
#
|
2
|
+
# Discovery is responsible for loading workers in app/workers.
|
3
|
+
#
|
4
|
+
module Workling
|
5
|
+
class Discovery
|
6
|
+
cattr_accessor :discovered
|
7
|
+
@@discovered ||= []
|
8
|
+
|
9
|
+
# requires worklings so that they are added to routing.
|
10
|
+
def self.discover!
|
11
|
+
Workling.load_path.each do |p|
|
12
|
+
Dir.glob(p).each { |wling| require wling }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
|
3
|
+
#
|
4
|
+
# TODO - Subscribes a single worker to a single queue
|
5
|
+
#
|
6
|
+
module Workling
|
7
|
+
module Invokers
|
8
|
+
class AmqpSingleSubscriber < Workling::Invokers::Base
|
9
|
+
|
10
|
+
def initialize(routing, client_class)
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
#
|
15
|
+
# Starts EM loop and sets up subscription callback for the worker
|
16
|
+
# Create the queue and bind to exchange using the routing key
|
17
|
+
#
|
18
|
+
def listen
|
19
|
+
EM.run do
|
20
|
+
connect do
|
21
|
+
queue_name = @routing.queue_for
|
22
|
+
routing_key = @routing.routing_key_for
|
23
|
+
|
24
|
+
# temp stuff to hook the queues and exchanges up
|
25
|
+
# wildcard routing - # (match all)
|
26
|
+
exch = MQ.topic
|
27
|
+
q = MQ.queue(queue_name)
|
28
|
+
q.bind(exch, :key => routing_key)
|
29
|
+
|
30
|
+
@client.subscribe(queue_name) do |args|
|
31
|
+
run(queue_name, args)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def stop
|
38
|
+
EM.stop if EM.reactor_running?
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|