right_agent 0.10.13 → 0.13.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/lib/right_agent.rb +2 -0
  2. data/lib/right_agent/actor.rb +45 -10
  3. data/lib/right_agent/actor_registry.rb +5 -5
  4. data/lib/right_agent/actors/agent_manager.rb +4 -4
  5. data/lib/right_agent/agent.rb +97 -37
  6. data/lib/right_agent/agent_tag_manager.rb +1 -2
  7. data/lib/right_agent/command/command_io.rb +1 -3
  8. data/lib/right_agent/command/command_runner.rb +9 -3
  9. data/lib/right_agent/dispatched_cache.rb +110 -0
  10. data/lib/right_agent/dispatcher.rb +119 -180
  11. data/lib/right_agent/history.rb +136 -0
  12. data/lib/right_agent/log.rb +6 -3
  13. data/lib/right_agent/monkey_patches/ruby_patch.rb +0 -1
  14. data/lib/right_agent/pid_file.rb +1 -1
  15. data/lib/right_agent/platform.rb +2 -2
  16. data/lib/right_agent/platform/linux.rb +8 -1
  17. data/lib/right_agent/platform/windows.rb +1 -1
  18. data/lib/right_agent/sender.rb +57 -41
  19. data/right_agent.gemspec +4 -4
  20. data/spec/actor_registry_spec.rb +7 -8
  21. data/spec/actor_spec.rb +87 -24
  22. data/spec/agent_spec.rb +107 -8
  23. data/spec/command/command_runner_spec.rb +12 -1
  24. data/spec/dispatched_cache_spec.rb +142 -0
  25. data/spec/dispatcher_spec.rb +110 -129
  26. data/spec/history_spec.rb +234 -0
  27. data/spec/idempotent_request_spec.rb +1 -1
  28. data/spec/log_spec.rb +15 -0
  29. data/spec/operation_result_spec.rb +4 -2
  30. data/spec/platform/darwin_spec.rb +13 -0
  31. data/spec/platform/linux_spec.rb +38 -0
  32. data/spec/platform/platform_spec.rb +46 -51
  33. data/spec/platform/windows_spec.rb +13 -0
  34. data/spec/sender_spec.rb +81 -38
  35. metadata +12 -9
  36. data/lib/right_agent/monkey_patches/ruby_patch/singleton_patch.rb +0 -45
  37. data/spec/platform/darwin.rb +0 -11
  38. data/spec/platform/linux.rb +0 -23
  39. data/spec/platform/windows.rb +0 -11
@@ -0,0 +1,136 @@
1
+ #
2
+ # Copyright (c) 2009-2011 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ module RightScale
24
+
25
+ # Agent history manager
26
+ class History
27
+
28
+ # Initialize history
29
+ #
30
+ # === Parameters
31
+ # identity(String):: Serialized agent identity
32
+ def initialize(identity)
33
+ @pid = Process.pid
34
+ @history = File.join(AgentConfig.pid_dir, identity + ".history")
35
+ end
36
+
37
+ # Append event to history file
38
+ #
39
+ # === Parameters
40
+ # event(Object):: Event to be stored in the form String or {String => Object},
41
+ # where String is the event name and Object is any associated JSON-encodable data
42
+ #
43
+ # === Return
44
+ # true:: Always return true
45
+ def update(event)
46
+ @last_update = {:time => Time.now.to_i, :pid => @pid, :event => event}
47
+ File.open(@history, "a") { |f| f.puts @last_update.to_json }
48
+ true
49
+ end
50
+
51
+ # Load events from history file
52
+ #
53
+ # === Return
54
+ # events(Array):: List of historical events with each being a hash of
55
+ # :time(Integer):: Time in seconds in Unix-epoch when event occurred
56
+ # :pid(Integer):: Process id of agent recording the event
57
+ # :event(Object):: Event object in the form String or {String => Object},
58
+ # where String is the event name and Object is any associated JSON-encodable data
59
+ def load
60
+ events = []
61
+ File.open(@history, "r") { |f| events = f.readlines.map { |l| JSON.load(l) } } if File.readable?(@history)
62
+ events
63
+ end
64
+
65
+ # Analyze history to determine service attributes like uptime and restart/crash counts
66
+ #
67
+ # === Return
68
+ # (Hash):: Results of analysis
69
+ # :uptime(Integer):: Current time in service
70
+ # :total_uptime(Integer):: Total time in service (but if there were crashes
71
+ # this total includes recovery time, which makes it inaccurate)
72
+ # :restarts(Integer|nil):: Number of restarts, if any
73
+ # :graceful_exits(Integer|nil):: Number of graceful terminations, if any
74
+ # :crashes(Integer|nil):: Number of crashes, if any
75
+ # :last_crash_time(Integer|nil):: Time in seconds in Unix-epoch when last crash occurred, if any
76
+ def analyze_service
77
+ now = Time.now.to_i
78
+ if @last_analysis && @last_event == @last_update
79
+ if @last_analysis[:uptime] > 0
80
+ delta = now - @last_analysis_time
81
+ @last_analysis[:uptime] += delta
82
+ @last_analysis[:total_uptime] += delta
83
+ end
84
+ else
85
+ last_run = last_crash = @last_event = {:time => 0, :pid => 0, :event => nil}
86
+ restarts = graceful_exits = crashes = accumulated_uptime = 0
87
+ load.each do |event|
88
+ event = SerializationHelper.symbolize_keys(event)
89
+ case event[:event]
90
+ when "start"
91
+ case @last_event[:event]
92
+ when "stop", "graceful exit"
93
+ restarts += 1
94
+ when "start"
95
+ crashes += 1
96
+ last_crash = event
97
+ when "run"
98
+ crashes += 1
99
+ last_crash = event
100
+ # Accumulating uptime here although this will wrongly include recovery time
101
+ accumulated_uptime += (event[:time] - @last_event[:time])
102
+ end
103
+ when "run"
104
+ last_run = event
105
+ when "stop"
106
+ if @last_event[:event] == "run" && @last_event[:pid] == event[:pid]
107
+ accumulated_uptime += (event[:time] - @last_event[:time])
108
+ end
109
+ when "graceful exit"
110
+ graceful_exits += 1
111
+ else
112
+ next
113
+ end
114
+ @last_event = event
115
+ end
116
+ current_uptime = last_run[:pid] == @pid ? (now - last_run[:time]) : 0
117
+ @last_analysis = {
118
+ :uptime => current_uptime,
119
+ :total_uptime => accumulated_uptime + current_uptime
120
+ }
121
+ if restarts > 0
122
+ @last_analysis[:restarts] = restarts
123
+ @last_analysis[:graceful_exits] = graceful_exits
124
+ end
125
+ if crashes > 0
126
+ @last_analysis[:crashes] = crashes
127
+ @last_analysis[:last_crash_time] = last_crash[:time]
128
+ end
129
+ end
130
+ @last_analysis_time = now
131
+ @last_analysis
132
+ end
133
+
134
+ end # History
135
+
136
+ end # RightScale
@@ -29,7 +29,6 @@ RIGHTSCALE_LOG_DEFINED = true
29
29
 
30
30
  require 'logger'
31
31
  require 'right_support'
32
- require 'singleton'
33
32
 
34
33
  require File.expand_path(File.join(File.dirname(__FILE__), 'platform'))
35
34
  require File.expand_path(File.join(File.dirname(__FILE__), 'multiplexer'))
@@ -42,7 +41,7 @@ module RightScale
42
41
 
43
42
  # Expecting use of RightScale patched Singleton so that clients of this
44
43
  # class do not need to use '.instance' in Log calls
45
- include Singleton
44
+ include RightSupport::Ruby::EasySingleton
46
45
 
47
46
  # Default formatter for a Log
48
47
  class Formatter < Logger::Formatter
@@ -118,6 +117,7 @@ module RightScale
118
117
  def initialize
119
118
  # Was log ever used?
120
119
  @initialized = false
120
+ @logger = RightSupport::Log::NullLogger.new # ensures respond_to? works before init is called
121
121
  end
122
122
 
123
123
  # Forward all method calls to underlying Logger object created with init
@@ -147,7 +147,6 @@ module RightScale
147
147
  # === Return
148
148
  # (true|false):: True if this object or its proxy responds to the names method, false otherwise
149
149
  def respond_to?(m)
150
- init unless @initialized
151
150
  super(m) || @logger.respond_to?(m)
152
151
  end
153
152
 
@@ -354,6 +353,10 @@ module RightScale
354
353
  end
355
354
  if new_level != @level
356
355
  @logger.info("[setup] Setting log level to #{level_to_sym(new_level).to_s.upcase}")
356
+ if new_level == Logger::DEBUG && !RightScale::Platform.windows?
357
+ @logger.info("[setup] Check syslog configuration to ensure debug messages are not discarded!")
358
+ else
359
+ end
357
360
  @logger.level = @level = new_level
358
361
  end
359
362
  # Notify even if unchanged since don't know when callback was set
@@ -51,6 +51,5 @@ RUBY_PATCH_BASE_DIR = File.join(File.dirname(__FILE__), 'ruby_patch')
51
51
 
52
52
  require File.normalize_path(File.join(RUBY_PATCH_BASE_DIR, 'array_patch'))
53
53
  require File.normalize_path(File.join(RUBY_PATCH_BASE_DIR, 'object_patch'))
54
- require File.normalize_path(File.join(RUBY_PATCH_BASE_DIR, 'singleton_patch'))
55
54
 
56
55
  end # Unless already defined
@@ -116,7 +116,7 @@ module RightScale
116
116
  open(@cookie_file,'r') do |f|
117
117
  command_options = (YAML.load(f.read) rescue {}) || {}
118
118
  content.merge!(command_options)
119
- end if File.exists?(@cookie_file)
119
+ end if File.readable?(@cookie_file)
120
120
  end
121
121
  content
122
122
  end
@@ -30,7 +30,7 @@ unless defined?(RightScale::Platform)
30
30
  # some install-time gem dependency issues.
31
31
 
32
32
  require 'rbconfig'
33
-
33
+ require 'right_support'
34
34
 
35
35
 
36
36
  # Load ruby interpreter monkey-patches first (to ensure File.normalize_path is defined, etc.).
@@ -69,7 +69,7 @@ module RightScale
69
69
  # - suse?
70
70
  class Platform
71
71
 
72
- include Singleton
72
+ include RightSupport::Ruby::EasySingleton
73
73
 
74
74
  # Generic platform family
75
75
  #
@@ -514,11 +514,18 @@ module RightScale
514
514
  raise PackageManagerNotFound, "No package manager binary (apt, yum, zypper) found in /usr/bin"
515
515
  end
516
516
 
517
- @output = `#{command}`
517
+ @output = run_installer_command(command)
518
518
  @output.scan(regex) { |package| failed_packages << package.first }
519
519
  raise PackageNotFound, "The following packages were not available: #{failed_packages.join(', ')}" unless failed_packages.empty?
520
520
  return true
521
521
  end
522
+
523
+ protected
524
+
525
+ # A test hook so we can mock the invocation of the installer.
526
+ def run_installer_command(cmd)
527
+ `#{cmd}`
528
+ end
522
529
  end
523
530
 
524
531
  end # Platform
@@ -219,7 +219,7 @@ module RightScale
219
219
 
220
220
  # Path to right link configuration and internal usage scripts
221
221
  def private_bin_dir
222
- return pretty_path(File.join(right_link_home_dir, 'right_link', 'scripts', 'windows'))
222
+ return pretty_path(File.join(right_link_home_dir, 'bin'))
223
223
  end
224
224
 
225
225
  def sandbox_dir
@@ -781,41 +781,49 @@ module RightScale
781
781
  end
782
782
 
783
783
  # Handle response to a request
784
+ # Acknowledge response after delivering it
784
785
  # Only to be called from primary thread
785
786
  #
786
787
  # === Parameters
787
788
  # response(Result):: Packet received as result of request
789
+ # header(AMQP::Frame::Header|nil):: Request header containing ack control
788
790
  #
789
791
  # === Return
790
792
  # true:: Always return true
791
- def handle_response(response)
792
- token = response.token
793
- if response.is_a?(Result)
794
- if result = OperationResult.from_results(response)
795
- if result.non_delivery?
796
- @non_delivery_stats.update(result.content.nil? ? "nil" : result.content.inspect)
797
- elsif result.error?
798
- @result_error_stats.update(result.content.nil? ? "nil" : result.content.inspect)
793
+ def handle_response(response, header = nil)
794
+ begin
795
+ ack_deferred = false
796
+ token = response.token
797
+ if response.is_a?(Result)
798
+ if result = OperationResult.from_results(response)
799
+ if result.non_delivery?
800
+ @non_delivery_stats.update(result.content.nil? ? "nil" : result.content.inspect)
801
+ elsif result.error?
802
+ @result_error_stats.update(result.content.nil? ? "nil" : result.content.inspect)
803
+ end
804
+ @result_stats.update(result.status)
805
+ else
806
+ @result_stats.update(response.results.nil? ? "nil" : response.results)
799
807
  end
800
- @result_stats.update(result.status)
801
- else
802
- @result_stats.update(response.results.nil? ? "nil" : response.results)
803
- end
804
808
 
805
- if handler = @pending_requests[token]
806
- if result && result.non_delivery? && handler.kind == :send_retryable_request &&
807
- [OperationResult::TARGET_NOT_CONNECTED, OperationResult::TTL_EXPIRATION].include?(result.content)
808
- # Log and ignore so that timeout retry mechanism continues
809
- # Leave purging of associated request until final response, i.e., success response or retry timeout
809
+ if handler = @pending_requests[token]
810
+ if result && result.non_delivery? && handler.kind == :send_retryable_request &&
811
+ [OperationResult::TARGET_NOT_CONNECTED, OperationResult::TTL_EXPIRATION].include?(result.content)
812
+ # Log and ignore so that timeout retry mechanism continues
813
+ # Leave purging of associated request until final response, i.e., success response or retry timeout
814
+ Log.info("Non-delivery of <#{token}> because #{result.content}")
815
+ else
816
+ ack_deferred = true
817
+ deliver(response, handler, header)
818
+ end
819
+ elsif result && result.non_delivery?
810
820
  Log.info("Non-delivery of <#{token}> because #{result.content}")
811
821
  else
812
- deliver(response, handler)
822
+ Log.debug("No pending request for response #{response.to_s([])}")
813
823
  end
814
- elsif result && result.non_delivery?
815
- Log.info("Non-delivery of <#{token}> because #{result.content}")
816
- else
817
- Log.debug("No pending request for response #{response.to_s([])}")
818
824
  end
825
+ ensure
826
+ header.ack unless ack_deferred || header.nil?
819
827
  end
820
828
  true
821
829
  end
@@ -1130,13 +1138,7 @@ module RightScale
1130
1138
  def publish_with_timeout_retry(request, parent, count = 0, multiplier = 1, elapsed = 0, broker_ids = nil)
1131
1139
  published_broker_ids = publish(request, broker_ids)
1132
1140
 
1133
- if published_broker_ids.empty?
1134
- # Could not publish request to any brokers, so respond with non-delivery
1135
- # to allow requester, e.g., IdempotentRequest, to retry
1136
- result = OperationResult.non_delivery("request send failed")
1137
- @non_delivery_stats.update(result.content)
1138
- handle_response(Result.new(request.token, request.reply_to, result, @identity))
1139
- elsif @retry_interval && @retry_timeout && parent
1141
+ if @retry_interval && @retry_timeout && parent && !published_broker_ids.empty?
1140
1142
  interval = [(@retry_interval * multiplier) + (@request_stats.avg_duration || 0), @retry_timeout - elapsed].min
1141
1143
  EM.add_timer(interval) do
1142
1144
  begin
@@ -1178,26 +1180,40 @@ module RightScale
1178
1180
  # === Parameters
1179
1181
  # response(Result):: Packet received as result of request
1180
1182
  # handler(Hash):: Associated request handler
1183
+ # header(AMQP::Frame::Header|nil):: Request header containing ack control
1181
1184
  #
1182
1185
  # === Return
1183
1186
  # true:: Always return true
1184
- def deliver(response, handler)
1185
- @request_stats.finish(handler.receive_time, response.token)
1187
+ def deliver(response, handler, header)
1188
+ begin
1189
+ ack_deferred = false
1190
+ @request_stats.finish(handler.receive_time, response.token)
1186
1191
 
1187
- @pending_requests.delete(response.token) if PendingRequests::REQUEST_KINDS.include?(handler.kind)
1188
- if parent = handler.retry_parent
1189
- @pending_requests.reject! { |k, v| k == parent || v.retry_parent == parent }
1190
- end
1192
+ @pending_requests.delete(response.token) if PendingRequests::REQUEST_KINDS.include?(handler.kind)
1193
+ if parent = handler.retry_parent
1194
+ @pending_requests.reject! { |k, v| k == parent || v.retry_parent == parent }
1195
+ end
1191
1196
 
1192
- if handler.response_handler
1193
- EM.__send__(@single_threaded ? :next_tick : :defer) do
1197
+ if handler.response_handler
1194
1198
  begin
1195
- handler.response_handler.call(response)
1196
- rescue Exception => e
1197
- Log.error("Failed processing response #{response.to_s([])}", e, :trace)
1198
- @exception_stats.track("response", e, response)
1199
+ ack_deferred = true
1200
+ EM.__send__(@single_threaded ? :next_tick : :defer) do
1201
+ begin
1202
+ handler.response_handler.call(response)
1203
+ rescue Exception => e
1204
+ Log.error("Failed processing response #{response.to_s([])}", e, :trace)
1205
+ @exception_stats.track("response", e, response)
1206
+ ensure
1207
+ header.ack if header
1208
+ end
1209
+ end
1210
+ rescue Exception
1211
+ header.ack if header
1212
+ raise
1199
1213
  end
1200
1214
  end
1215
+ ensure
1216
+ header.ack unless ack_deferred || header.nil?
1201
1217
  end
1202
1218
  true
1203
1219
  end
data/right_agent.gemspec CHANGED
@@ -24,8 +24,8 @@ require 'rubygems'
24
24
 
25
25
  Gem::Specification.new do |spec|
26
26
  spec.name = 'right_agent'
27
- spec.version = '0.10.13'
28
- spec.date = '2013-02-07'
27
+ spec.version = '0.13.5'
28
+ spec.date = '2012-10-03'
29
29
  spec.authors = ['Lee Kirchhoff', 'Raphael Simon', 'Tony Spataro']
30
30
  spec.email = 'lee@rightscale.com'
31
31
  spec.homepage = 'https://github.com/rightscale/right_agent'
@@ -37,8 +37,8 @@ Gem::Specification.new do |spec|
37
37
  spec.required_ruby_version = '>= 1.8.7'
38
38
  spec.require_path = 'lib'
39
39
 
40
- spec.add_dependency('right_support', ['>= 1.3', '< 3.0'])
41
- spec.add_dependency('right_amqp', '~> 0.3')
40
+ spec.add_dependency('right_support', ['>= 2.4.1', '< 3.0'])
41
+ spec.add_dependency('right_amqp', '~> 0.4')
42
42
  spec.add_dependency('json', ['~> 1.4'])
43
43
  spec.add_dependency('eventmachine', '~> 0.12.10')
44
44
  spec.add_dependency('right_popen', '~> 1.0.11')
@@ -26,14 +26,12 @@ describe RightScale::ActorRegistry do
26
26
 
27
27
  class ::WebDocumentImporter
28
28
  include RightScale::Actor
29
- expose :import, :cancel
29
+ expose_non_idempotent :import, :cancel
30
+ expose_idempotent :special
30
31
 
31
- def import
32
- 1
33
- end
34
- def cancel
35
- 0
36
- end
32
+ def import; 1 end
33
+ def cancel; 0 end
34
+ def special; 2 end
37
35
  end
38
36
 
39
37
  module ::Actors
@@ -53,7 +51,8 @@ describe RightScale::ActorRegistry do
53
51
  it "should know about all services" do
54
52
  @registry.register(WebDocumentImporter.new, nil)
55
53
  @registry.register(Actors::ComedyActor.new, nil)
56
- @registry.services.sort.should == ["/actors/comedy_actor/fun_tricks", "/web_document_importer/cancel", "/web_document_importer/import"]
54
+ @registry.services.sort.should == ["/actors/comedy_actor/fun_tricks", "/web_document_importer/cancel",
55
+ "/web_document_importer/import", "/web_document_importer/special"]
57
56
  end
58
57
 
59
58
  it "should not register anything except RightScale::Actor" do
data/spec/actor_spec.rb CHANGED
@@ -25,17 +25,14 @@ require File.expand_path(File.join(File.dirname(__FILE__), 'spec_helper'))
25
25
  describe RightScale::Actor do
26
26
  class ::WebDocumentImporter
27
27
  include RightScale::Actor
28
- expose :import, :cancel
28
+ expose_non_idempotent :import, :cancel
29
+ expose_idempotent :special
29
30
 
30
- def import
31
- 1
32
- end
33
- def cancel
34
- 0
35
- end
36
- def continue
37
- 1
38
- end
31
+ def import; 1 end
32
+ def cancel; 0 end
33
+ def continue; 1 end
34
+ def special; 2 end
35
+ def more_special; 3 end
39
36
  end
40
37
 
41
38
  module ::Actors
@@ -52,35 +49,88 @@ describe RightScale::Actor do
52
49
  include RightScale::Actor
53
50
  expose :non_existing
54
51
  end
55
-
56
- describe ".expose" do
52
+
53
+ before :each do
54
+ @log = flexmock(RightScale::Log)
55
+ @log.should_receive(:warning).by_default.and_return { |m| raise RightScale::Log.format(*m) }
56
+ @log.should_receive(:error).by_default.and_return { |m| raise RightScale::Log.format(*m) }
57
+ end
58
+
59
+ describe "expose" do
60
+
57
61
  before :each do
58
62
  @exposed = WebDocumentImporter.instance_variable_get(:@exposed).dup
59
63
  end
60
-
64
+
61
65
  after :each do
62
66
  WebDocumentImporter.instance_variable_set(:@exposed, @exposed)
63
67
  end
64
-
65
-
66
- it "should single expose method only once" do
67
- 3.times { WebDocumentImporter.expose(:continue) }
68
- WebDocumentImporter.provides_for("webfiles").should == ["/webfiles/import", "/webfiles/cancel", "/webfiles/continue"]
68
+
69
+ context "idempotent" do
70
+
71
+ it "exposes as idempotent" do
72
+ WebDocumentImporter.expose_idempotent(:more_special)
73
+ WebDocumentImporter.provides_for("webfiles").sort.should == [
74
+ "/webfiles/cancel", "/webfiles/import", "/webfiles/more_special", "/webfiles/special"]
75
+ WebDocumentImporter.idempotent?(:special).should be_true
76
+ WebDocumentImporter.idempotent?(:more_special).should be_true
77
+ end
78
+
79
+ it "treats already exposed non-idempotent method as non-idempotent" do
80
+ @log.should_receive(:warning).with(/Method cancel declared both idempotent and non-idempotent/).once
81
+ WebDocumentImporter.expose_idempotent(:cancel)
82
+ WebDocumentImporter.provides_for("webfiles").sort.should == [
83
+ "/webfiles/cancel", "/webfiles/import", "/webfiles/special"]
84
+ WebDocumentImporter.idempotent?(:cancel).should be_false
85
+ end
86
+
69
87
  end
88
+
89
+ context "non_idempotent" do
90
+
91
+ it "exposes as non-idempotent" do
92
+ WebDocumentImporter.expose_non_idempotent(:continue)
93
+ WebDocumentImporter.provides_for("webfiles").sort.should == [
94
+ "/webfiles/cancel", "/webfiles/continue", "/webfiles/import", "/webfiles/special"]
95
+ WebDocumentImporter.idempotent?(:import).should be_false
96
+ WebDocumentImporter.idempotent?(:cancel).should be_false
97
+ WebDocumentImporter.idempotent?(:continue).should be_false
98
+ end
99
+
100
+ it "treats already exposed idempotent method as non-idempotent" do
101
+ @log.should_receive(:warning).with(/Method special declared both idempotent and non-idempotent/).once
102
+ WebDocumentImporter.expose_non_idempotent(:special)
103
+ WebDocumentImporter.provides_for("webfiles").sort.should == [
104
+ "/webfiles/cancel", "/webfiles/import", "/webfiles/special"]
105
+ WebDocumentImporter.idempotent?(:special).should be_false
106
+ end
107
+
108
+ it "defaults expose method to declare non_idempotent" do
109
+ WebDocumentImporter.expose(:continue)
110
+ WebDocumentImporter.provides_for("webfiles").sort.should == [
111
+ "/webfiles/cancel", "/webfiles/continue", "/webfiles/import", "/webfiles/special"]
112
+ WebDocumentImporter.idempotent?(:continue).should be_false
113
+ end
114
+
115
+ end
116
+
70
117
  end
71
-
72
- describe ".default_prefix" do
118
+
119
+ describe "default_prefix" do
120
+
73
121
  it "is calculated as default prefix as const path of class name" do
74
122
  Actors::ComedyActor.default_prefix.should == "actors/comedy_actor"
75
123
  WebDocumentImporter.default_prefix.should == "web_document_importer"
76
124
  end
125
+
77
126
  end
78
127
 
79
- describe ".provides_for(prefix)" do
128
+ describe "provides_for" do
129
+
80
130
  before :each do
81
131
  @provides = Actors::ComedyActor.provides_for("money")
82
132
  end
83
-
133
+
84
134
  it "returns an array" do
85
135
  @provides.should be_kind_of(Array)
86
136
  end
@@ -90,10 +140,23 @@ describe RightScale::Actor do
90
140
  wdi_provides = WebDocumentImporter.provides_for("webfiles")
91
141
  wdi_provides.should include("/webfiles/import")
92
142
  wdi_provides.should include("/webfiles/cancel")
143
+ wdi_provides.should include("/webfiles/special")
93
144
  end
94
-
95
- it "should not include methods not existing in the actor class" do
145
+
146
+ it "excludes methods not existing in the actor class" do
147
+ @log.should_receive(:warning).with("Exposing non-existing method non_existing in actor money").once
96
148
  Actors::InvalidActor.provides_for("money").should_not include("/money/non_existing")
97
149
  end
150
+
98
151
  end
152
+
153
+ describe "idempotent?" do
154
+
155
+ it "returns whether idempotent" do
156
+ WebDocumentImporter.idempotent?(:import).should be_false
157
+ WebDocumentImporter.idempotent?(:special).should be_true
158
+ end
159
+
160
+ end
161
+
99
162
  end