rereplay 0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *~
2
+ .bundle
3
+ *.gem
4
+ *.rbc
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,3 @@
1
+ Dir[File.join(File.dirname(__FILE__), "monitors/*.rb")].each do |monitor|
2
+ require monitor
3
+ 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
@@ -0,0 +1,3 @@
1
+ module ReReplay
2
+ VERSION = "0.1"
3
+ end
data/lib/rereplay.rb ADDED
@@ -0,0 +1,5 @@
1
+ require 'ostruct'
2
+ require 'eventmachine'
3
+ require 'em-http'
4
+
5
+ require 'rereplay/runner'
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
@@ -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
+