cloudist 0.2.1 → 0.4.1

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 (49) hide show
  1. data/Gemfile +15 -11
  2. data/Gemfile.lock +20 -7
  3. data/README.md +61 -39
  4. data/VERSION +1 -1
  5. data/cloudist.gemspec +50 -16
  6. data/examples/amqp/Gemfile +3 -0
  7. data/examples/amqp/Gemfile.lock +12 -0
  8. data/examples/amqp/amqp_consumer.rb +56 -0
  9. data/examples/amqp/amqp_publisher.rb +50 -0
  10. data/examples/queue_message.rb +7 -7
  11. data/examples/sandwich_client_with_custom_listener.rb +77 -0
  12. data/examples/sandwich_worker_with_class.rb +22 -7
  13. data/lib/cloudist.rb +113 -56
  14. data/lib/cloudist/application.rb +60 -0
  15. data/lib/cloudist/core_ext/class.rb +139 -0
  16. data/lib/cloudist/core_ext/kernel.rb +13 -0
  17. data/lib/cloudist/core_ext/module.rb +11 -0
  18. data/lib/cloudist/encoding.rb +13 -0
  19. data/lib/cloudist/errors.rb +2 -0
  20. data/lib/cloudist/job.rb +21 -18
  21. data/lib/cloudist/listener.rb +108 -54
  22. data/lib/cloudist/message.rb +97 -0
  23. data/lib/cloudist/messaging.rb +29 -0
  24. data/lib/cloudist/payload.rb +45 -105
  25. data/lib/cloudist/payload_old.rb +155 -0
  26. data/lib/cloudist/publisher.rb +7 -2
  27. data/lib/cloudist/queue.rb +152 -0
  28. data/lib/cloudist/queues/basic_queue.rb +83 -53
  29. data/lib/cloudist/queues/job_queue.rb +13 -24
  30. data/lib/cloudist/queues/reply_queue.rb +13 -21
  31. data/lib/cloudist/request.rb +33 -7
  32. data/lib/cloudist/worker.rb +9 -2
  33. data/lib/cloudist_old.rb +300 -0
  34. data/lib/em/em_timer_utils.rb +55 -0
  35. data/lib/em/iterator.rb +27 -0
  36. data/spec/cloudist/message_spec.rb +91 -0
  37. data/spec/cloudist/messaging_spec.rb +19 -0
  38. data/spec/cloudist/payload_spec.rb +10 -4
  39. data/spec/cloudist/payload_spec_2_spec.rb +78 -0
  40. data/spec/cloudist/queue_spec.rb +16 -0
  41. data/spec/cloudist_spec.rb +49 -45
  42. data/spec/spec_helper.rb +0 -1
  43. data/spec/support/amqp.rb +16 -0
  44. metadata +112 -102
  45. data/examples/extending_values.rb +0 -44
  46. data/examples/sandwich_client.rb +0 -57
  47. data/lib/cloudist/callback.rb +0 -16
  48. data/lib/cloudist/callback_methods.rb +0 -19
  49. data/lib/cloudist/callbacks/error_callback.rb +0 -14
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ require "rubygems"
5
+ require 'amqp'
6
+
7
+ def amqp_settings
8
+ uri = URI.parse(ENV["AMQP_URL"] || 'amqp://guest:guest@localhost:5672/')
9
+ {
10
+ :vhost => uri.path,
11
+ :host => uri.host,
12
+ :user => uri.user,
13
+ :port => uri.port || 5672,
14
+ :pass => uri.password,
15
+ :heartbeat => 120,
16
+ :logging => false
17
+ }
18
+ rescue Object => e
19
+ raise "invalid AMQP_URL: (#{uri.inspect}) #{e.class} -> #{e.message}"
20
+ end
21
+
22
+ p amqp_settings
23
+
24
+ def log(*args)
25
+ puts args.inspect
26
+ end
27
+
28
+ EM.run do
29
+ puts "Running..."
30
+ AMQP.start(amqp_settings) do |connection|
31
+ log "Connected to AMQP broker"
32
+
33
+ channel = AMQP::Channel.new(connection)
34
+ channel.prefetch(1)
35
+ queue = channel.queue("test.hello.world")
36
+ exchange = channel.direct
37
+ queue.bind(exchange)
38
+
39
+ EM.defer do
40
+ 10000.times { |i|
41
+ log "Publishing message #{i+1}"
42
+ if i % 1000 == 0
43
+ puts "Sleeping..."
44
+ sleep(1)
45
+ end
46
+ exchange.publish "Hello, world! - #{i+1}"#, :routing_key => queue.name
47
+ }
48
+ end
49
+ end
50
+ end
@@ -2,16 +2,16 @@ $:.unshift File.dirname(__FILE__) + '/../lib'
2
2
  require "rubygems"
3
3
  require "cloudist"
4
4
 
5
- ::Signal.trap('INT') { Cloudist.stop }
6
- ::Signal.trap('TERM'){ Cloudist.stop }
7
-
5
+ Cloudist.signal_trap!
6
+ #
7
+ # This demonstrates how to send a message to a listener
8
+ #
8
9
  Cloudist.start {
9
10
 
10
- payload = Cloudist::Payload.new({:event => "started"})
11
+ payload = Cloudist::Payload.new(:event => :started, :message_type => 'event')
11
12
 
12
13
  q = Cloudist::ReplyQueue.new('temp.reply.make.sandwich')
13
- q.setup
14
- q.publish_to_q(payload)
14
+ q.publish(payload)
15
15
 
16
16
  stop
17
- }
17
+ }
@@ -0,0 +1,77 @@
1
+ # Cloudst Example: Sandwich Client with custom listener class
2
+ #
3
+ # This example demonstrates dispatching a job to the worker and receiving event callbacks.
4
+ #
5
+ # Be sure to update the Cloudist connection settings if they differ from defaults:
6
+ # user: guest
7
+ # pass: guest
8
+ # port: 5672
9
+ # host: localhost
10
+ # vhost: /
11
+ #
12
+ $:.unshift File.dirname(__FILE__) + '/../lib'
13
+ require "rubygems"
14
+ require "cloudist"
15
+
16
+ $total_jobs = 0
17
+
18
+ class SandwichListener < Cloudist::Listener
19
+ listen_to "make.sandwich"
20
+
21
+ before :find_job
22
+
23
+ def find_job
24
+ puts "--- #{payload.id}"
25
+ end
26
+
27
+ def progress(i)
28
+ puts "Progress: %1d%" % i
29
+ end
30
+
31
+ def runtime(seconds)
32
+ puts "#{id} Finished job in #{seconds} seconds"
33
+ $total_jobs -= 1
34
+ puts "--- #{$total_jobs} jobs remaining"
35
+ end
36
+
37
+ # def started
38
+ # puts "Started"
39
+ # end
40
+
41
+ def event(type)
42
+ puts "Event: #{type}"
43
+ end
44
+
45
+ def finished
46
+ puts "*** Finished ***"
47
+
48
+ if $total_jobs == 0
49
+ puts "Completed all jobs"
50
+ Cloudist.stop
51
+ end
52
+ end
53
+
54
+ def error(e)
55
+ puts "#{e.exception}: #{e.message} (#{e.backtrace.first})"
56
+ end
57
+ end
58
+
59
+
60
+ Cloudist.signal_trap!
61
+
62
+ Cloudist.start(:logging => true) {
63
+ puts AMQP.settings.inspect
64
+
65
+ unless ARGV.empty?
66
+ puts "*** Please ensure you have a worker running ***"
67
+
68
+ job_count = ARGV.pop.to_i
69
+ $total_jobs = job_count
70
+ job_count.times { |i|
71
+ log.info("Dispatching sandwich making job...")
72
+ puts "Queued job: " + enqueue('make.sandwich', {:bread => 'white', :sandwich_number => i}).id
73
+ }
74
+ end
75
+
76
+ add_listener(SandwichListener)
77
+ }
@@ -16,22 +16,37 @@ require "cloudist"
16
16
 
17
17
  class SandwichWorker < Cloudist::Worker
18
18
  def process
19
- log.info("Processing queue: #{queue.name}")
20
- log.info(data.inspect)
19
+ log.info("Processing #{queue.name} job: #{id}")
21
20
 
21
+ # This will trigger the start event
22
+ # Appending ! to the end of a method will trigger an
23
+ # event reply with its name
24
+ #
25
+ # e.g. job.working!
26
+ #
22
27
  job.started!
23
- (1..20).each do |i|
24
- job.progress(i * 5)
28
+
29
+ (1..5).each do |i|
30
+ # This sends a progress reply, you could use this to
31
+ # update a progress bar in your UI
32
+ #
33
+ # usage: #progress([INTEGER 0 - 100])
34
+ job.progress(i * 20)
35
+
36
+ # Work hard!
25
37
  sleep(1)
26
38
 
27
- raise ArgumentError, "NOT GOOD!" if i == 4
39
+ # Uncomment this to test error handling in Listener
40
+ # raise ArgumentError, "NOT GOOD!" if i == 4
28
41
  end
42
+
43
+ # Trigger finished event
29
44
  job.finished!
30
45
  end
31
46
  end
32
47
 
33
48
  Cloudist.signal_trap!
34
49
 
35
- Cloudist.start {
36
- Cloudist.handle('make.sandwich', 'eat.sandwich').with(SandwichWorker)
50
+ Cloudist.start(:logging => false) {
51
+ Cloudist.handle('make.sandwich').with(SandwichWorker)
37
52
  }
@@ -1,33 +1,38 @@
1
1
  require 'uri'
2
2
  require 'json' unless defined? ActiveSupport::JSON
3
- require "active_support/hash_with_indifferent_access"
4
3
  require "amqp"
5
- require "mq"
4
+ require "hashie"
6
5
  require "logger"
7
6
  require "digest/md5"
7
+ require "uuid"
8
8
 
9
9
  $:.unshift File.dirname(__FILE__)
10
+
11
+ require "em/em_timer_utils"
10
12
  require "cloudist/core_ext/string"
11
13
  require "cloudist/core_ext/object"
14
+ require "cloudist/core_ext/class"
12
15
  require "cloudist/errors"
13
16
  require "cloudist/utils"
17
+ require "cloudist/encoding"
14
18
  require "cloudist/queues/basic_queue"
15
19
  require "cloudist/queues/job_queue"
16
20
  require "cloudist/queues/reply_queue"
17
21
  require "cloudist/publisher"
18
22
  require "cloudist/payload"
19
23
  require "cloudist/request"
20
- require "cloudist/callback_methods"
21
24
  require "cloudist/listener"
22
- require "cloudist/callback"
23
- require "cloudist/callbacks/error_callback"
24
25
  require "cloudist/job"
25
26
  require "cloudist/worker"
26
27
 
27
28
  module Cloudist
29
+ DEFAULT_TTL = 300
30
+
28
31
  class << self
29
-
30
- @@workers = {}
32
+ thread_local_accessor :channels, :default => {}
33
+ thread_local_accessor :workers, :default => {}
34
+ thread_local_accessor :listeners, :default => []
35
+ thread_local_accessor :listener_instances, :default => {}
31
36
 
32
37
  # Start the Cloudist loop
33
38
  #
@@ -41,15 +46,25 @@ module Cloudist
41
46
  # * :host => 'localhost'
42
47
  # * :port => 5672
43
48
  # * :vhost => /
49
+ # * :heartbeat => 0
50
+ # * :logging => false
44
51
  #
45
52
  # Refer to default config below for how to set these as defaults
46
53
  #
47
54
  def start(options = {}, &block)
48
55
  config = settings.update(options)
49
56
  AMQP.start(config) do
50
- self.instance_eval(&block)
57
+ self.instance_eval(&block) if block_given?
51
58
  end
52
59
  end
60
+
61
+ def connection
62
+ AMQP.connection
63
+ end
64
+
65
+ def connection=(conn)
66
+ AMQP.connection = conn
67
+ end
53
68
 
54
69
  # Define a worker. Must be called inside start loop
55
70
  #
@@ -58,6 +73,7 @@ module Cloudist
58
73
  # }
59
74
  #
60
75
  # REMOVED
76
+ #
61
77
  def worker(&block)
62
78
  raise NotImplementedError, "This DSL format has been removed. Please use job('make.sandwich') {} instead."
63
79
  end
@@ -73,8 +89,13 @@ module Cloudist
73
89
  #
74
90
  # Refer to sandwich_worker.rb example
75
91
  #
76
- def job(queue_name, &block)
77
- register_worker(queue_name, &block)
92
+ def job(queue_name)
93
+ if block_given?
94
+ block = Proc.new
95
+ register_worker(queue_name, &block)
96
+ else
97
+ raise ArgumentError, "You must supply a block as the last argument"
98
+ end
78
99
  end
79
100
 
80
101
  # Registers a worker class to handle a specific queue
@@ -107,28 +128,27 @@ module Cloudist
107
128
  job_queue = JobQueue.new(queue_name)
108
129
  job_queue.subscribe do |request|
109
130
  j = Job.new(request.payload.dup)
110
- EM.defer do
111
- begin
112
- if block_given?
113
- worker_instance = GenericWorker.new(j, job_queue.q)
114
- worker_instance.process(&block)
115
- elsif klass
116
- worker_instance = klass.new(j, job_queue.q)
117
- worker_instance.process
118
- else
119
- raise RuntimeError, "Failed to register worker, I need either a handler class or block."
120
- end
121
- finished = Time.now.utc.to_i
122
- log.debug("Finished Job in #{finished - request.start} seconds")
123
-
124
- rescue Exception => e
125
- j.handle_error(e)
131
+ begin
132
+ if block_given?
133
+ worker_instance = GenericWorker.new(j, job_queue.q)
134
+ worker_instance.process(&block)
135
+ elsif klass
136
+ worker_instance = klass.new(j, job_queue.q)
137
+ worker_instance.process
138
+ else
139
+ raise RuntimeError, "Failed to register worker, I need either a handler class or block."
126
140
  end
141
+ rescue Exception => e
142
+ j.handle_error(e)
143
+ ensure
144
+ finished = Time.now.utc.to_f
145
+ log.debug("Finished Job in #{finished - request.start} seconds")
146
+ j.reply({:runtime => (finished - request.start)}, {:message_type => 'runtime'})
147
+ j.cleanup
127
148
  end
128
- j.cleanup
129
149
  end
130
150
 
131
- ((@@workers[queue_name.to_s] ||= []) << job_queue).uniq!
151
+ ((self.workers[queue_name.to_s] ||= []) << job_queue).uniq!
132
152
  end
133
153
 
134
154
  # Accepts either a queue name or a job instance returned from enqueue.
@@ -136,14 +156,25 @@ module Cloudist
136
156
  # will return all responses regardless of job id so you can use the job
137
157
  # id to lookup a database record to update etc.
138
158
  # When given a job instance it will only return messages from that job.
159
+ #
160
+ # DEPRECATED
161
+ #
139
162
  def listen(*queue_names, &block)
140
- @@listeners ||= []
141
- queue_names.each do |job_or_queue_name|
142
- _listener = Cloudist::Listener.new(job_or_queue_name)
143
- _listener.subscribe(&block)
144
- @@listeners << _listener
163
+ raise NotImplementedError, "This DSL method has been removed. Please use add_listener"
164
+ end
165
+
166
+ # Adds a listener class
167
+ def add_listener(klass)
168
+ raise ArgumentError, "Your listener must extend Cloudist::Listener" unless klass.superclass == Cloudist::Listener
169
+ raise ArgumentError, "Your listener must declare at least one queue to listen to. Use listen_to 'queue.name'" if klass.job_queue_names.nil?
170
+
171
+ klass.job_queue_names.each do |queue_name|
172
+ klass.subscribe(queue_name)
145
173
  end
146
- return @@listeners
174
+
175
+ self.listeners << klass
176
+
177
+ return self.listeners
147
178
  end
148
179
 
149
180
  # Enqueues a job.
@@ -151,37 +182,52 @@ module Cloudist
151
182
  # Returns Job instance
152
183
  # Use Job#id to reference job later on.
153
184
  def enqueue(job_queue_name, data = nil)
154
- raise EnqueueError, "Incorrect arguments, you must include data when enquing job" if data.nil?
185
+ raise EnqueueError, "Incorrect arguments, you must include data when enqueuing job" if data.nil?
155
186
  # TODO: Detect if inside loop, if not use bunny sync
156
187
  Cloudist::Publisher.enqueue(job_queue_name, data)
157
188
  end
189
+
190
+ # Send a reply synchronously
191
+ # This uses bunny instead of AMQP and as such can be run outside
192
+ # of EventMachine and the Cloudist start loop.
193
+ #
194
+ # Usage: Cloudist.reply('make.sandwich', {:sandwhich_id => 12345})
195
+ #
196
+ # def reply(queue_name, job_id, data, options = {})
197
+ # headers = {
198
+ # :message_id => job_id,
199
+ # :message_type => "reply",
200
+ # # :event => 'working',
201
+ # :message_type => 'reply'
202
+ # }.update(options)
203
+ #
204
+ # payload = Cloudist::Payload.new(data, headers)
205
+ #
206
+ # queue = Cloudist::SyncReplyQueue.new(queue_name)
207
+ #
208
+ # queue.setup
209
+ # queue.publish_to_q(payload)
210
+ # end
158
211
 
159
212
  # Call this at anytime inside the loop to exit the app.
160
213
  def stop_safely
161
- # ::EM.add_timer(0.2) {
162
- ::AMQP.stop {
163
- ::EM.stop
214
+ if EM.reactor_running?
215
+ ::EM.add_timer(0.2) {
216
+ ::AMQP.stop {
217
+ ::EM.stop
218
+ puts "\n"
219
+ }
164
220
  }
165
- # }
221
+ end
166
222
  end
167
223
 
168
224
  alias :stop :stop_safely
169
-
170
- def closing?
171
- ::AMQP.closing?
172
- end
173
-
174
- def log
175
- @@log ||= Logger.new($stdout)
176
- end
177
-
178
- def log=(log)
179
- @@log = log
180
- end
181
225
 
182
226
  def handle_error(e)
183
227
  log.error "#{e.class}: #{e.message}"#, :exception => e
184
- log.error e.backtrace.join("\n")
228
+ e.backtrace.each do |line|
229
+ log.error line
230
+ end
185
231
  end
186
232
 
187
233
  def version
@@ -195,7 +241,9 @@ module Cloudist
195
241
  :host => uri.host,
196
242
  :user => uri.user,
197
243
  :port => uri.port || 5672,
198
- :pass => uri.password
244
+ :pass => uri.password,
245
+ :heartbeat => 0,
246
+ :logging => false
199
247
  }
200
248
  rescue Object => e
201
249
  raise "invalid AMQP_URL: (#{uri.inspect}) #{e.class} -> #{e.message}"
@@ -214,14 +262,23 @@ module Cloudist
214
262
  ::Signal.trap('TERM'){ Cloudist.stop }
215
263
  end
216
264
 
217
- def workers
218
- @@workers
265
+ def log
266
+ @@log ||= Logger.new($stdout)
267
+ end
268
+
269
+ def log=(log)
270
+ @@log = log
219
271
  end
220
272
 
273
+ alias :install_signal_trap :signal_trap!
274
+
221
275
  def remove_workers
222
- @@workers = {}
276
+ self.workers.keys.each do |worker|
277
+ self.workers.delete(worker)
278
+ end
223
279
  end
224
280
 
225
281
  end
226
282
 
283
+ include Cloudist::EMTimerUtils
227
284
  end