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
@@ -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