rereplay 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +34 -0
- data/LICENSE +19 -0
- data/README.md +40 -0
- data/design +22 -0
- data/lib/rereplay/monitors/delay_monitor.rb +13 -0
- data/lib/rereplay/monitors/memory_monitor.rb +16 -0
- data/lib/rereplay/monitors/request_time_monitor.rb +13 -0
- data/lib/rereplay/monitors/timeout_failer.rb +17 -0
- data/lib/rereplay/monitors/verbose_monitor.rb +12 -0
- data/lib/rereplay/monitors.rb +3 -0
- data/lib/rereplay/runner.rb +200 -0
- data/lib/rereplay/version.rb +3 -0
- data/lib/rereplay.rb +5 -0
- data/rereplay.gemspec +28 -0
- data/spec/advanced.rb +42 -0
- data/spec/basic.rb +38 -0
- data/spec/monitor.rb +56 -0
- data/spec/profile.rb +90 -0
- data/spec/spec_custom_matchers.rb +35 -0
- data/spec/spec_helper.rb +50 -0
- metadata +202 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
|
3
|
+
gem "eventmachine", "~>0.12.10"
|
4
|
+
gem "em-http-request", "~>0.2.14"
|
5
|
+
|
6
|
+
group :test do
|
7
|
+
gem "rspec", "~>2.0"
|
8
|
+
gem "webmock", "~>1.4.0"
|
9
|
+
gem "activesupport", "~>3.0.1"
|
10
|
+
# gem "autotest"
|
11
|
+
# gem "test_notifier"
|
12
|
+
end
|
13
|
+
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
activesupport (3.0.1)
|
5
|
+
addressable (2.2.2)
|
6
|
+
crack (0.1.8)
|
7
|
+
diff-lcs (1.1.2)
|
8
|
+
em-http-request (0.2.14)
|
9
|
+
addressable (>= 2.0.0)
|
10
|
+
eventmachine (>= 0.12.9)
|
11
|
+
eventmachine (0.12.10)
|
12
|
+
rspec (2.0.1)
|
13
|
+
rspec-core (~> 2.0.1)
|
14
|
+
rspec-expectations (~> 2.0.1)
|
15
|
+
rspec-mocks (~> 2.0.1)
|
16
|
+
rspec-core (2.0.1)
|
17
|
+
rspec-expectations (2.0.1)
|
18
|
+
diff-lcs (>= 1.1.2)
|
19
|
+
rspec-mocks (2.0.1)
|
20
|
+
rspec-core (~> 2.0.1)
|
21
|
+
rspec-expectations (~> 2.0.1)
|
22
|
+
webmock (1.4.0)
|
23
|
+
addressable (>= 2.2.2)
|
24
|
+
crack (>= 0.1.7)
|
25
|
+
|
26
|
+
PLATFORMS
|
27
|
+
ruby
|
28
|
+
|
29
|
+
DEPENDENCIES
|
30
|
+
activesupport (~> 3.0.1)
|
31
|
+
em-http-request (~> 0.2.14)
|
32
|
+
eventmachine (~> 0.12.10)
|
33
|
+
rspec (~> 2.0)
|
34
|
+
webmock (~> 1.4.0)
|
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2010 Max Aller <nanodeath@gmail.com>
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# ReReplay
|
2
|
+
|
3
|
+
ReReplay is for replaying production traffic (or any scripted traffic pattern, for that matter). You simply input a list of URLs that you want ReReplay to hit, and their associated times to make the request, and run it.
|
4
|
+
|
5
|
+
There are a couple other main features as well. You can provide Request Monitors to ReReplay -- these are executed before and/or after every request. There are also Periodic Monitors, which execute on regular intervals (for monitoring memory usage, or something). Lastly, you can provide a rampup strategy as well. For example, you could start out making requests at the "regular" rate, and by the end of the run be making requests at double the rate presribed in the original input.
|
6
|
+
|
7
|
+
# Examples
|
8
|
+
|
9
|
+
(It's assumed you've already require'd "rereplay")
|
10
|
+
|
11
|
+
## Simple
|
12
|
+
input = [
|
13
|
+
[0, :get, "http://www.google.com/"],
|
14
|
+
[0.5, :get, "http://www.microsoft.com/"],
|
15
|
+
[0.9, :get, "http://www.amazon.com/"]
|
16
|
+
]
|
17
|
+
r = ReReplay.new(input)
|
18
|
+
r.run
|
19
|
+
# and done!
|
20
|
+
|
21
|
+
Of course, this doesn't actually track any output, so...let's monitor the request time using the request_time_monitor:
|
22
|
+
|
23
|
+
## Request Monitor
|
24
|
+
require "rereplay/monitors/request_time_monitor"
|
25
|
+
input = [same as in Simple]
|
26
|
+
mon = RequestTimeMonitor.new
|
27
|
+
r = ReReplay.new(input)
|
28
|
+
r.request_monitors << mon
|
29
|
+
r.run
|
30
|
+
puts mon.results.inspect
|
31
|
+
|
32
|
+
This will print out the results from the RequestTimeMonitor instance, which includes the url, the duration of the request, and its scheduled start time.
|
33
|
+
|
34
|
+
## Specs
|
35
|
+
|
36
|
+
Specs have more examples and probably more up-to-date, too. They're fairly simple -- start with basic.rb.
|
37
|
+
|
38
|
+
# License
|
39
|
+
|
40
|
+
Licensed under the permissive MIT license, provided in the LICENSE file.
|
data/design
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
First, we have to acquire our initial input. Anything that implements #readlines (and returns an array of strings) can be used. Note, this should be provided by Rereplay -- can either be a stream via ARGF or a filename.
|
2
|
+
|
3
|
+
Next we need a processor to turn our input into an array of arrays: each array element must match this pattern: [seconds offset (float), method (lowercase symbol), url (string), [optional: headers (hash)], (optional: post data, string)]
|
4
|
+
|
5
|
+
Next, we can filter this input with 1..n filters. Each filter must have a #filter method that takes an array and return a new array, which will be used instead; nil, if the string is to be removed; or true if the original input should be used.
|
6
|
+
|
7
|
+
After all of the filters have run, we will have our input. At this point, a profile is consumed that determines some runtime behavior. Keys include:
|
8
|
+
- time_for_setup: integer (seconds) (default 5s)
|
9
|
+
- timer_granularity: integer (milliseconds) (default 50ms)
|
10
|
+
- run_for: integer (minutes) (default 5m)
|
11
|
+
- when_input_consumed: enum (one of: loop, stop:default)
|
12
|
+
- monitors_enabled: array (list of monitor classes) default:[]
|
13
|
+
|
14
|
+
RequestMonitors are ruby classes that implement this interface:
|
15
|
+
- [#start(request:{url, scheduled_start, actual_start, index})=>nil] optional
|
16
|
+
- [#finish(request:{url, scheduled_start, actual_start, finish, index})=>nil] optional
|
17
|
+
- [#results=>whatever] optional
|
18
|
+
|
19
|
+
Periodic Monitors are also special ruby classes that provide some additional statistics that can be used. They must implement this interface:
|
20
|
+
- #tick(time:float)=>nil, returns nothing (executed every fixed interval). time is seconds since start.
|
21
|
+
- [#results()=>[[]], returns an array of arrays, where the first element is a time and the second is a string.] optional
|
22
|
+
- [#interval()=>time:integer], optional, returns the interval at which this monitor should be run, in seconds, default 5
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# Simple request monitor that tracks the delays of requests
|
2
|
+
module ReReplay
|
3
|
+
class DelayMonitor
|
4
|
+
attr_reader :results
|
5
|
+
def initialize
|
6
|
+
@results = []
|
7
|
+
end
|
8
|
+
|
9
|
+
def start(request)
|
10
|
+
@results[request.index] = [request.url, request.actual_start - request.scheduled_start]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# Simple periodic monitor that tracks ongoing memory usage
|
2
|
+
module ReReplay
|
3
|
+
class MemoryMonitor
|
4
|
+
attr_reader :results
|
5
|
+
attr_accessor :interval
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@results = []
|
9
|
+
end
|
10
|
+
def tick(time)
|
11
|
+
# http://laurelfan.com/2008/1/15/ruby-memory-usage
|
12
|
+
memory_usage = `ps -o rss= -p #{Process.pid}`.to_i
|
13
|
+
@results << [time, memory_usage]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# Simple request monitor that tracks how long requests take
|
2
|
+
module ReReplay
|
3
|
+
class RequestTimeMonitor
|
4
|
+
attr_reader :results
|
5
|
+
def initialize
|
6
|
+
@results = []
|
7
|
+
end
|
8
|
+
|
9
|
+
def finish(request)
|
10
|
+
@results[request.index] = [request.url, request.finish - request.actual_start, request.scheduled_start, request.actual_start]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module ReReplay
|
2
|
+
class TimeoutFailer
|
3
|
+
def initialize(max_timeouts=1)
|
4
|
+
@max_timeouts = max_timeouts
|
5
|
+
@timeouts = 0
|
6
|
+
end
|
7
|
+
|
8
|
+
def finish(request)
|
9
|
+
if(request.status == :timeout)
|
10
|
+
@timeouts += 1
|
11
|
+
end
|
12
|
+
if(@timeouts >= @max_timeouts)
|
13
|
+
raise "TimeoutFailer triggered because timeout limit #{@max_timeouts} was reached"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# Prints out start and stop times of requests
|
2
|
+
module ReReplay
|
3
|
+
class VerboseMonitor
|
4
|
+
def start(request)
|
5
|
+
puts "started request #{request.index}:(#{request.url}) at #{request.actual_start}"
|
6
|
+
end
|
7
|
+
|
8
|
+
def finish(request)
|
9
|
+
puts " - finished request #{request.index}, status #{request.status}"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,200 @@
|
|
1
|
+
module ReReplay
|
2
|
+
class Runner
|
3
|
+
attr_accessor :periodic_monitors
|
4
|
+
attr_accessor :request_monitors
|
5
|
+
|
6
|
+
def initialize(input=nil)
|
7
|
+
if(!input.nil?)
|
8
|
+
self.input = input
|
9
|
+
end
|
10
|
+
@periodic_monitors = []
|
11
|
+
@request_monitors = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def input=(input)
|
15
|
+
if(input.is_a? Array)
|
16
|
+
@input = input
|
17
|
+
elsif(input.respond_to? :readlines)
|
18
|
+
@input = input.readlines
|
19
|
+
elsif(input.respond_to? :split)
|
20
|
+
@input = input.split("\n").map do |i|
|
21
|
+
i = i.strip.split(",").map {|j| j.strip}
|
22
|
+
i[0] = i[0].to_f
|
23
|
+
i[1] = i[1].to_sym
|
24
|
+
i
|
25
|
+
end
|
26
|
+
else
|
27
|
+
raise "Invalid input, expected Array, #readlines, or #split"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def validate_input
|
32
|
+
if(@input.nil? || @input.empty?)
|
33
|
+
raise ArgumentError, "Nothing to process (input was empty)"
|
34
|
+
end
|
35
|
+
valid_methods = [:get, :head]
|
36
|
+
@input.each_with_index do |a, i|
|
37
|
+
if(!a[0].is_a? Numeric)
|
38
|
+
raise ArgumentError, "Expected element at index 0 of input #{i+1} to be Numeric; was #{a[0]}"
|
39
|
+
end
|
40
|
+
if(!a[1].is_a?(Symbol) || !valid_methods.include?(a[1]))
|
41
|
+
raise ArgumentError, "Expected element at index 1 of input #{i+1} to be a symbol in #{valid_methods.inspect}; was #{a[1].inspect}"
|
42
|
+
end
|
43
|
+
if(!a[2].is_a? String)
|
44
|
+
raise ArgumentError, "Expected element at index 2 of input #{i+1} to be a String; was #{a[2]}"
|
45
|
+
end
|
46
|
+
if(!a[3].nil? && !a[3].is_a?(Hash))
|
47
|
+
raise ArgumentError, "Expected element at index 3 of input #{i+1} to be nil or a Hash; was #{a[3]}"
|
48
|
+
end
|
49
|
+
# TODO post data
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def run
|
54
|
+
validate_input
|
55
|
+
p = profile
|
56
|
+
done_count = 0
|
57
|
+
# request monitors with a start method
|
58
|
+
request_monitors_start = request_monitors.select {|mon| mon.respond_to? :start}
|
59
|
+
# request monitors with a finish method
|
60
|
+
request_monitors_finish = request_monitors.select {|mon| mon.respond_to? :finish}
|
61
|
+
EM::run do
|
62
|
+
EM.set_quantum(p[:timer_granularity])
|
63
|
+
start = Time.now
|
64
|
+
setup_time = p[:time_for_setup]
|
65
|
+
actual_start = start + setup_time
|
66
|
+
|
67
|
+
loop_count = 1
|
68
|
+
|
69
|
+
max_time = @input.max {|a,b| a[0] <=> b[0]}[0]
|
70
|
+
#avg_time = @input.inject(0){|memo, i| i[0] + memo}.to_f / @input.length
|
71
|
+
if(p[:when_input_consumed] == :loop)
|
72
|
+
if(max_time < p[:run_for])
|
73
|
+
loop_count = (p[:run_for].to_f / max_time).ceil
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
if(loop_count > 1)
|
78
|
+
# If we need to have multiple iterations of the data,
|
79
|
+
# we pad that on here
|
80
|
+
new_inputs = []
|
81
|
+
2.upto(loop_count) do |loop|
|
82
|
+
new_input = @input.map do |i|
|
83
|
+
new_i = i.dup
|
84
|
+
new_i[0] += max_time * (loop - 1)
|
85
|
+
new_i
|
86
|
+
end
|
87
|
+
new_inputs << new_input
|
88
|
+
end
|
89
|
+
new_inputs.each {|input| @input += input}
|
90
|
+
end
|
91
|
+
real_max_time = [max_time * loop_count, p[:run_for]].min
|
92
|
+
if(p[:rampup][0] != p[:rampup][1] || p[:rampup][0] != 1.0)
|
93
|
+
case p[:rampup_method]
|
94
|
+
when :linear
|
95
|
+
sr = 1.0 / p[:rampup][0]
|
96
|
+
fr = 1.0 / p[:rampup][1]
|
97
|
+
prev_time = 0
|
98
|
+
new_prev_time = 0
|
99
|
+
@input.map! do |a|
|
100
|
+
time = a[0].to_f
|
101
|
+
percent = time / real_max_time
|
102
|
+
fraction = sr + (fr - sr)*(time / real_max_time)
|
103
|
+
tmp = a[0]
|
104
|
+
a[0] = (time - prev_time)*fraction + new_prev_time
|
105
|
+
prev_time = tmp
|
106
|
+
new_prev_time = a[0]
|
107
|
+
a
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
total_urls = @input.length
|
112
|
+
|
113
|
+
requests = []
|
114
|
+
# pregenerate requests
|
115
|
+
@input.each do |a|
|
116
|
+
requests << EventMachine::HttpRequest.new(a[2])
|
117
|
+
end
|
118
|
+
|
119
|
+
@input.each_with_index do |a, i|
|
120
|
+
scheduled_start = a[0]
|
121
|
+
request = OpenStruct.new(:url => a[2], :scheduled_start => scheduled_start, :index => i, :http_method => a[1])
|
122
|
+
delay = actual_start + scheduled_start
|
123
|
+
if(delay < Time.now)
|
124
|
+
raise "Not enough time allotted for setup! Try increasing time_for_setup in your profile."
|
125
|
+
end
|
126
|
+
delay -= Time.now
|
127
|
+
EM::add_timer(delay) do
|
128
|
+
EM.defer do
|
129
|
+
begin
|
130
|
+
request.actual_start = Time.now - actual_start
|
131
|
+
http = requests[i].send(request.http_method, :timeout => p[:timeout])
|
132
|
+
request_monitors_start.each {|mon| mon.start(request)}
|
133
|
+
rescue => e
|
134
|
+
EM.next_tick do
|
135
|
+
raise e
|
136
|
+
end
|
137
|
+
end
|
138
|
+
callback = lambda {
|
139
|
+
time_finished = Time.now - actual_start
|
140
|
+
request.finish = time_finished
|
141
|
+
request.status = http.response_header.status
|
142
|
+
if(request.status == 0)
|
143
|
+
request.status = :timeout
|
144
|
+
end
|
145
|
+
begin
|
146
|
+
request_monitors_finish.each {|mon| mon.finish(request)}
|
147
|
+
rescue => e
|
148
|
+
EM.next_tick do
|
149
|
+
raise e
|
150
|
+
end
|
151
|
+
end
|
152
|
+
done_count += 1
|
153
|
+
if(done_count == total_urls && p[:when_input_consumed] == :stop)
|
154
|
+
EM.stop
|
155
|
+
end
|
156
|
+
}
|
157
|
+
http.errback { callback.call }
|
158
|
+
http.callback { callback.call }
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
periodic_monitors.each do |mon|
|
163
|
+
interval = mon.respond_to?(:interval) ? mon.interval : 5
|
164
|
+
EM::add_timer(actual_start - Time.now - interval) do
|
165
|
+
EM::add_periodic_timer(interval) do
|
166
|
+
mon.tick(Time.now - actual_start)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
run_for = actual_start + p[:run_for] - Time.now
|
171
|
+
EM::add_timer(run_for) do
|
172
|
+
#puts "run_for hit (#{run_for})"
|
173
|
+
EM.stop
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def profile=(new_profile)
|
179
|
+
@profile = {
|
180
|
+
:time_for_setup => 1,
|
181
|
+
:timer_granularity => 50,
|
182
|
+
:run_for => 5,
|
183
|
+
:when_input_consumed => :stop,
|
184
|
+
:timeout => 1,
|
185
|
+
:rampup => [1.0, 1.0],
|
186
|
+
:rampup_method => :linear
|
187
|
+
}
|
188
|
+
if(new_profile.is_a? Hash)
|
189
|
+
@profile.merge!(new_profile)
|
190
|
+
end
|
191
|
+
# TODO validate profile
|
192
|
+
end
|
193
|
+
def profile
|
194
|
+
if(@profile.nil?)
|
195
|
+
self.profile = {}
|
196
|
+
end
|
197
|
+
@profile
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
data/lib/rereplay.rb
ADDED
data/rereplay.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
lib = File.expand_path('../lib/', __FILE__)
|
2
|
+
$:.unshift lib unless $:.include?(lib)
|
3
|
+
|
4
|
+
require 'rereplay/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = "rereplay"
|
8
|
+
s.version = ReReplay::VERSION
|
9
|
+
s.platform = Gem::Platform::RUBY
|
10
|
+
s.authors = ["Max Aller"]
|
11
|
+
s.email = ["nanodeath@gmail.com"]
|
12
|
+
s.homepage = "http://github.com/nanodeath/ReReplay"
|
13
|
+
s.summary = %q{Replay your prod traffic}
|
14
|
+
s.description = %q{Replay or script traffic in order to track performance of your site over time.}
|
15
|
+
|
16
|
+
s.required_rubygems_version = ">= 1.3.6"
|
17
|
+
|
18
|
+
s.add_runtime_dependency "eventmachine", ">= 0.12.10", "< 0.13"
|
19
|
+
s.add_runtime_dependency "em-http-request", ">= 0.2.14", "< 0.3"
|
20
|
+
|
21
|
+
s.add_development_dependency "rspec", ">= 2.0", "< 3"
|
22
|
+
s.add_development_dependency "webmock", ">= 1.4.0", "< 1.5"
|
23
|
+
s.add_development_dependency "active_support", ">= 3.0.1", "< 3.1"
|
24
|
+
|
25
|
+
s.files = `git ls-files`.split("\n")
|
26
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
27
|
+
s.require_paths = ["lib"]
|
28
|
+
end
|
data/spec/advanced.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'spec/spec_helper'
|
2
|
+
|
3
|
+
describe ReReplay, "advanced functions" do
|
4
|
+
it "should support linear rampup" do
|
5
|
+
input = generate_input(10, :start_at_0 => true)
|
6
|
+
|
7
|
+
r = ReReplay::Runner.new(input)
|
8
|
+
profile = {
|
9
|
+
:rampup => [1.0, 2.0]
|
10
|
+
}
|
11
|
+
r.profile = profile
|
12
|
+
lambda { r.run }.should take_between(1.65.seconds).and(1.8.seconds)
|
13
|
+
validate_input(10)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should support linear rampup from <1 to >1" do
|
17
|
+
input = generate_input(10, :start_at_0 => true)
|
18
|
+
|
19
|
+
r = ReReplay::Runner.new(input)
|
20
|
+
profile = {
|
21
|
+
:rampup => [0.5, 2]
|
22
|
+
}
|
23
|
+
r.profile = profile
|
24
|
+
lambda { r.run }.should take_between(2.05.seconds).and(2.2.seconds)
|
25
|
+
validate_input(10)
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should play nicely with run_for and :loop" do
|
29
|
+
input = generate_input(10, :start_at_0 => true)
|
30
|
+
|
31
|
+
r = ReReplay::Runner.new(input)
|
32
|
+
profile = {
|
33
|
+
:rampup => [0.5, 2],
|
34
|
+
:run_for => 2,
|
35
|
+
:when_input_consumed => :loop
|
36
|
+
}
|
37
|
+
r.profile = profile
|
38
|
+
lambda { r.run }.should take_between(3.seconds).and(3.1.seconds)
|
39
|
+
validate_input(10, 2)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
data/spec/basic.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'spec/spec_helper'
|
2
|
+
|
3
|
+
describe ReReplay, "basic functions" do
|
4
|
+
it "should process input from array" do
|
5
|
+
urls = ["google", "microsoft", "amazon"].map {|u| "http://#{u}.com/"}
|
6
|
+
|
7
|
+
urls.each {|url| stub_request(:get, url)}
|
8
|
+
|
9
|
+
interval = 0.0
|
10
|
+
input = urls.map {|i| [interval += 0.1, :get, i]}
|
11
|
+
|
12
|
+
r = ReReplay::Runner.new(input)
|
13
|
+
lambda { r.run }.should take_between(1.second).and(2.seconds)
|
14
|
+
|
15
|
+
urls.each {|url| WebMock.should have_requested(:get, url)}
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should process input from string" do
|
19
|
+
input = <<EOF
|
20
|
+
0.1, get, http://www.google.com/
|
21
|
+
0.6, get, http://www.amazon.com/
|
22
|
+
EOF
|
23
|
+
|
24
|
+
stub_request(:get, "http://www.google.com/")
|
25
|
+
stub_request(:get, "http://www.amazon.com/")
|
26
|
+
|
27
|
+
r = ReReplay::Runner.new(input)
|
28
|
+
lambda { r.run }.should take_between(1.second).and(2.seconds)
|
29
|
+
|
30
|
+
WebMock.should have_requested(:get, "http://www.google.com/")
|
31
|
+
WebMock.should have_requested(:get, "http://www.amazon.com/")
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should throw exception on empty input" do
|
35
|
+
r = ReReplay::Runner.new
|
36
|
+
lambda { r.run }.should raise_error(ArgumentError, /input was empty/)
|
37
|
+
end
|
38
|
+
end
|
data/spec/monitor.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'spec/spec_helper'
|
2
|
+
|
3
|
+
describe ReReplay, "periodic monitors" do
|
4
|
+
it "should work" do
|
5
|
+
mem_monitor = ReReplay::MemoryMonitor.new
|
6
|
+
mem_monitor.interval = 0.2
|
7
|
+
|
8
|
+
input = generate_input(3, :interval => 0.25)
|
9
|
+
r = ReReplay::Runner.new(input)
|
10
|
+
r.periodic_monitors << mem_monitor
|
11
|
+
|
12
|
+
# the periodic monitor will start around `time_for_setup` (1 second)
|
13
|
+
# and run once every ~0.23s thereafter.
|
14
|
+
# Because the final request will finish around 1.75, this run four times
|
15
|
+
r.run
|
16
|
+
validate_input(3)
|
17
|
+
mem_monitor.results.length.should == 4
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe ReReplay, "request monitors" do
|
22
|
+
it "should work" do
|
23
|
+
req_mon = ReReplay::RequestTimeMonitor.new
|
24
|
+
delay_mon = ReReplay::DelayMonitor.new
|
25
|
+
|
26
|
+
input = generate_input(3, :interval => 0.25)
|
27
|
+
r = ReReplay::Runner.new(input)
|
28
|
+
r.request_monitors << req_mon << delay_mon
|
29
|
+
# the periodic monitor will start around `time_for_setup` (1 second)
|
30
|
+
# and run once every ~0.23s thereafter.
|
31
|
+
# Because the final request will finish around 1.75, this run four times
|
32
|
+
r.run
|
33
|
+
validate_input(3)
|
34
|
+
req_mon.results.length.should == 3
|
35
|
+
delay_mon.results.length.should == 3
|
36
|
+
end
|
37
|
+
|
38
|
+
describe ReReplay::VerboseMonitor do
|
39
|
+
it "should be verbose" do
|
40
|
+
input = generate_input(3, :interval => 0.25)
|
41
|
+
r = ReReplay::Runner.new(input)
|
42
|
+
r.request_monitors << ReReplay::VerboseMonitor.new
|
43
|
+
expected = Regexp.new(<<EOF, Regexp::MULTILINE)
|
44
|
+
started request 0:\\(http://google.com/\\) at [\\d\\.]+
|
45
|
+
- finished request 0, status 200
|
46
|
+
started request 1:\\(http://microsoft.com/\\) at [\\d\\.]+
|
47
|
+
- finished request 1, status 200
|
48
|
+
started request 2:\\(http://amazon.com/\\) at [\\d\\.]+
|
49
|
+
- finished request 2, status 200
|
50
|
+
EOF
|
51
|
+
capture_stdout { r.run }.should match(expected)
|
52
|
+
validate_input(3)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
data/spec/profile.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'spec/spec_helper'
|
2
|
+
|
3
|
+
describe ReReplay, "profile options" do
|
4
|
+
it "respects 'time_for_setup' parameter" do
|
5
|
+
input = generate_input(2)
|
6
|
+
|
7
|
+
r = ReReplay::Runner.new(input)
|
8
|
+
profile = {
|
9
|
+
:time_for_setup => 0.25
|
10
|
+
}
|
11
|
+
r.profile = profile
|
12
|
+
lambda { r.run }.should take_between(0.45.seconds).and(0.6.seconds)
|
13
|
+
validate_input(2)
|
14
|
+
end
|
15
|
+
it "respects 'timer_granularity' parameter" do
|
16
|
+
input = generate_input(2)
|
17
|
+
|
18
|
+
r = ReReplay::Runner.new(input)
|
19
|
+
profile = {
|
20
|
+
:timer_granularity => 1000
|
21
|
+
}
|
22
|
+
r.profile = profile
|
23
|
+
|
24
|
+
# normally this would finish at around 1.2 seconds, but with such a high
|
25
|
+
# timer resolution, it rounds up from 0.1 to 1
|
26
|
+
lambda { r.run }.should take_between(2.seconds).and(3.seconds)
|
27
|
+
validate_input(2)
|
28
|
+
end
|
29
|
+
it "respects 'run_for' parameter" do
|
30
|
+
input = generate_input(3, :interval => 1)
|
31
|
+
r = ReReplay::Runner.new(input)
|
32
|
+
profile = {
|
33
|
+
:run_for => 1
|
34
|
+
}
|
35
|
+
r.profile = profile
|
36
|
+
|
37
|
+
# normally this would run for the full 4 seconds, but with run_for fixed at 2,
|
38
|
+
# it will stop then
|
39
|
+
lambda { r.run }.should take_between(2.seconds).and(2.1.seconds)
|
40
|
+
validate_input(1)
|
41
|
+
end
|
42
|
+
|
43
|
+
it "respects 'when_input_consumed' parameter when :stop" do
|
44
|
+
input = generate_input(3, :interval => 0.5)
|
45
|
+
r = ReReplay::Runner.new(input)
|
46
|
+
profile = {
|
47
|
+
:run_for => 10,
|
48
|
+
:when_input_consumed => :stop
|
49
|
+
}
|
50
|
+
r.profile = profile
|
51
|
+
|
52
|
+
lambda { r.run }.should take_between(2.5.seconds).and(2.7.seconds)
|
53
|
+
validate_input(3)
|
54
|
+
end
|
55
|
+
|
56
|
+
it "respects 'when_input_consumed' parameter when :loop" do
|
57
|
+
input = generate_input(1, :interval => 0.5)
|
58
|
+
r = ReReplay::Runner.new(input)
|
59
|
+
profile = {
|
60
|
+
:run_for => 1.1,
|
61
|
+
:when_input_consumed => :loop
|
62
|
+
}
|
63
|
+
r.profile = profile
|
64
|
+
req_mon = ReReplay::RequestTimeMonitor.new
|
65
|
+
r.request_monitors << req_mon
|
66
|
+
|
67
|
+
# normally this would take 1.5 seconds with :stop, but we're forcing it to loop and take 2 seconds
|
68
|
+
lambda { r.run }.should take_between(2.seconds).and(2.2.seconds)
|
69
|
+
req_mon.results.length.should == 2
|
70
|
+
validate_input(1, 2)
|
71
|
+
end
|
72
|
+
|
73
|
+
# eh, this isn't that good a test because WebMock causes #to_timeout requests to timeout immediately
|
74
|
+
it "respects 'timeout' parameter" do
|
75
|
+
input = generate_input(1, :interval => 0.25, :timeout => true)
|
76
|
+
r = ReReplay::Runner.new(input)
|
77
|
+
profile = {
|
78
|
+
:timeout => 10,
|
79
|
+
:time_for_setup => 0.25
|
80
|
+
}
|
81
|
+
r.profile = profile
|
82
|
+
r.request_monitors << ReReplay::TimeoutFailer.new
|
83
|
+
|
84
|
+
# normally this would take 4 seconds with :stop, but after 2 seconds we hit the timeout
|
85
|
+
|
86
|
+
lambda { lambda { r.run }.should raise_error(StandardError, /TimeoutFailer/) }.should take_between(0.5.seconds).and(0.7.seconds)
|
87
|
+
validate_input(1)
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# Custom matchers
|
2
|
+
module CustomMatchers
|
3
|
+
class TimeTaken
|
4
|
+
def initialize(lower_seconds, upper_seconds=nil)
|
5
|
+
@lower_seconds = lower_seconds
|
6
|
+
@upper_seconds = upper_seconds
|
7
|
+
end
|
8
|
+
|
9
|
+
def matches?(given_proc)
|
10
|
+
range = @lower_seconds..@upper_seconds
|
11
|
+
start = Time.now
|
12
|
+
given_proc.call
|
13
|
+
fin = Time.now
|
14
|
+
@diff = fin - start
|
15
|
+
range.include?(@diff)
|
16
|
+
end
|
17
|
+
|
18
|
+
def and(upper_seconds)
|
19
|
+
@upper_seconds = upper_seconds
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
def failure_message_for_should
|
24
|
+
"expected block to take between #{@lower_seconds} and #{@upper_seconds} seconds, but took #{@diff} seconds"
|
25
|
+
end
|
26
|
+
|
27
|
+
def failure_message_for_should_not
|
28
|
+
"expected block not to take between #{@lower_seconds} and #{@upper_seconds} seconds, but took #{@diff} seconds"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def take_between(lower_seconds, upper_seconds=nil)
|
33
|
+
TimeTaken.new(lower_seconds, upper_seconds)
|
34
|
+
end
|
35
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "bundler"
|
3
|
+
Bundler.setup(:default, :test)
|
4
|
+
|
5
|
+
require "rereplay"
|
6
|
+
require "rereplay/monitors/memory_monitor"
|
7
|
+
require "rereplay/monitors/request_time_monitor"
|
8
|
+
require "rereplay/monitors/delay_monitor"
|
9
|
+
require "rereplay/monitors/verbose_monitor"
|
10
|
+
require "rereplay/monitors/timeout_failer"
|
11
|
+
|
12
|
+
require "rspec"
|
13
|
+
require "webmock/rspec"
|
14
|
+
require 'active_support/time'
|
15
|
+
require 'spec_custom_matchers'
|
16
|
+
|
17
|
+
RSpec.configure do |config|
|
18
|
+
config.include WebMock::API
|
19
|
+
config.include CustomMatchers
|
20
|
+
end
|
21
|
+
|
22
|
+
def generate_input(length, opts={})
|
23
|
+
interval = opts[:start] || 0.0
|
24
|
+
interval_increment = opts[:interval] || 0.1
|
25
|
+
urls = ["google", "microsoft", "amazon", "mint", "yahoo", "windowshop", "xbox", "samsung", "qwest", "comcast"][0...length].map {|u| "http://#{u}.com/"}
|
26
|
+
if(opts[:timeout])
|
27
|
+
urls.each {|url| stub_request(:get, url).to_timeout}
|
28
|
+
else
|
29
|
+
urls.each {|url| stub_request(:get, url)}
|
30
|
+
end
|
31
|
+
if(opts[:start_at_0])
|
32
|
+
interval -= interval_increment
|
33
|
+
end
|
34
|
+
input = urls.map {|i| [interval += interval_increment, :get, i]}
|
35
|
+
input
|
36
|
+
end
|
37
|
+
|
38
|
+
def validate_input(length, count=1)
|
39
|
+
urls = ["google", "microsoft", "amazon"][0...length].map {|u| "http://#{u}.com/"}
|
40
|
+
urls.each {|u| WebMock.should have_requested(:get, u).times(count)}
|
41
|
+
end
|
42
|
+
|
43
|
+
def capture_stdout
|
44
|
+
s = StringIO.new
|
45
|
+
$stdout = s
|
46
|
+
yield
|
47
|
+
s.string
|
48
|
+
ensure
|
49
|
+
$stdout = STDOUT
|
50
|
+
end
|
metadata
ADDED
@@ -0,0 +1,202 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rereplay
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 9
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
version: "0.1"
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Max Aller
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-10-25 00:00:00 -07:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: eventmachine
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 59
|
29
|
+
segments:
|
30
|
+
- 0
|
31
|
+
- 12
|
32
|
+
- 10
|
33
|
+
version: 0.12.10
|
34
|
+
- - <
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
hash: 17
|
37
|
+
segments:
|
38
|
+
- 0
|
39
|
+
- 13
|
40
|
+
version: "0.13"
|
41
|
+
type: :runtime
|
42
|
+
version_requirements: *id001
|
43
|
+
- !ruby/object:Gem::Dependency
|
44
|
+
name: em-http-request
|
45
|
+
prerelease: false
|
46
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
47
|
+
none: false
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
hash: 11
|
52
|
+
segments:
|
53
|
+
- 0
|
54
|
+
- 2
|
55
|
+
- 14
|
56
|
+
version: 0.2.14
|
57
|
+
- - <
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
hash: 13
|
60
|
+
segments:
|
61
|
+
- 0
|
62
|
+
- 3
|
63
|
+
version: "0.3"
|
64
|
+
type: :runtime
|
65
|
+
version_requirements: *id002
|
66
|
+
- !ruby/object:Gem::Dependency
|
67
|
+
name: rspec
|
68
|
+
prerelease: false
|
69
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
70
|
+
none: false
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
hash: 3
|
75
|
+
segments:
|
76
|
+
- 2
|
77
|
+
- 0
|
78
|
+
version: "2.0"
|
79
|
+
- - <
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
hash: 5
|
82
|
+
segments:
|
83
|
+
- 3
|
84
|
+
version: "3"
|
85
|
+
type: :development
|
86
|
+
version_requirements: *id003
|
87
|
+
- !ruby/object:Gem::Dependency
|
88
|
+
name: webmock
|
89
|
+
prerelease: false
|
90
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
91
|
+
none: false
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
hash: 7
|
96
|
+
segments:
|
97
|
+
- 1
|
98
|
+
- 4
|
99
|
+
- 0
|
100
|
+
version: 1.4.0
|
101
|
+
- - <
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
hash: 5
|
104
|
+
segments:
|
105
|
+
- 1
|
106
|
+
- 5
|
107
|
+
version: "1.5"
|
108
|
+
type: :development
|
109
|
+
version_requirements: *id004
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: active_support
|
112
|
+
prerelease: false
|
113
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
114
|
+
none: false
|
115
|
+
requirements:
|
116
|
+
- - ">="
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
hash: 5
|
119
|
+
segments:
|
120
|
+
- 3
|
121
|
+
- 0
|
122
|
+
- 1
|
123
|
+
version: 3.0.1
|
124
|
+
- - <
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
hash: 5
|
127
|
+
segments:
|
128
|
+
- 3
|
129
|
+
- 1
|
130
|
+
version: "3.1"
|
131
|
+
type: :development
|
132
|
+
version_requirements: *id005
|
133
|
+
description: Replay or script traffic in order to track performance of your site over time.
|
134
|
+
email:
|
135
|
+
- nanodeath@gmail.com
|
136
|
+
executables: []
|
137
|
+
|
138
|
+
extensions: []
|
139
|
+
|
140
|
+
extra_rdoc_files: []
|
141
|
+
|
142
|
+
files:
|
143
|
+
- .gitignore
|
144
|
+
- Gemfile
|
145
|
+
- Gemfile.lock
|
146
|
+
- LICENSE
|
147
|
+
- README.md
|
148
|
+
- design
|
149
|
+
- lib/rereplay.rb
|
150
|
+
- lib/rereplay/monitors.rb
|
151
|
+
- lib/rereplay/monitors/delay_monitor.rb
|
152
|
+
- lib/rereplay/monitors/memory_monitor.rb
|
153
|
+
- lib/rereplay/monitors/request_time_monitor.rb
|
154
|
+
- lib/rereplay/monitors/timeout_failer.rb
|
155
|
+
- lib/rereplay/monitors/verbose_monitor.rb
|
156
|
+
- lib/rereplay/runner.rb
|
157
|
+
- lib/rereplay/version.rb
|
158
|
+
- rereplay.gemspec
|
159
|
+
- spec/advanced.rb
|
160
|
+
- spec/basic.rb
|
161
|
+
- spec/monitor.rb
|
162
|
+
- spec/profile.rb
|
163
|
+
- spec/spec_custom_matchers.rb
|
164
|
+
- spec/spec_helper.rb
|
165
|
+
has_rdoc: true
|
166
|
+
homepage: http://github.com/nanodeath/ReReplay
|
167
|
+
licenses: []
|
168
|
+
|
169
|
+
post_install_message:
|
170
|
+
rdoc_options: []
|
171
|
+
|
172
|
+
require_paths:
|
173
|
+
- lib
|
174
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
175
|
+
none: false
|
176
|
+
requirements:
|
177
|
+
- - ">="
|
178
|
+
- !ruby/object:Gem::Version
|
179
|
+
hash: 3
|
180
|
+
segments:
|
181
|
+
- 0
|
182
|
+
version: "0"
|
183
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
184
|
+
none: false
|
185
|
+
requirements:
|
186
|
+
- - ">="
|
187
|
+
- !ruby/object:Gem::Version
|
188
|
+
hash: 23
|
189
|
+
segments:
|
190
|
+
- 1
|
191
|
+
- 3
|
192
|
+
- 6
|
193
|
+
version: 1.3.6
|
194
|
+
requirements: []
|
195
|
+
|
196
|
+
rubyforge_project:
|
197
|
+
rubygems_version: 1.3.7
|
198
|
+
signing_key:
|
199
|
+
specification_version: 3
|
200
|
+
summary: Replay your prod traffic
|
201
|
+
test_files: []
|
202
|
+
|