right_agent 0.10.13 → 0.13.5

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