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.
- data/lib/right_agent.rb +2 -0
- data/lib/right_agent/actor.rb +45 -10
- data/lib/right_agent/actor_registry.rb +5 -5
- data/lib/right_agent/actors/agent_manager.rb +4 -4
- data/lib/right_agent/agent.rb +97 -37
- data/lib/right_agent/agent_tag_manager.rb +1 -2
- data/lib/right_agent/command/command_io.rb +1 -3
- data/lib/right_agent/command/command_runner.rb +9 -3
- data/lib/right_agent/dispatched_cache.rb +110 -0
- data/lib/right_agent/dispatcher.rb +119 -180
- data/lib/right_agent/history.rb +136 -0
- data/lib/right_agent/log.rb +6 -3
- data/lib/right_agent/monkey_patches/ruby_patch.rb +0 -1
- data/lib/right_agent/pid_file.rb +1 -1
- data/lib/right_agent/platform.rb +2 -2
- data/lib/right_agent/platform/linux.rb +8 -1
- data/lib/right_agent/platform/windows.rb +1 -1
- data/lib/right_agent/sender.rb +57 -41
- data/right_agent.gemspec +4 -4
- data/spec/actor_registry_spec.rb +7 -8
- data/spec/actor_spec.rb +87 -24
- data/spec/agent_spec.rb +107 -8
- data/spec/command/command_runner_spec.rb +12 -1
- data/spec/dispatched_cache_spec.rb +142 -0
- data/spec/dispatcher_spec.rb +110 -129
- data/spec/history_spec.rb +234 -0
- data/spec/idempotent_request_spec.rb +1 -1
- data/spec/log_spec.rb +15 -0
- data/spec/operation_result_spec.rb +4 -2
- data/spec/platform/darwin_spec.rb +13 -0
- data/spec/platform/linux_spec.rb +38 -0
- data/spec/platform/platform_spec.rb +46 -51
- data/spec/platform/windows_spec.rb +13 -0
- data/spec/sender_spec.rb +81 -38
- metadata +12 -9
- data/lib/right_agent/monkey_patches/ruby_patch/singleton_patch.rb +0 -45
- data/spec/platform/darwin.rb +0 -11
- data/spec/platform/linux.rb +0 -23
- 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
|
data/lib/right_agent/log.rb
CHANGED
@@ -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
|
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
|
data/lib/right_agent/pid_file.rb
CHANGED
data/lib/right_agent/platform.rb
CHANGED
@@ -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
|
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 =
|
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, '
|
222
|
+
return pretty_path(File.join(right_link_home_dir, 'bin'))
|
223
223
|
end
|
224
224
|
|
225
225
|
def sandbox_dir
|
data/lib/right_agent/sender.rb
CHANGED
@@ -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
|
-
|
793
|
-
|
794
|
-
|
795
|
-
|
796
|
-
|
797
|
-
|
798
|
-
|
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
|
-
|
806
|
-
|
807
|
-
|
808
|
-
|
809
|
-
|
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
|
-
|
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
|
-
|
1187
|
+
def deliver(response, handler, header)
|
1188
|
+
begin
|
1189
|
+
ack_deferred = false
|
1190
|
+
@request_stats.finish(handler.receive_time, response.token)
|
1186
1191
|
|
1187
|
-
|
1188
|
-
|
1189
|
-
|
1190
|
-
|
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
|
-
|
1193
|
-
EM.__send__(@single_threaded ? :next_tick : :defer) do
|
1197
|
+
if handler.response_handler
|
1194
1198
|
begin
|
1195
|
-
|
1196
|
-
|
1197
|
-
|
1198
|
-
|
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.
|
28
|
-
spec.date = '
|
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
|
41
|
-
spec.add_dependency('right_amqp', '~> 0.
|
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')
|
data/spec/actor_registry_spec.rb
CHANGED
@@ -26,14 +26,12 @@ describe RightScale::ActorRegistry do
|
|
26
26
|
|
27
27
|
class ::WebDocumentImporter
|
28
28
|
include RightScale::Actor
|
29
|
-
|
29
|
+
expose_non_idempotent :import, :cancel
|
30
|
+
expose_idempotent :special
|
30
31
|
|
31
|
-
def import
|
32
|
-
|
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",
|
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
|
-
|
28
|
+
expose_non_idempotent :import, :cancel
|
29
|
+
expose_idempotent :special
|
29
30
|
|
30
|
-
def import
|
31
|
-
|
32
|
-
end
|
33
|
-
def
|
34
|
-
|
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
|
-
|
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
|
-
|
67
|
-
|
68
|
-
|
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 "
|
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 "
|
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 "
|
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
|