cloudist 0.2.1 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
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