internethakai 0.2.0

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