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,60 @@
1
+ require "singleton"
2
+
3
+ module Cloudist
4
+ class Application
5
+ include Singleton
6
+
7
+ class << self
8
+ def start(options = {}, &block)
9
+ options = instance.settings.update(options)
10
+ AMQP.start(options) do
11
+ instance.setup_reconnect_hook!
12
+
13
+ instance.instance_eval(&block) if block_given?
14
+ end
15
+ end
16
+
17
+ def signal_trap!
18
+ ::Signal.trap('INT') { Cloudist.stop }
19
+ ::Signal.trap('TERM'){ Cloudist.stop }
20
+ end
21
+ end
22
+
23
+ def settings
24
+ @@settings ||= default_settings
25
+ end
26
+
27
+ def settings=(settings_hash)
28
+ @@settings = default_settings.update(settings_hash)
29
+ end
30
+
31
+ def default_settings
32
+ uri = URI.parse(ENV["AMQP_URL"] || 'amqp://guest:guest@localhost:5672/')
33
+ {
34
+ :vhost => uri.path,
35
+ :host => uri.host,
36
+ :user => uri.user,
37
+ :port => uri.port || 5672,
38
+ :pass => uri.password,
39
+ :heartbeat => 5,
40
+ :logging => false
41
+ }
42
+ rescue Object => e
43
+ raise "invalid AMQP_URL: (#{uri.inspect}) #{e.class} -> #{e.message}"
44
+ end
45
+
46
+ private
47
+
48
+ def setup_reconnect_hook!
49
+ AMQP.conn.connection_status do |status|
50
+
51
+ log.debug("AMQP connection status changed: #{status}")
52
+
53
+ if status == :disconnected
54
+ AMQP.conn.reconnect(true)
55
+ end
56
+ end
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,139 @@
1
+ require 'cloudist/core_ext/kernel'
2
+ require 'cloudist/core_ext/module'
3
+
4
+ # Extracted from ActiveSupport 3.0
5
+ class Class
6
+
7
+ # Taken from http://coderrr.wordpress.com/2008/04/10/lets-stop-polluting-the-threadcurrent-hash/
8
+ def thread_local_accessor name, options = {}
9
+ m = Module.new
10
+ m.module_eval do
11
+ class_variable_set :"@@#{name}", Hash.new {|h,k| h[k] = options[:default] }
12
+ end
13
+ m.module_eval %{
14
+ FINALIZER = lambda {|id| @@#{name}.delete id }
15
+
16
+ def #{name}
17
+ @@#{name}[Thread.current.object_id]
18
+ end
19
+
20
+ def #{name}=(val)
21
+ ObjectSpace.define_finalizer Thread.current, FINALIZER unless @@#{name}.has_key? Thread.current.object_id
22
+ @@#{name}[Thread.current.object_id] = val
23
+ end
24
+ }
25
+
26
+ class_eval do
27
+ include m
28
+ extend m
29
+ end
30
+ end
31
+
32
+
33
+ # Declare a class-level attribute whose value is inheritable by subclasses.
34
+ # Subclasses can change their own value and it will not impact parent class.
35
+ #
36
+ # class Base
37
+ # class_attribute :setting
38
+ # end
39
+ #
40
+ # class Subclass < Base
41
+ # end
42
+ #
43
+ # Base.setting = true
44
+ # Subclass.setting # => true
45
+ # Subclass.setting = false
46
+ # Subclass.setting # => false
47
+ # Base.setting # => true
48
+ #
49
+ # In the above case as long as Subclass does not assign a value to setting
50
+ # by performing <tt>Subclass.setting = _something_ </tt>, <tt>Subclass.setting</tt>
51
+ # would read value assigned to parent class. Once Subclass assigns a value then
52
+ # the value assigned by Subclass would be returned.
53
+ #
54
+ # This matches normal Ruby method inheritance: think of writing an attribute
55
+ # on a subclass as overriding the reader method. However, you need to be aware
56
+ # when using +class_attribute+ with mutable structures as +Array+ or +Hash+.
57
+ # In such cases, you don't want to do changes in places but use setters:
58
+ #
59
+ # Base.setting = []
60
+ # Base.setting # => []
61
+ # Subclass.setting # => []
62
+ #
63
+ # # Appending in child changes both parent and child because it is the same object:
64
+ # Subclass.setting << :foo
65
+ # Base.setting # => [:foo]
66
+ # Subclass.setting # => [:foo]
67
+ #
68
+ # # Use setters to not propagate changes:
69
+ # Base.setting = []
70
+ # Subclass.setting += [:foo]
71
+ # Base.setting # => []
72
+ # Subclass.setting # => [:foo]
73
+ #
74
+ # For convenience, a query method is defined as well:
75
+ #
76
+ # Subclass.setting? # => false
77
+ #
78
+ # Instances may overwrite the class value in the same way:
79
+ #
80
+ # Base.setting = true
81
+ # object = Base.new
82
+ # object.setting # => true
83
+ # object.setting = false
84
+ # object.setting # => false
85
+ # Base.setting # => true
86
+ #
87
+ # To opt out of the instance writer method, pass :instance_writer => false.
88
+ #
89
+ # object.setting = false # => NoMethodError
90
+ def class_attribute(*attrs)
91
+ instance_writer = !attrs.last.is_a?(Hash) || attrs.pop[:instance_writer]
92
+
93
+ attrs.each do |name|
94
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
95
+ def self.#{name}() nil end
96
+ def self.#{name}?() !!#{name} end
97
+
98
+ def self.#{name}=(val)
99
+ singleton_class.class_eval do
100
+ remove_possible_method(:#{name})
101
+ define_method(:#{name}) { val }
102
+ end
103
+
104
+ if singleton_class?
105
+ class_eval do
106
+ remove_possible_method(:#{name})
107
+ def #{name}
108
+ defined?(@#{name}) ? @#{name} : singleton_class.#{name}
109
+ end
110
+ end
111
+ end
112
+ val
113
+ end
114
+
115
+ remove_method :#{name} if method_defined?(:#{name})
116
+ def #{name}
117
+ defined?(@#{name}) ? @#{name} : self.class.#{name}
118
+ end
119
+
120
+ def #{name}?
121
+ !!#{name}
122
+ end
123
+ RUBY
124
+
125
+ attr_writer name if instance_writer
126
+ end
127
+ end
128
+
129
+ private
130
+ def singleton_class?
131
+ # in case somebody is crazy enough to overwrite allocate
132
+ allocate = Class.instance_method(:allocate)
133
+ # object.class always points to a real (non-singleton) class
134
+ allocate.bind(self).call.class != self
135
+ rescue TypeError
136
+ # MRI/YARV/JRuby all disallow creating new instances of a singleton class
137
+ true
138
+ end
139
+ end
@@ -0,0 +1,13 @@
1
+ module Kernel
2
+ # Returns the object's singleton class.
3
+ def singleton_class
4
+ class << self
5
+ self
6
+ end
7
+ end unless respond_to?(:singleton_class) # exists in 1.9.2
8
+
9
+ # class_eval on an object acts like singleton_class.class_eval.
10
+ def class_eval(*args, &block)
11
+ singleton_class.class_eval(*args, &block)
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ class Module
2
+ def remove_possible_method(method)
3
+ remove_method(method)
4
+ rescue NameError
5
+ end
6
+
7
+ def redefine_method(method, &block)
8
+ remove_possible_method(method)
9
+ define_method(method, &block)
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ module Cloudist
2
+ module Encoding
3
+ def encode(message)
4
+ Marshal.dump(message)
5
+ end
6
+
7
+ def decode(message)
8
+ raise ArgumentError, "First argument can't be nil" if message.nil?
9
+ return message unless message.is_a?(String)
10
+ Marshal.load(message)
11
+ end
12
+ end
13
+ end
@@ -3,4 +3,6 @@ module Cloudist
3
3
  class BadPayload < Error; end
4
4
  class EnqueueError < Error; end
5
5
  class StaleHeadersError < BadPayload; end
6
+ class UnknownReplyTo < RuntimeError; end
7
+ class ExpiredMessage < RuntimeError; end
6
8
  end
@@ -1,8 +1,16 @@
1
1
  module Cloudist
2
2
  class Job
3
- attr_reader :payload
3
+ attr_reader :payload, :reply_queue
4
+
4
5
  def initialize(payload)
5
6
  @payload = payload
7
+
8
+ if payload.reply_to
9
+ @reply_queue = ReplyQueue.new(payload.reply_to)
10
+ reply_queue.setup
11
+ else
12
+ @reply_queue = nil
13
+ end
6
14
  end
7
15
 
8
16
  def id
@@ -13,34 +21,34 @@ module Cloudist
13
21
  payload.body
14
22
  end
15
23
 
24
+ def body
25
+ data
26
+ end
27
+
16
28
  def log
17
29
  Cloudist.log
18
30
  end
19
31
 
20
32
  def cleanup
21
-
33
+ # :noop
22
34
  end
23
35
 
24
36
  def reply(body, headers = {}, options = {})
37
+ raise ArgumentError, "Reply queue not ready" unless reply_queue
38
+
25
39
  options = {
26
40
  :echo => false
27
41
  }.update(options)
28
42
 
29
43
  headers = {
30
- :message_id => payload.headers[:message_id],
44
+ :message_id => payload.id,
31
45
  :message_type => "reply"
32
46
  }.update(headers)
33
47
 
34
- # Echo the payload back
35
- # body.merge!(payload.body) if options[:echo] == true
36
-
37
48
  reply_payload = Payload.new(body, headers)
49
+ published_headers = reply_queue.publish(reply_payload)
38
50
 
39
- reply_queue = ReplyQueue.new(payload.reply_to)
40
- reply_queue.setup
41
- published_headers = reply_queue.publish_to_q(reply_payload)
42
-
43
- log.debug("Replying: #{body.inspect} HEADERS: #{headers.inspect}")
51
+ reply_payload
44
52
  end
45
53
 
46
54
  # Sends a progress update
@@ -51,23 +59,18 @@ module Cloudist
51
59
  end
52
60
 
53
61
  def event(event_name, event_data = {}, options = {})
54
- event_data = {} if event_data.nil?
62
+ event_data ||= {}
55
63
  reply(event_data, {:event => event_name, :message_type => 'event'}, options)
56
64
  end
57
65
 
58
66
  def safely(&blk)
59
- # begin
60
67
  yield
61
68
  rescue Exception => e
62
69
  handle_error(e)
63
- # end
64
- # result
65
70
  end
66
71
 
67
- # This will transfer the Exception object to the client
68
72
  def handle_error(e)
69
- # reply({:exception_class => e.class.name, :message => e.message, :backtrace => e.backtrace}, {:message_type => 'error'})
70
- reply({:exception => e}, {:message_type => 'error'})
73
+ reply({:exception => e.class.name.to_s, :message => e.message.to_s, :backtrace => e.backtrace}, {:message_type => 'error'})
71
74
  end
72
75
 
73
76
  def method_missing(meth, *args, &blk)
@@ -1,79 +1,133 @@
1
+ require "active_support"
1
2
  module Cloudist
2
3
  class Listener
3
- include Cloudist::CallbackMethods
4
+ include ActiveSupport::Callbacks
4
5
 
5
- attr_reader :job_queue_name, :job_id, :callbacks
6
+ attr_reader :job_queue_name, :payload
7
+ class_attribute :job_queue_names
6
8
 
7
- @@valid_callbacks = ["event", "progress", "reply", "update", "error"]
8
-
9
- def initialize(job_or_queue_name)
10
- @callbacks = {}
9
+ class << self
10
+ def listen_to(*job_queue_names)
11
+ self.job_queue_names = job_queue_names.map { |q| Utils.reply_prefix(q) }
12
+ end
11
13
 
12
- if job_or_queue_name.is_a?(Cloudist::Job)
13
- @job_queue_name = Utils.reply_prefix(job_or_queue_name.payload.headers[:master_queue])
14
- @job_id = job_or_queue_name.id
15
- elsif job_or_queue_name.is_a?(String)
16
- @job_queue_name = Utils.reply_prefix(job_or_queue_name)
17
- @job_id = nil
18
- else
19
- raise ArgumentError, "Invalid listener type, accepts job queue name or Cloudist::Job instance"
14
+ def subscribe(queue_name)
15
+ raise RuntimeError, "You can't subscribe until EM is running" unless EM.reactor_running?
16
+
17
+ reply_queue = Cloudist::ReplyQueue.new(queue_name)
18
+ reply_queue.subscribe do |request|
19
+ instance = Cloudist.listener_instances[queue_name] ||= new
20
+ instance.handle_request(request)
21
+ end
22
+
23
+ queue_name
24
+ end
25
+
26
+ def before(*args, &block)
27
+ set_callback(:call, :before, *args, &block)
28
+ end
29
+
30
+ def after(*args, &block)
31
+ set_callback(:call, :after, *args, &block)
20
32
  end
21
33
  end
22
34
 
23
- def subscribe(&block)
24
- reply_queue = Cloudist::ReplyQueue.new(job_queue_name)
25
- reply_queue.setup(job_id) if job_id
35
+ define_callbacks :call, :rescuable => true
36
+
37
+ def handle_request(request)
38
+ @payload = request.payload
39
+ key = [payload.message_type.to_s, payload.headers[:event]].compact.join(':')
26
40
 
27
- self.instance_eval(&block)
41
+ meth, *args = handle_key(key)
28
42
 
29
- reply_queue.subscribe do |request|
30
- payload = request.payload
31
-
32
- key = [payload.message_type.to_s, payload.headers[:event]].compact.join(':')
33
-
34
- # If we want to get a callback on every event, do it here
35
- if callbacks.has_key?('everything')
36
- callbacks['everything'].each do |c|
37
- c.call(payload)
38
- end
39
- end
40
-
41
- if callbacks.has_key?('error')
42
- callbacks['error'].each do |c|
43
- # c.call(payload)
44
-
45
- end
46
- end
47
-
48
- if callbacks.has_key?(key)
49
- callbacks_to_call = callbacks[key]
50
- callbacks_to_call.each do |c|
51
- c.call(payload)
52
- end
43
+ if meth.present? && self.respond_to?(meth)
44
+ if method(meth).arity <= args.size
45
+ call(meth, args.first(method(meth).arity))
46
+ else
47
+ raise ArgumentError, "Unable to fire callback (#{meth}) because we don't have enough args"
53
48
  end
54
49
  end
55
50
  end
56
51
 
57
- def everything(&blk)
58
- (@callbacks['everything'] ||= []) << Callback.new(blk)
52
+ def id
53
+ payload.id
59
54
  end
60
55
 
61
- def method_missing(meth, *args, &blk)
62
- if @@valid_callbacks.include?(meth.to_s)
56
+ def data
57
+ payload.body
58
+ end
59
+
60
+ def handle_key(key)
61
+ key = key.split(':', 2)
62
+ return [nil, nil] if key.empty?
63
+
64
+ method_and_args = [key.shift.to_sym]
65
+ case method_and_args[0]
66
+ when :event
67
+ if key.size > 0 && self.respond_to?(key.first)
68
+ method_and_args = [key.shift]
69
+ end
70
+ method_and_args << key
71
+
72
+ when :progress
73
+ method_and_args << payload.progress
74
+ method_and_args << payload.description
75
+
76
+ when :runtime
77
+ method_and_args << payload.runtime
63
78
 
64
- # callback should in format of "event:started" or "progress"
65
- key = [meth.to_s, args.shift].compact.join(':')
79
+ when :reply
66
80
 
67
- case meth.to_sym
68
- when :error
69
- (@callbacks[key] ||= []) << ErrorCallback.new(blk)
81
+ when :update
82
+
83
+ when :error
84
+ # method_and_args << Cloudist::SafeError.new(payload)
85
+ method_and_args << Hashie::Mash.new(payload.body)
86
+
87
+ when :log
88
+ method_and_args << payload.message
89
+ method_and_args << payload.level
90
+
91
+ else
92
+ method_and_args << data if method(method_and_args[0]).arity == 1
93
+ end
94
+
95
+ return method_and_args
96
+ end
97
+
98
+ def call(meth, args)
99
+ run_callbacks :call do
100
+ if args.empty?
101
+ send(meth)
70
102
  else
71
- (@callbacks[key] ||= []) << Callback.new(blk)
103
+ send(meth, *args)
72
104
  end
73
- else
74
- super
75
105
  end
76
106
  end
77
107
 
108
+ def progress(pct)
109
+ # :noop
110
+ end
111
+
112
+ def runtime(seconds)
113
+ # :noop
114
+ end
115
+
116
+ def event(type)
117
+ # :noop
118
+ end
119
+
120
+ def log(message, level)
121
+ # :noop
122
+ end
123
+
124
+ def error(e)
125
+ # :noop
126
+ end
127
+
128
+ end
129
+
130
+ class GenericListener < Listener
131
+
78
132
  end
79
133
  end