internethakai 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. data/CHANGES +53 -0
  2. data/README +177 -0
  3. data/bin/hakaigen +10 -0
  4. data/bin/internethakai +9 -0
  5. data/internethakai.gemspec +27 -0
  6. data/lib/internethakai.rb +26 -0
  7. data/lib/internethakai.rb.~1~ +25 -0
  8. data/lib/internethakai/action.rb +403 -0
  9. data/lib/internethakai/client_handler.rb +175 -0
  10. data/lib/internethakai/client_queue.rb +27 -0
  11. data/lib/internethakai/concurrency_manager.rb +333 -0
  12. data/lib/internethakai/concurrency_manager.rb.~1~ +331 -0
  13. data/lib/internethakai/core.rb +146 -0
  14. data/lib/internethakai/executor.rb +129 -0
  15. data/lib/internethakai/generator.rb +32 -0
  16. data/lib/internethakai/hakairev.rb +119 -0
  17. data/lib/internethakai/hakairev/executor.rb +43 -0
  18. data/lib/internethakai/hakairev/http_client.rb +362 -0
  19. data/lib/internethakai/hakairev/http_client.rb.~1~ +371 -0
  20. data/lib/internethakai/hakairev/monkey.rb +70 -0
  21. data/lib/internethakai/hakairev/revpipe.rb +39 -0
  22. data/lib/internethakai/hakairev/task.rb +39 -0
  23. data/lib/internethakai/hakairev/time_register.rb +116 -0
  24. data/lib/internethakai/hakairev/timer_factory.rb +38 -0
  25. data/lib/internethakai/http_client.rb +120 -0
  26. data/lib/internethakai/logger.rb +52 -0
  27. data/lib/internethakai/main.rb +90 -0
  28. data/lib/internethakai/reporter.rb +143 -0
  29. data/lib/internethakai/response.rb +65 -0
  30. data/lib/internethakai/scenario.rb +98 -0
  31. data/lib/internethakai/scenario.tmpl +58 -0
  32. data/lib/internethakai/scenario_builder.rb +183 -0
  33. data/lib/internethakai/scenario_handler.rb +91 -0
  34. data/lib/internethakai/time_register.rb +53 -0
  35. data/lib/internethakai/util.rb +130 -0
  36. metadata +134 -0
@@ -0,0 +1,143 @@
1
+ module InternetHakai
2
+ class Reporter < BaseHandler
3
+ UNIQUE_BY_THREAD = false
4
+ def run record
5
+ @config = BaseHandler.get_config
6
+ @logger = BaseHandler::get_handler(@config["logger"])
7
+ end
8
+ def set_dir dir
9
+ @dir = dir
10
+ end
11
+ def init_filename fname
12
+ @filename = fname
13
+ end
14
+ def get_filename
15
+ @filename
16
+ end
17
+ end
18
+ class PStoreReporter < Reporter
19
+ require 'pstore'
20
+ def run record
21
+ super
22
+ return unless record
23
+ fpath = File::join(@dir, @filename)
24
+ @logger.run("save to #{fpath}\n", 3)
25
+ db = PStore::new(fpath)
26
+ db.transaction do
27
+ db["result"] = ResponseRecord::new unless db.root?('result')
28
+ db['result'] += record
29
+ end
30
+ end
31
+ end
32
+ #TSV形式で保存するクラス
33
+ class TsvReporter < Reporter
34
+ UNIQUE_BY_THREAD = false
35
+ def init_filename starttime
36
+ @filename = "report_#{starttime}.tsv"
37
+ end
38
+ def run record
39
+ super
40
+ if not record
41
+ @logger.run("no rocord\n", 2)
42
+ exit -1
43
+ end
44
+ keys = record.keys.to_a.sort
45
+ strResultTSV = "Path\tCount\tAvgResponse(ms)\tMinResponse(ms)\tMaxResponse(ms)\tAvgContentLength\tError%\tHTTP_STATUS_CODE\ttime(ms)\n"
46
+
47
+ rank_avg_time = []
48
+ sum_avg_time = 0
49
+ rank_error_per = []
50
+ sum_error = 0
51
+ sum_access = 0
52
+
53
+ if keys.nil?
54
+ @logger.run("cannot show report\n", 2)
55
+ exit -1
56
+ end
57
+ keys.to_a.each do |key|
58
+ v = record[key]
59
+ next if v[:accesscount] == 0
60
+ avg_res = v[:totaltime] * 1000.0 / v[:accesscount]
61
+ error_per = (v[:errorcount].to_f / v[:accesscount])*100
62
+
63
+ rank_avg_time << [key, avg_res, v[:accesscount], v[:min]*1000.0]
64
+ sum_avg_time += v[:totaltime] * 1000.0
65
+ rank_error_per << [key, error_per, v[:accesscount]]
66
+ sum_error += v[:errorcount]
67
+ sum_access += v[:accesscount]
68
+
69
+ strResultTSV +=
70
+ key + "\t" + # URL
71
+ v[:accesscount].to_s + "\t" + # COUNT
72
+ format( '%f', avg_res ) + "\t" + # AVG
73
+ format('%f', v[:min].to_f*1000) + "\t" +
74
+ format('%f', v[:max].to_f*1000) + "\t" +
75
+ format( '%d', v[:size] / v[:accesscount] ) + "\t" + # AVG
76
+ (error_per).to_s + "\t" # ERROR%
77
+ status_code = []
78
+ res_time =[]
79
+ v.keys.each do |v_key|
80
+ if(v_key.to_s =~ /^status:/)
81
+ status_code << "#{v_key}: #{v[v_key]}"
82
+ end
83
+ if(v_key.to_s =~ /^time:/)
84
+ res_time << "#{v_key}: #{v[v_key]}"
85
+ end
86
+ end
87
+ strResultTSV += status_code.sort.join(",") + "\t" # HTTP_STAT
88
+ strResultTSV += res_time.sort.join(",") # RESPONSE TIME
89
+ strResultTSV += "\n"
90
+ end
91
+
92
+ if @config["save_report"]
93
+ save strResultTSV
94
+ fpath = File::join(@dir, @filename)
95
+ @logger.run("save to:"+fpath+"\n", 2)
96
+ end
97
+
98
+
99
+ #ranking
100
+ rank_error_per = rank_error_per.sort_by{|p|p[1]}.reverse.slice(0,@config["ranking"]).to_a
101
+ rank_avg_time = rank_avg_time.sort_by{|p|p[1]}.reverse.slice(0,@config["ranking"]).to_a
102
+ rank_min_time = rank_avg_time.sort_by{|p|p[3]}.reverse.slice(0,@config["ranking"]).to_a
103
+
104
+ @logger.run("MinResponse Worst:\n", 2)
105
+ rank = 1
106
+ rank_min_time.each do |p|
107
+ @logger.run("\t#{rank}: #{p[0]} #{p[3]} #{p[2]}\n", 2)
108
+ rank += 1
109
+ end
110
+ @logger.run("AvgResponse Worst:\n", 2)
111
+ rank = 1
112
+ rank_avg_time.each do |p|
113
+ @logger.run("\t#{rank}: #{p[0]} #{p[1]} #{p[2]}\n", 2)
114
+ rank += 1
115
+ end
116
+ @logger.run("AvgResponse Total:\n", 2)
117
+ @logger.run("\t#{sum_avg_time.to_f / sum_access}\n", 2)
118
+
119
+ @logger.run("ErrorRate Worst:\n", 2)
120
+ rank = 1
121
+ rank_error_per.each do |p|
122
+ @logger.run("\t#{rank}: #{p[0]} #{p[1]} #{p[2]}\n", 2)
123
+ rank += 1
124
+ end
125
+ @logger.run("ErrorRate Total:\n", 2)
126
+ @logger.run("\t#{(sum_error.to_f / sum_access)*100.0}\n", 2)
127
+
128
+ if(@config["show_report"])
129
+ @logger.run(strResultTSV, 2)
130
+ end
131
+ end
132
+ def save str
133
+ begin
134
+ fpath = File::join(@dir, @filename)
135
+ File::open(fpath, "w") do |io|
136
+ io.print str
137
+ end
138
+ rescue
139
+ raise $!
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,65 @@
1
+ module InternetHakai
2
+ #http response
3
+ class ResponseObject
4
+ def self::create_from_nethttp obj
5
+ r = self::new
6
+ r.body = obj.body.to_s
7
+ r.status = obj.code.to_i
8
+ r.content_type = obj['content-type']
9
+ r.location = obj['location'] if obj['location']
10
+ r.cookie = obj['set-cookie']
11
+ return r
12
+ end
13
+ def initialize
14
+ @header = nil
15
+ @status = 0
16
+ @time = 0
17
+ @content_type = ''
18
+ @location = nil
19
+ @body = ''
20
+ @cookie = nil
21
+ end
22
+ attr_accessor :body, :header, :status, :time, :content_type, :location, :cookie
23
+ end
24
+ #レスポンスタイム記録クラス
25
+ class ResponseRecord < Hash
26
+ ## 参照
27
+ def []( key )
28
+ if (! self.has_key?( key ))
29
+ self[key] = {
30
+ :totaltime => 0,
31
+ :accesscount => 0,
32
+ :errorcount => 0,
33
+ :size => 0,
34
+ :min => nil,
35
+ :max => nil
36
+ }
37
+ end
38
+ return( super(key) )
39
+ end
40
+ # 演算
41
+ def +(other)
42
+ other.each do |key, value|
43
+ s = self[key]
44
+ s[:totaltime] += value[:totaltime]
45
+ s[:accesscount] += value[:accesscount]
46
+ s[:errorcount] += value[:errorcount]
47
+ s[:size] += value[:size]
48
+ if s[:min].nil?
49
+ s[:min] = value[:min]
50
+ s[:max] = value[:max]
51
+ else
52
+ s[:min] = (s[:min] < value[:min]) ? s[:min] : value[:min] unless value[:min].nil?
53
+ s[:max] = (s[:max] > value[:max]) ? s[:max] : value[:max] unless value[:max].nil?
54
+ end
55
+
56
+ value.keys.each do |v_key|
57
+ if(v_key.to_s =~ /status:/ || v_key.to_s =~ /time:/)
58
+ s[v_key] = s[v_key].to_i + value[v_key]
59
+ end
60
+ end
61
+ end
62
+ return self
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,98 @@
1
+ module InternetHakai
2
+ # = シナリオ
3
+ class Scenario
4
+ def initialize(opt)
5
+ @config = opt
6
+ @size = 0
7
+ @actions_size
8
+ @performance_id = nil
9
+ @scenario_id = nil
10
+ @loop = @config['loop']
11
+ @loop_org = @loop
12
+ digit = @loop.to_s.size
13
+ @tmpl = "%0#{digit}d"
14
+ @request_pool = BaseHandler::get_handler(@config['request_pool'])
15
+
16
+ #@page_size = [10, @loop].min
17
+ @page_size = 1
18
+ @page = (@loop.quo(@page_size)).ceil
19
+ @page_counter = @page
20
+ @vars = {}
21
+ @cookie = {}
22
+ end
23
+ attr_reader :actions_size
24
+ attr_accessor :performance_id, :scenario_id, :vars, :cookie
25
+ def size
26
+ @size
27
+ end
28
+ def init
29
+ @performance_id = (@scenario_id.to_s + sprintf(@tmpl, @loop)).to_i
30
+ act = @commands.shift
31
+ act.performance_id = @performance_id
32
+ @request_pool.add(act)
33
+ end
34
+ def next
35
+ @action_counter -= 1
36
+ #puts "action: #{@action_counter}"
37
+ #puts "loop: #{@loop}"
38
+ if @action_counter == 0
39
+ @loop -= 1
40
+ #これで最後
41
+ return if @loop == 0
42
+ @action_counter = @actions_size
43
+ @performance_id = (@scenario_id.to_s + sprintf(@tmpl, @loop)).to_i
44
+ end
45
+ if @commands.empty?
46
+ #ループが残っていればまた最初から
47
+ @commands = @commands_org.clone
48
+ @cookie = {}
49
+ @vars = {}
50
+ end
51
+ act = @commands.shift
52
+ act.performance_id = @performance_id
53
+ @request_pool.add(act)
54
+ end
55
+ def each &block
56
+ @commands_org.each do |cm|
57
+ block.call(cm)
58
+ end
59
+ end
60
+ def create_scenario
61
+ @commands = []
62
+ @performance_id = (@scenario_id.to_s + sprintf(@tmpl, @loop)).to_i
63
+ @actions_size = @config['actions'].inject(0){|sum, i|
64
+ times = i['times']
65
+ sum + times
66
+ }
67
+ baseconfig = @config.clone
68
+ baseconfig.delete('actions')
69
+ 1.upto(@page_size){
70
+ @config["actions"].each do |actconfig|
71
+ actconfig = baseconfig.merge(actconfig)
72
+ times = actconfig['times']
73
+ command = actconfig['class_obj']
74
+ 1.upto(times) do
75
+ actconfignew = actconfig.clone
76
+ obj = command.new(actconfignew, self)
77
+ obj.set_wait if(actconfignew['wait'])
78
+ @commands << obj
79
+ end
80
+ end
81
+ }
82
+ @action_counter = @actions_size
83
+ @commands_org = @commands.clone
84
+ @size = @actions_size * @loop_org
85
+ end
86
+ def set_cookie cookie
87
+ return unless cookie
88
+ k, v = cookie[0...cookie.index(';')].split('=')
89
+ @cookie[k] = v if k && v
90
+ end
91
+ def create_act idx
92
+ actconf = @actconfigs[idx]
93
+ obj = actconf['class_obj']::new(actconf)
94
+ obj.set_scenario(self)
95
+ obj
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,58 @@
1
+ loop:
2
+ 1
3
+ max_request:
4
+ 1
5
+ max_scenario:
6
+ 1
7
+ log_level:
8
+ 2
9
+ encoding: Shift_JIS
10
+ ranking: 20
11
+ timeout: 5
12
+ show_report: true
13
+ save_report: false
14
+
15
+ ## 負荷試験対象のサイトに変更してください
16
+ domain:
17
+ "<%= domain %>"
18
+
19
+
20
+ <% if social %>
21
+ ## ソーシャルアプリの場合コメントアウト
22
+ client_handler: "SocialClientHandler"
23
+ opensocial_id_path: #ユーザーidを書いたファイルを用意してください
24
+ ./userids
25
+ opensocial_app_id:
26
+ yourapp
27
+ <% else %>
28
+ ## ソーシャルアプリの場合コメントアウト
29
+ #client_handler: "SocialClientHandler"
30
+ #opensocial_id_path: #ユーザーidを書いたファイルを用意してください
31
+ # ./userids
32
+ #opensocial_app_id:
33
+ # app
34
+ <% end %>
35
+
36
+ ## ユーザーエージェントを変更したい場合、コメントアウト
37
+ #user_agent:
38
+ # 'Mozilla/4.0 (compatible; MSIE 4.0; MSN 2.5; Windows 95)'
39
+
40
+
41
+ ## fork して使用する場合コメントアウト
42
+ #max_process: 4
43
+
44
+
45
+ #以下にアクション(シナリオ)を書きます
46
+ actions:
47
+ -
48
+ path: /
49
+ ## example
50
+ # -
51
+ # path: /somepath
52
+ # -
53
+ # path: /somepath
54
+ # method: POST
55
+ # post_params:
56
+ # key1: hoge
57
+ # key2: fuga
58
+
@@ -0,0 +1,183 @@
1
+ module InternetHakai
2
+ class ScenarioBuilder < BaseHandler
3
+ UNIQUE_BY_THREAD = false
4
+ require 'socket'
5
+ require 'yaml'
6
+ DEFAULT_CONFIG = {
7
+ "loop" => 1,
8
+ "rev" => true,
9
+ "max_scenario" => 1,
10
+ "max_request" => 1,
11
+ "log_level" => 3,
12
+ "ranking" => 5,
13
+ "follow_redirect" => true,
14
+ "queue" => "SimpleQueue",
15
+ "action_handler" => "RequestAction",
16
+ "client_handler" => "ClientHandler",
17
+ "response_handler" => "TimeRegister",
18
+ "request_pool" => "RequestPool",
19
+ "http_client" => "HttpClient",
20
+ "scenario_executer" => "ScenarioExecuter",
21
+ "logger" => "Logger",
22
+ "encoding" => "Shift_JIS",
23
+ "save_report" => false,
24
+ "reporter" => "TsvReporter",
25
+ "show_report" => false,
26
+ "sleep" => false,
27
+ "timeout" => 10,
28
+ "actions" => [],
29
+ }
30
+ def get_config
31
+ @config
32
+ end
33
+ def prepare_config
34
+ baseconfig = @config
35
+ basehost, baseport, = Util::parse_url(baseconfig['domain'])
36
+ basehostaddress = IPSocket::getaddress(basehost)
37
+ baseconfig['host'] = basehostaddress
38
+ baseconfig['host_name'] = basehost unless baseconfig['host_name']
39
+ baseconfig['port'] = baseport
40
+ baseconfig['encoding'] ||= 'UTF-8'
41
+
42
+ #後方互換性のため
43
+ if baseconfig.has_key?('fork') && baseconfig['fork'].has_key?('max_process') && baseconfig['fork']['max_process'] > 1 && !baseconfig.has_key?('max_process')
44
+ baseconfig['max_process'] = baseconfig['fork']['max_process']
45
+ else
46
+ baseconfig['max_process'] ||= 1
47
+ end
48
+ #後方互換性のため
49
+ if baseconfig.has_key?('concurrency') && baseconfig['max_scenario'] == 1
50
+ baseconfig['max_scenario'] = baseconfig['concurrency']
51
+ end
52
+
53
+ headers = baseconfig.has_key?('header') ? baseconfig['header'] : Hash::new
54
+ headers['User-Agent'] = baseconfig['user_agent'] if baseconfig.has_key?('user_agent')
55
+ headers['Host'] = baseconfig['host_name']
56
+ baseconfig['header'] = headers
57
+ hosts = []
58
+ hosts << [basehostaddress, basehost, baseport]
59
+ hosts_cache = {}
60
+ hosts_cache[hosts[0].join(':')] = true
61
+ idx = 0
62
+
63
+ base = baseconfig.clone
64
+ base.delete('actions')
65
+ actions = []
66
+ @config["actions"].each do |actconfig|
67
+ if actconfig.is_a? String
68
+ path = actconfig
69
+ actconfig = Hash::new
70
+ actconfig['path'] = path
71
+ end
72
+ url = actconfig['path']
73
+ actconfig['path'] = url
74
+ actconfig['method'] = 'GET' unless actconfig.has_key? 'method'
75
+ actconfig['class'] = baseconfig['action_handler'] unless actconfig.has_key? 'class'
76
+ classname = actconfig["class"]
77
+ command = BaseHandler::get_class(classname)
78
+ raise "invalid class" unless command < BaseAction
79
+ actconfig['class_obj'] = command
80
+
81
+ if (!url.match( /^http:\/\// ))
82
+ url = baseconfig['domain'].to_s + url.to_s
83
+ actconfig['host'] = basehostaddress
84
+ actconfig['host_name'] = basehost
85
+ actconfig['port'] = baseport
86
+ else
87
+ host, port, path = Util::parse_url(url)
88
+ #名前解決
89
+ address = IPSocket::getaddress(host)
90
+ actconfig['host'] = address
91
+ actconfig['path'] = path
92
+ actconfig['host_name'] = host unless baseconfig['host_name']
93
+ actconfig['port'] = port
94
+ info = [address, host, port]
95
+ headers = actconfig.has_key?('header') ? actconfig['header'] : Hash::new
96
+ headers['User-Agent'] = baseconfig['user_agent'] if baseconfig.has_key?('user_agent')
97
+ headers['Host'] = actconfig['host_name']
98
+ actconfig['header'] = headers
99
+ key = info.join(':')
100
+ unless hosts_cache.has_key?(key)
101
+ hosts << info
102
+ hosts_cache[key] = true
103
+ end
104
+ end
105
+ if(actconfig.has_key?('post_params'))
106
+ actconfig['post_string'] = Util::hash2poststring(actconfig['post_params'])
107
+ end
108
+ unless actconfig.has_key?('type')
109
+ actconfig['type'] = actconfig['path'].gsub( /\?.*$/, '' ).gsub( /[#;].*/, '' )
110
+ end
111
+ if actconfig.has_key?('expr')
112
+ if RUBY_VERSION >= "1.9"
113
+ s = actconfig['expr'].encode(baseconfig['encoding'])
114
+ actconfig['compiled_expr'] = Regexp::compile(s)
115
+ else
116
+ actconfig['compiled_expr'] = Regexp::compile(actconfig['expr'])
117
+ end
118
+ end
119
+ actconfig['url'] = url
120
+ time = actconfig['times'] || 1
121
+ actconfig['times'] = time unless actconfig.has_key?('times')
122
+ idx += 1
123
+ actions << actconfig
124
+ end
125
+ scenario_size = actions.inject(0){|sum, i|
126
+ sum + i['times']
127
+ }
128
+ @config['scenario_size'] = scenario_size
129
+ @config['actions'] = actions
130
+ @config['hosts'] = hosts
131
+ #concurrency はデフォルトでスレッドと同じ
132
+ if !@config.has_key?('max_scenario') || @config['max_scenario'] < @config['max_request']
133
+ @config['max_scenario'] = @config['max_request']
134
+ end
135
+ @config['max_scenario'] = 1 if @config['max_scenario'] <= 0
136
+
137
+ @config['max_scenario_show'] = @config['max_scenario']
138
+ @config['max_request_show'] = @config['max_request']
139
+ if @config['max_process'] > 1
140
+ #fork する場合は、concurrency をプロセス数で割る
141
+ process = @config['max_process']
142
+ @config['max_process'] = process
143
+ @config['max_scenario'] = (@config['max_scenario'] / process).round
144
+ @config['max_scenario'] = 1 if @config['max_scenario'] <= 0
145
+ @config['max_request'] = (@config['max_request']/ process).round
146
+ @config['max_request'] = 1 if @config['max_request'] <= 0
147
+ end
148
+ ch = BaseHandler::get_class(@config["client_handler"])
149
+ @config = ch::on_config_load(@config)
150
+ end
151
+ def load path
152
+ begin
153
+ basedir = File::expand_path(File::dirname(path))
154
+ str = File::open(path){|io|io.read}
155
+ load_config(str, basedir)
156
+ rescue
157
+ raise $!
158
+ end
159
+ end
160
+ def load_config str, basedir='.'
161
+ default_config = DEFAULT_CONFIG
162
+ if(RUBY_VERSION.to_f >= 1.9)
163
+ str.force_encoding('UTF-8')
164
+ end
165
+ if str.include?('%%BASEDIR%%')
166
+ puts "WARNING: %%BASEDIR%% is obsolete"
167
+ str.gsub!('%%BASEDIR%%', basedir)
168
+ end
169
+ @config = ::YAML::load(str)
170
+ raise "invalid file" unless @config
171
+ @config = default_config.merge(@config)
172
+ prepare_config
173
+ if(@config["require"])
174
+ begin
175
+ require @config["require"]
176
+ rescue
177
+ @logger.run("class load error: #{@config['require']}", 1)
178
+ end
179
+ end
180
+ @config
181
+ end
182
+ end
183
+ end