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