gargor 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -7,7 +7,6 @@ Gemfile.lock
7
7
  InstalledFiles
8
8
  _yardoc
9
9
  coverage
10
- doc/
11
10
  lib/bundler/man
12
11
  pkg
13
12
  rdoc
@@ -15,3 +14,4 @@ spec/reports
15
14
  test/tmp
16
15
  test/version_tmp
17
16
  tmp
17
+ /gargor.log
@@ -0,0 +1,11 @@
1
+ bundler_args: --without development
2
+ language: ruby
3
+ matrix:
4
+ allow_failures:
5
+ - rvm: 1.8.7
6
+ rvm:
7
+ - 1.8.7
8
+ - 1.9.2
9
+ - 1.9.3
10
+ - 2.0.0
11
+ script: bundle exec rspec
data/Gemfile CHANGED
@@ -1,8 +1,24 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- # Specify your gem's dependencies in gargor.gemspec
4
- gemspec
5
-
6
3
  gem 'json'
7
4
  gem 'jsonpath'
5
+ gem "progressbar", "~> 0.20.0"
6
+ gem "version", "~> 1.0.0"
7
+
8
+ group :development do
9
+ gem 'pry'
10
+ gem 'pry-debugger'
11
+ end
12
+
13
+ group :test, :development do
14
+ gem 'childlabor'
15
+ gem 'coveralls'
16
+ gem 'fakeweb'
17
+ gem 'rspec'
18
+ gem 'rspec-mocks'
19
+ gem 'simplecov'
20
+ end
21
+
22
+ # Specify your gem's dependencies in gargor.gemspec
23
+ gemspec
8
24
 
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # gargor
1
+ # gargor [![Build Status](https://travis-ci.org/tumf/gargor.png?branch=master)](https://travis-ci.org/tumf/gargor) [![Gem Version](https://badge.fury.io/rb/gargor.png)](http://badge.fury.io/rb/gargor) [![Code Climate](https://codeclimate.com/github/tumf/gargor.png)](https://codeclimate.com/github/tumf/gargor) [![Dependency Status](https://gemnasium.com/tumf/gargor.png)](https://gemnasium.com/tumf/gargor)
2
2
 
3
3
  `gargor` is software which uses genetic algorithm to support parameter tuning of the servers controlled by Chef.Using this software, you are able to optimize and automate the server tuning, which you did until now based on a combination of my experience and intuition.  
4
4
 
@@ -46,6 +46,14 @@ target_nodes ["www-1.example","www-2.example","db-1.example"]
46
46
  # attack command
47
47
  attack_cmd "ssh attacker.example ./bin/ghakai www-1.example.yml 2>/dev/null"
48
48
 
49
+ # logger
50
+ logger "gargor.log"
51
+
52
+ # or optional settings like blows:
53
+ #
54
+ # logger "gargor.log" do |log|
55
+ # log.level = Logger::INFO
56
+ # end
49
57
 
50
58
  # evalute of the attack
51
59
  # code: exit code of attack_cmd command (0 => succees)
@@ -46,6 +46,14 @@ target_nodes ["www-1.example","www-2.example","db-1.example"]
46
46
  # 攻撃コマンド
47
47
  attack_cmd "ssh attacker.example ./bin/ghakai www-1.example.yml 2>/dev/null"
48
48
 
49
+ # ログ
50
+ logger "gargor.log"
51
+
52
+ # もしくは以下のようにしてLoggerの設定ができる
53
+ #
54
+ # logger "gargor.log" do |log|
55
+ # log.level = Logger::INFO
56
+ # end
49
57
 
50
58
  # 攻撃結果の評価
51
59
  # code: attack_cmdのプロセス終了コード(普通は0:成功)
data/Rakefile CHANGED
@@ -1 +1,10 @@
1
1
  require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ task :default => :spec
5
+
6
+ desc "Run all specs in spec directory"
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ require 'rake/version_task'
10
+ Rake::VersionTask.new
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.4
data/bin/gargor CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
2
  $:.unshift(File.dirname(__FILE__) + '/../lib')
3
+ $TESTING=false
3
4
  require 'gargor'
5
+ require 'progressbar'
4
6
 
5
7
  dsl_file=ARGV.last||"gargor.rb"
6
8
 
@@ -11,22 +13,25 @@ end
11
13
  begin
12
14
  Gargor.start
13
15
  Gargor.load_dsl(dsl_file)
14
- rescue
16
+ rescue =>e
17
+ STDERR.puts e.backtrace.join("\n")
18
+
15
19
  usage
16
20
  exit 1
17
21
  end
18
22
 
23
+ pbar = ProgressBar.new("auto-tuning",Gargor.total_trials)
19
24
  loop {
20
25
  Gargor.populate.each { |i|
21
26
  if i.fitness == nil
22
27
  i.set_params
23
- if i.deploy
24
- i.attack
25
- end
28
+ i.attack if i.deploy
26
29
  end
30
+ pbar.set(Gargor.total_trials-Gargor.last_trials)
27
31
  }
28
32
  break unless Gargor.next_generation
29
33
  }
34
+ pbar.finish
30
35
 
31
36
  best = Gargor.individuals.max { |a,b| a.fitness <=> b.fitness }
32
37
  if best
@@ -0,0 +1,168 @@
1
+ Auto-tuning internet servers by Genetic Algorithm
2
+ ====================================================
3
+
4
+ You can get good settings during sleeping ;)
5
+
6
+ First it is necessary to prepare to the point where an ordinary stress test is possible. Please prepare the server to be used for attacking in a location that is close (in terms of network) to the target. Suitable specifications are required for the Attacker as well.
7
+
8
+ Anything is acceptable as a stress tool providing that it can be used from a command line. Although `ab(Apache Bench)` etc. have been included from the beginning and therefore would be simple, this time, a software called [green-hakai](https://github.com/KLab/green-hakai) was used. (The installation instructions for `green-hakai` are available from the product’s website)
9
+
10
+ When performing automatic tuning, the target (= sever group of the tuning target) is controlled by Chef and it is necessary that the parameters become attributes of Chef. For example, when the template `httpd.conf.erb` of `httpd.conf` becomes as shown:
11
+
12
+ ```
13
+ <IfModule prefork.c>
14
+ StartServers 8
15
+ MinSpareServers 10
16
+ MaxSpareServers 20
17
+ ServerLimit 255
18
+ MaxClients 255
19
+ MaxRequestsPerChild 1000
20
+ </IfModule>
21
+ ```
22
+
23
+ If these `MaxClients` are to be made attribute of Chef, do it as follows:
24
+
25
+ ```
26
+ <IfModule prefork.c>
27
+ StartServers 8
28
+ MinSpareServers 10
29
+ MaxSpareServers 20
30
+ ServerLimit 255
31
+ MaxClients <%= node["httpd"]["max_clients"] %>
32
+ MaxRequestsPerChild 1000
33
+ </IfModule>
34
+ ```
35
+
36
+ When in this state, by writing as shown below in `nodes/www-1.example.json`, it is possible to control `MaxClients` of the `www-1.example` server.
37
+
38
+ ```
39
+ {
40
+ "httpd" :{
41
+ "max_clients" :255
42
+ },
43
+ ...
44
+ }
45
+ ```
46
+
47
+ As a result of rewriting this JSON, deploying it to the target by using Chef, and then applying the stress test, it will be `gargor` (the new software) that searches the new test parameter using GA.
48
+
49
+ Install `gargor` by doing the following: (Ruby 1.9.3 or higher is required)
50
+
51
+ ```
52
+ [sudo] gem install gargor
53
+ ```
54
+
55
+ Position `gargor.rb` under the Chef repository. Rewrite the contents as necessary.
56
+
57
+ ```ruby
58
+ # generations: set > 1
59
+ max_generations 10
60
+
61
+ # individuals number of some generation.
62
+ population 10
63
+
64
+ # elite number of some generation.(carried over)
65
+ elite 1
66
+
67
+ # Probability of mutation set "0.01" to "1%" (when crossover)
68
+ mutation 0.01
69
+
70
+ # target cook command : '%s' will replace by node name.
71
+ target_cooking_cmd "knife solo cook %s"
72
+
73
+ # target nodes
74
+ # performing target_cooking_command before the attack.
75
+ target_nodes ["www-1.example","www-2.example","db-1.example"]
76
+
77
+ # attack command
78
+ attack_cmd "ssh attacker.example ./bin/ghakai www-1.example.yml 2>/dev/null"
79
+
80
+ # logger
81
+ logger "gargor.log"
82
+
83
+ # or optional settings like blows:
84
+ #
85
+ # logger "gargor.log" do |log|
86
+ # log.level = Logger::INFO
87
+ # end
88
+
89
+
90
+ # evalute of the attack
91
+ # code: exit code of attack_cmd command (0 => succees)
92
+ # out: standard output of attack_cmd command
93
+ # time: execute time of attack_cmd
94
+ evaluate do |code,out,time|
95
+ puts out
96
+ fitness = 0
97
+ # get "FAILED" count from stadard output of stress-tool,
98
+ # and set fitess to 0 when FAILED > 0.
99
+ if time > 0 && code == 0 && /^FAILED (\d+)/ =~ out && $1 == "0"
100
+ # get fitness from stadard output of stress-tool.
101
+ # e.g.: request count:200, concurrenry:20, 45.060816 req/s
102
+ if /, ([\.\d]+) req\/s/ =~ out
103
+ fitness = $1.to_f
104
+ end
105
+ # To get fitness simply,to use execution time
106
+ # fitness = 1/time
107
+ end
108
+ # This block must return the fitness.(integer or float)
109
+ fitness
110
+ end
111
+
112
+ # definition of parameters(GA)
113
+ #
114
+ # param _name_ do
115
+ # json_file: Chef parameter(JSON) file (such as nodes/*.json or roles/*.json)
116
+ # (Warning!) gargor will overwrite their json files.
117
+ # json_path: to locate the value by JSONPath
118
+ # mutaion: to set value when mutaion
119
+ param "max_clients" do
120
+ json_file "roles/www.json"
121
+ json_path '$.httpd.max_clients'
122
+ mutation rand(500)+10
123
+ end
124
+
125
+ param "innodb_log_file_size" do
126
+ json_file "nodes/db-1.example.json"
127
+ json_path '$.mysqld.innodb_log_file_size'
128
+ mutation rand(200)
129
+ end
130
+
131
+ param "sort_buffer_size" do
132
+ json_file "nodes/db-1.example.json"
133
+ json_path '$.mysqld.sort_buffer_size'
134
+ mutation rand(1000)
135
+ end
136
+
137
+ param "read_buffer_size" do
138
+ json_file "nodes/db-1.example.json"
139
+ json_path '$.mysqld.read_buffer_size'
140
+ mutation rand(1000)
141
+ end
142
+
143
+ param "query_cache_size" do
144
+ json_file "nodes/db-1.example.json"
145
+ json_path '$.mysqld.query_cache_size'
146
+ mutation rand(100)
147
+ end
148
+ ```
149
+
150
+
151
+ When using something other than `green-hakai`, it will be necessary to customize `evaluate` block.
152
+
153
+
154
+ ```
155
+ # fitness = 1/time
156
+ ```
157
+
158
+ However, by removing the comment out of this part, it is possible to decide a rough score by the (inverse number of) the measurement time.
159
+
160
+ `gargor` performs the stress test for the number of (number of generations * number of individuals) at the maximum and searches for parameters that appear good. As it takes a long time, it is probably best to run when finishing work for the day in order to confirm the results the next morning.
161
+
162
+ Of course, it is possible to perform multiple settings for parameters.
163
+
164
+ As GA is used, there are erratic elements, and unless the software is tried, it is unknown whether good results will be output. If the number of individuals and the number of generations are increased, it will take longer but the precision will be improved. Also, because the precision also changes depending on how `mutation` is written, the user is encouraged to try many things.
165
+
166
+ As `gargor` is only just-created software, please report bugs and fixes to `github`. Developer is waiting for pull-request.
167
+
168
+ > https://github.com/tumf/gargor
@@ -0,0 +1,90 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Gargor Sample DSL file
3
+
4
+ # 世代数: 1以上を指定してください
5
+ max_generations 10
6
+
7
+ # 個体数: 1世代あたりの個体数
8
+ population 10
9
+
10
+ # エリート: 世代交代の時に適応値の高い個体をそのまま次世代に引き継ぐ数
11
+ elite 1
12
+
13
+ # 突然変異の確率: "0.01"で"1%"
14
+ mutation 0.01
15
+
16
+ # ターゲットをChefで料理するコマンド %sには、ノード名が入る
17
+ target_cooking_cmd "knife solo cook %s"
18
+
19
+ # ターゲットのノード
20
+ # 攻撃前に以下のノードすべてに対してtarget_cooking_cmdが実施される
21
+ target_nodes ["www-1.example","www-2.example","db-1.example"]
22
+
23
+ # 攻撃コマンド
24
+ attack_cmd "ssh attacker.example ./bin/ghakai www-1.example.yml 2>/dev/null"
25
+
26
+
27
+ # 攻撃結果の評価
28
+ # code: attack_cmdのプロセス終了コード(普通は0:成功)
29
+ # out: attack_cmdの標準出力
30
+ # time: attack_cmdの実行時間
31
+ evaluate do |code,out,time|
32
+ puts out
33
+ fitness = 0
34
+
35
+ # 攻撃コマンドで使っている、グリーン破壊の標準出力から
36
+ # FAILEDの値を抜き出し0以外は適応値0としている
37
+ if time > 0 && code == 0 && /^FAILED (\d+)/ =~ out && $1 == "0"
38
+ # 攻撃コマンドで使っている、グリーン破壊の標準出力から
39
+ # 適応値に代用できそうなものを正規表現で取得する
40
+ # request count:200, concurrenry:20, 45.060816 req/s
41
+ if /, ([\.\d]+) req\/s/ =~ out
42
+ fitness = $1.to_f
43
+ end
44
+ # 単純に実行時間で適応値を設定したいなら以下のようにしても良い
45
+ # fitness = 1/time
46
+ end
47
+ # このブロックは必ず適応値を返すこと(整数or浮動小数)
48
+ fitness
49
+ end
50
+
51
+ # パラメータ定義
52
+ # GAにより変動されるパラメータをここで定義する
53
+ #
54
+ # param 名前 do
55
+ # json_file: 値を上書くJSONファイル nodes/*.jsonやroles/*.json
56
+ # (注意!) gargorはこのjsonファイルを容赦なく書き換える
57
+ # json_path: json_fileの中から書変える値の場所をJSONPath形式で指定する
58
+ # mutaion: 突然変異時の値の設定
59
+ param "max_clients" do
60
+ json_file "roles/www.json"
61
+ json_path '$.httpd.max_clients'
62
+ mutation rand(500)+10
63
+ end
64
+
65
+ param "innodb_log_file_size" do
66
+ json_file "nodes/db-1.example.json"
67
+ json_path '$.mysqld.innodb_log_file_size'
68
+ mutation rand(200)
69
+ end
70
+
71
+ param "sort_buffer_size" do
72
+ json_file "nodes/db-1.example.json"
73
+ json_path '$.mysqld.sort_buffer_size'
74
+ mutation rand(1000)
75
+ end
76
+
77
+ param "read_buffer_size" do
78
+ json_file "nodes/db-1.example.json"
79
+ json_path '$.mysqld.read_buffer_size'
80
+ mutation rand(1000)
81
+ end
82
+
83
+ param "query_cache_size" do
84
+ json_file "nodes/db-1.example.json"
85
+ json_path '$.mysqld.query_cache_size'
86
+ mutation rand(100)
87
+ end
88
+
89
+
90
+
@@ -1,13 +1,27 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  require 'rubygems' if RUBY_VERSION < '1.9'
3
+ require "logger"
4
+
3
5
  require "gargor/version"
4
6
  require "gargor/individual"
5
7
  require "gargor/parameter"
6
8
 
7
9
  class Gargor
10
+ class GargorError < RuntimeError; end
11
+ class ExterminationError < GargorError; end
12
+ class ParameterError < GargorError; end
13
+ class ValidationError < GargorError; end
14
+ class ArgumentError < GargorError; end
15
+
16
+ class Individuals < Array
17
+ def has? i
18
+ !!self.find { |ii| ii.params == i.params }
19
+ end
20
+ end
21
+
8
22
  GLOBAL_OPTS = ["population","max_generations","target_nodes",
9
23
  "attack_cmd","elite","mutation","target_cooking_cmd",
10
- "attack_node","fitness_precision","attack_result"]
24
+ "fitness_precision"]
11
25
 
12
26
  GLOBAL_OPTS.each { |name|
13
27
  define_method(name) { |val|
@@ -15,8 +29,29 @@ class Gargor
15
29
  }
16
30
  }
17
31
 
32
+ def log message,level=Logger::INFO
33
+ Gargor.log(message,level)
34
+ end
18
35
  class << self
36
+ def log message,level=Logger::INFO
37
+ return if $TESTING
38
+ message.to_s.split("\n").each { |line| @@logger.add(level) {line} }
39
+ end
40
+
41
+ def debug message
42
+ log message,Logger::DEBUG
43
+ end
44
+
45
+ def params
46
+ result = {}
47
+ GLOBAL_OPTS.map { |name|
48
+ result[name] = Gargor.class_variable_get("@@#{name}")
49
+ }
50
+ result
51
+ end
52
+
19
53
  def start
54
+ @@logger = Logger.new(STDOUT)
20
55
  @@fitness_precision = 100000000
21
56
  @@prev_generation = nil
22
57
  @@individuals = []
@@ -29,26 +64,47 @@ class Gargor
29
64
  @@attack_proc = nil
30
65
  @@evaluate_proc = Proc.new { 0 }
31
66
  @@target_nodes = []
67
+ @@dsl_file = nil
68
+ true
69
+ end
70
+ Gargor.start
71
+
72
+ def validate
73
+ raise ValidationError,"POPULATION isn't > 0" unless @@population > 0
74
+ true
75
+ end
76
+
77
+ def first_generation?
78
+ @@generation == 1
79
+ end
80
+
81
+ # 前世代の数
82
+ def prev_count g = @@prev_generation
83
+ # fitness > 0 適応している個体
84
+ g.select { |i| i.fitness && i.fitness > 0 }.count
32
85
  end
33
86
 
34
87
  def load_dsl(params_file)
35
- contents = File.open(params_file, 'rb'){ |f| f.read }
88
+ @@dsl_file = params_file
89
+ contents = File.read(params_file)
36
90
  new.instance_eval(contents)
37
- raise "POPULATION == 0" if @@population == 0
91
+ validate
38
92
  end
39
93
 
40
- def mutation
94
+ def mutate
41
95
  individual = Individual.new
42
96
  @@param_procs.each { |name,proc|
43
97
  param = Parameter.new(name)
44
98
  param.instance_eval(&proc)
45
99
  individual.params[name] = param
46
100
  }
101
+ log "mutate #{individual}"
47
102
  individual
48
103
  end
49
104
 
50
105
  # 浮動小数点対応のrand
51
106
  def float_rand(f,p = @@fitness_precision)
107
+ raise ArgumentError,"max must be > 0" unless f > 0
52
108
  f *= @@fitness_precision
53
109
  i = f.to_i
54
110
  f = rand(i)
@@ -57,17 +113,11 @@ class Gargor
57
113
 
58
114
  def crossover a,b
59
115
  return a.clone if a.params == b.params
60
- puts "crossover: #{a} #{b}"
116
+ log "crossover: #{a} #{b}"
61
117
  total = a.fitness + b.fitness
62
118
  c = Individual.new
63
119
  c.params = a.params.clone
64
-
65
- c.params.each { |name,param|
66
- cur = float_rand(total)
67
- c.params[name] = b.params[name] if b.fitness > cur
68
- }
69
- puts "#{a.to_s} + #{b.to_s} \n => #{c.to_s}"
70
- c
120
+ c.overwrite_by(b,b.fitness,total)
71
121
  end
72
122
 
73
123
  def selection g
@@ -77,58 +127,73 @@ class Gargor
77
127
  return i if i.fitness > cur
78
128
  cur -= i.fitness
79
129
  }
80
- raise "error selection"
130
+ raise GargorError,"error selection"
81
131
  end
82
132
 
83
- def populate
84
- @@individuals = []
133
+ def populate_first_generation
134
+ individuals = Gargor::Individuals.new
135
+ individuals << mutate.load_now
136
+ loop{
137
+ break if individuals.length >= @@population
138
+ individuals << mutate
139
+ }
85
140
 
86
- # 第一世代
87
- unless @@prev_generation
88
- @@individuals << mutation.load_now
89
- loop{
90
- break if @@individuals.length >= @@population
91
- @@individuals << mutation
92
- }
93
- return @@individuals
94
- end
141
+ Gargor::Individuals.new(individuals.shuffle)
142
+ end
95
143
 
96
- # fitness > 0 適応している個体
97
- prev_count = @@prev_generation.select { |i| i.fitness > 0 }.count
144
+ def select_elites g,count
145
+ return [] unless count > 0
146
+ Gargor::Individuals.new(g.sort{ |a,b| a.fitness<=>b.fitness }.last(count))
147
+ end
98
148
 
99
- if prev_count < 2
100
- raise "***** EXTERMINATION ******"
149
+ def mutation? mutation=@@mutation
150
+ rand <= mutation
151
+ end
152
+
153
+ def select_parents g
154
+ [selection(g),selection(g)]
155
+ end
156
+
157
+ def populate_one
158
+ if mutation?
159
+ mutate
160
+ else
161
+ crossover(*select_parents(@@prev_generation))
101
162
  end
163
+ end
164
+
165
+ def populate_next_generation
166
+ log "population: #{@@prev_generation.length}"
167
+ individuals = Gargor::Individuals.new(select_elites @@prev_generation,@@elite)
102
168
 
103
- puts "population: #{@@prev_generation.length}"
104
- @@individuals = @@prev_generation.sort{ |a,b| a.fitness<=>b.fitness }.last(@@elite) if @@elite > 0
105
169
  loop{
106
- break if @@individuals.length >= @@population
107
- if rand <= @@mutation
108
- i = mutation
109
- puts "mutation #{i}"
110
- else
111
- a = selection @@prev_generation
112
- b = selection @@prev_generation
113
- i = crossover(a,b)
114
- end
115
-
116
- #同じのは追加しない
117
- if i and !@@individuals.find { |ii| ii.params == i.params }
118
- @@individuals << i
119
- end
170
+ break if individuals.length >= @@population
171
+ i = populate_one
172
+ individuals << i unless individuals.has?(i)
120
173
  }
121
- puts "poulate: #{@@individuals}"
122
- @@individuals
174
+ log "populate:"
175
+ individuals.each { |i| log i }
176
+ Gargor::Individuals.new(individuals.shuffle)
177
+ end
178
+
179
+ def populate
180
+ @@individuals = if first_generation?
181
+ # 第一世代
182
+ populate_first_generation
183
+ else
184
+ # 次世代
185
+ raise ExterminationError unless prev_count >= 2
186
+ populate_next_generation
187
+ end
123
188
  end
124
189
 
125
190
 
126
191
  def next_generation
127
- puts "<== end generation #{@@generation}"
192
+ log "<== end generation #{@@generation}"
128
193
  @@generation += 1
129
194
  return false if @@generation > @@max_generations
130
195
 
131
- puts "==> next generation #{@@generation}"
196
+ log "==> next generation #{@@generation}"
132
197
  @@prev_generation = @@individuals
133
198
  true
134
199
  end
@@ -140,6 +205,24 @@ class Gargor
140
205
  def opt name
141
206
  Gargor.class_variable_get("@@#{name}")
142
207
  end
208
+
209
+ def logfile file
210
+ File.expand_path(File.join(File.dirname(@@dsl_file),file))
211
+ end
212
+
213
+ def total_trials
214
+ @@population+(@@population-@@elite)*(@@max_generations-1)
215
+ end
216
+
217
+ def last_trials_at_this_generation
218
+ @@individuals.select{ |i| i.fitness == nil }.count
219
+ end
220
+
221
+ def last_trials
222
+ last_trials_at_this_generation +
223
+ (@@max_generations-@@generation)*(@@population-@@elite)
224
+ end
225
+
143
226
  end
144
227
 
145
228
  def param name,&block
@@ -154,4 +237,11 @@ class Gargor
154
237
  def evaluate &block
155
238
  @@evaluate_proc = block
156
239
  end
240
+
241
+ def logger *args, &block
242
+ file = args.shift
243
+ @@logger = Logger.new(Gargor.logfile(file),*args)
244
+ block.call(@@logger) if block
245
+ end
246
+
157
247
  end
@@ -15,57 +15,83 @@ class Gargor
15
15
  [@params,@fitness].to_s
16
16
  end
17
17
 
18
+ def log message,level = Logger::INFO
19
+ Gargor.log message,level
20
+ end
21
+
18
22
  def load_now
23
+ log "==> load current json"
19
24
  @params.each { |name,param|
20
- json = File.open(param.file).read
25
+ json = File.read(param.file)
21
26
  @params[name].value = JsonPath.on(json,param.path).first
22
27
  }
23
28
  self
24
29
  end
25
30
 
31
+ def set_param param,json
32
+ JSON.pretty_generate(JsonPath.for(json).gsub(param.path) { |v| param.value }.to_hash)
33
+ end
34
+
26
35
  def set_params
27
- puts "==> set params"
28
- puts @params
36
+ log "==> set params"
37
+ jsons = {}
29
38
  @params.each { |name,param|
30
- json = File.open(param.file).read
31
- json = JSON.pretty_generate(JsonPath.for(json).gsub(param.path) { |v| param.value }.to_hash)
32
- # 書き出し
33
- File.open(param.file,"w") { |f| f.write(json) }
34
- puts " write #{param.file}"
39
+ unless jsons.has_key?(param.file)
40
+ log "load #{param.file}"
41
+ jsons[param.file] = File.read(param.file)
42
+ end
43
+ jsons[param.file] = set_param(param,jsons[param.file])
44
+ log " #{name}: #{param}"
45
+ }
46
+ jsons.each { |file,json|
47
+ File.open(file,"w") { |f| f.write(json) }
48
+ log " write #{file}"
35
49
  }
36
50
  end
37
51
 
38
52
  def deploy
39
- ret = true
40
53
  Gargor.opt("target_nodes").each { |node|
41
- puts "==> deploy to #{node}"
54
+ log "==> deploy to #{node}"
42
55
  cmd = Gargor.opt("target_cooking_cmd") % [node]
43
- puts " #{cmd}"
44
- r = system(cmd)
45
- unless r
46
- puts "deploy failed"
56
+ log " #{cmd}"
57
+ out,r = shell(cmd)
58
+ unless r == 0
59
+ log "deploy failed",Logger::ERROR
47
60
  @fitness = 0
48
61
  sleep 1
62
+ return false
49
63
  end
50
- ret &= r
51
64
  }
52
- ret
65
+ true
53
66
  end
54
67
 
68
+ def shell command
69
+ out = `#{command}`
70
+ ret = $?
71
+ [out,ret.exitstatus]
72
+ end
55
73
 
56
74
  def attack
57
75
  ret = nil;out = nil
58
76
  cmd = Gargor.opt('attack_cmd')
59
- puts "==> attack"
60
- puts "execute: #{cmd}"
77
+ log "==> attack"
78
+ log "execute: #{cmd}"
61
79
  tms = Benchmark.realtime do
62
- out = `#{cmd}`
63
- ret = $?
80
+ out,ret = shell(cmd)
64
81
  end
65
82
 
66
- @fitness = Gargor.opt('evaluate_proc').call(ret.to_i,out,tms)
67
- puts "fitness: #{@fitness}"
83
+ @fitness = Gargor.opt('evaluate_proc').call(ret,out,tms)
84
+ log "fitness: #{@fitness}"
68
85
  @fitness
69
86
  end
87
+
88
+ # a/b の確率でiを上書きする
89
+ def overwrite_by i,a,b
90
+ @params.each { |name,param|
91
+ @params[name] = i.params[name] if a > Gargor.float_rand(b)
92
+ }
93
+ self
94
+ end
95
+
70
96
  end
71
97
  end
@@ -9,6 +9,7 @@ class Gargor
9
9
  def initialize name
10
10
  @name = name
11
11
  end
12
+
12
13
  def to_s
13
14
  @value.to_s
14
15
  end
@@ -1,4 +1,4 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  class Gargor
3
- VERSION = "0.0.3"
3
+ VERSION=File.read(File.expand_path(File.join(File.dirname(__FILE__),"..","..","VERSION")))
4
4
  end
@@ -0,0 +1,85 @@
1
+ # -*- coding: utf-8 -*-
2
+ # generations: set > 1
3
+ max_generations 10
4
+
5
+ # individuals number of some generation.
6
+ population 10
7
+
8
+ # elite number of some generation.(carried over)
9
+ elite 1
10
+
11
+ # Probability of mutation set "0.01" to "1%" (when crossover)
12
+ mutation 0.01
13
+
14
+ # target cook command : '%s' will replace by node name.
15
+ target_cooking_cmd "knife solo cook %s"
16
+
17
+ # target nodes
18
+ # performing target_cooking_command before the attack.
19
+ target_nodes ["www-1.example","www-2.example","db-1.example"]
20
+
21
+ # attack command
22
+ attack_cmd "ssh attacker.example ./bin/ghakai www-1.example.yml 2>/dev/null"
23
+
24
+ # logging
25
+ logger("gargor.log") do |logger|
26
+ logger.level = Logger::INFO
27
+ end
28
+
29
+ # evalute of the attack
30
+ # code: exit code of attack_cmd command (0 => succees)
31
+ # out: standard output of attack_cmd command
32
+ # time: execute time of attack_cmd
33
+ evaluate do |code,out,time|
34
+ fitness = 0
35
+ # get "FAILED" count from stadard output of stress-tool,
36
+ # and set fitess to 0 when FAILED > 0.
37
+ if time > 0 && code == 0 && /^FAILED (\d+)/ =~ out && $1 == "0"
38
+ # get fitness from stadard output of stress-tool.
39
+ # e.g.: request count:200, concurrenry:20, 45.060816 req/s
40
+ if /, ([\.\d]+) req\/s/ =~ out
41
+ fitness = $1.to_f
42
+ end
43
+ # To get fitness simply,to use execution time
44
+ # fitness = 1/time
45
+ end
46
+ # This block must return the fitness.(integer or float)
47
+ fitness
48
+ end
49
+
50
+ # definition of parameters(GA)
51
+ #
52
+ # param _name_ do
53
+ # json_file: Chef parameter(JSON) file (such as nodes/*.json or roles/*.json)
54
+ # (Warning!) gargor will overwrite their json files.
55
+ # json_path: to locate the value by JSONPath
56
+ # mutaion: to set value when mutaion
57
+ param "max_clients" do
58
+ json_file "roles/www.json"
59
+ json_path '$.httpd.max_clients'
60
+ mutation rand(500)+10
61
+ end
62
+
63
+ param "innodb_log_file_size" do
64
+ json_file "nodes/db-1.example.json"
65
+ json_path '$.mysqld.innodb_log_file_size'
66
+ mutation rand(200)
67
+ end
68
+
69
+ param "sort_buffer_size" do
70
+ json_file "nodes/db-1.example.json"
71
+ json_path '$.mysqld.sort_buffer_size'
72
+ mutation rand(1000)
73
+ end
74
+
75
+ param "read_buffer_size" do
76
+ json_file "nodes/db-1.example.json"
77
+ json_path '$.mysqld.read_buffer_size'
78
+ mutation rand(1000)
79
+ end
80
+
81
+ param "query_cache_size" do
82
+ json_file "nodes/db-1.example.json"
83
+ json_path '$.mysqld.query_cache_size'
84
+ mutation rand(100)
85
+ end
@@ -0,0 +1,57 @@
1
+ require 'helper'
2
+
3
+ describe Gargor::Individual,"#load_now" do
4
+ it "returns self" do
5
+ expect(Gargor::Individual.new.load_now).to be_kind_of Gargor::Individual
6
+ end
7
+ end
8
+
9
+ describe Gargor::Individual, "#set_params" do
10
+
11
+ end
12
+
13
+ describe Gargor::Individual, "#deploy" do
14
+ before do
15
+ to_load_fixture "sample-1.rb"
16
+ Gargor.start
17
+ Gargor.load_dsl("dummy")
18
+
19
+ @i = Gargor.mutate
20
+ end
21
+
22
+ it "build collect command-line" do
23
+ @i.stub(:shell) { |cmd|
24
+ expect(cmd).to match /knife solo cook (www-1|www-2|db-1).example/
25
+ ["",0]
26
+ }
27
+ expect(@i.deploy).to be true
28
+ end
29
+
30
+ it "returns false if deploy failed" do
31
+ @i.stub(:shell) { |cmd|
32
+ ["",255]
33
+ }
34
+ expect(@i.deploy).to be false
35
+
36
+ end
37
+ end
38
+
39
+ describe Gargor::Individual, "attack" do
40
+ it "build collect attack command" do
41
+ to_load_fixture "sample-1.rb"
42
+ Gargor.start
43
+ Gargor.load_dsl("dummy")
44
+ i = Gargor.mutate
45
+ i.stub(:shell) { |cmd|
46
+ expect(cmd).to eq Gargor.opt("attack_cmd")
47
+ out=<<EOF
48
+ abc, 3300 req/s
49
+ FAILED 0
50
+ EOF
51
+ [out,0]
52
+ }
53
+ i.attack
54
+ expect(i.fitness).to eq 3300.0
55
+
56
+ end
57
+ end
@@ -0,0 +1,183 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'helper'
3
+
4
+ describe Gargor,".start" do
5
+ it "must initialize attributes" do
6
+ expect(Gargor.start).to eq true
7
+ end
8
+ end
9
+
10
+ describe Gargor,".load_dsl" do
11
+ it "must raise RuntimeError when load from file with population 0" do
12
+ to_load_contents ""
13
+ Gargor.start
14
+ expect {
15
+ Gargor.load_dsl("dummy")
16
+ }.to raise_error(RuntimeError)
17
+ end
18
+
19
+ it "must load DSL successfully from sample-1.rb file" do
20
+ to_load_fixture "sample-1.rb"
21
+ Gargor.start
22
+ expect(Gargor.load_dsl("dummy")).to be true
23
+ expect(Gargor.params["population"]).to be 10
24
+
25
+ to_load_contents "population 100"
26
+ expect(Gargor.load_dsl("dummy")).to be true
27
+ expect(Gargor.params["population"]).to be 100
28
+ end
29
+
30
+ it "must raise NoMethodError when unknown command" do
31
+ Gargor.start
32
+ to_load_contents "hoge 100"
33
+ expect {
34
+ Gargor.load_dsl("dummy")
35
+ }.to raise_error(NoMethodError)
36
+ end
37
+
38
+ end
39
+
40
+ describe Gargor,".mutation" do
41
+ it "must create individual" do
42
+ to_load_fixture "sample-1.rb"
43
+ Gargor.start
44
+ Gargor.load_dsl("dummy")
45
+ expect(Gargor.mutate).to be_kind_of Gargor::Individual
46
+ end
47
+ end
48
+
49
+ describe Gargor,".poplutate" do
50
+ it "must create individuals" do
51
+ to_load_fixture "sample-1.rb"
52
+ Gargor.start
53
+ Gargor.load_dsl("dummy")
54
+ to_load_contents("{}") # dummy json
55
+ expect(Gargor.populate.size).to be Gargor.params["population"]
56
+ expect(Gargor.opt("generation")).to be 1
57
+ expect(Gargor.next_generation).to be true
58
+ expect(Gargor.opt("generation")).to be 2
59
+
60
+ expect{
61
+ Gargor.populate
62
+ }.to raise_error RuntimeError
63
+ end
64
+ end
65
+
66
+ describe Gargor,".float_rand" do
67
+ it "returns 0...max" do
68
+ Gargor.stub(:rand) { |max| max/2.0 }
69
+ expect(Gargor.float_rand(0.1,100)).to eq 0.05
70
+ end
71
+
72
+ it "raise RuntimeError unless max > 0" do
73
+ expect {
74
+ Gargor.float_rand(0)}.to raise_error RuntimeError
75
+ expect {
76
+ Gargor.float_rand(-1.2)}.to raise_error RuntimeError
77
+ end
78
+ end
79
+
80
+ describe Gargor,".crossover" do
81
+ it "crossoveres two Gargor::Individual objects" do
82
+ to_load_fixture "sample-1.rb"
83
+ Gargor.start
84
+ Gargor.load_dsl("dummy")
85
+ a = Gargor.mutate; a.fitness = 0.4
86
+ b = Gargor.mutate; b.fitness = 0.6
87
+
88
+ expect(Gargor.crossover(a,b)).to be_kind_of Gargor::Individual
89
+ end
90
+ end
91
+
92
+ describe Gargor,".selection" do
93
+ it "does something" do
94
+ to_load_fixture "sample-1.rb"
95
+ Gargor.start
96
+ Gargor.load_dsl("dummy")
97
+ a = Gargor.mutate; a.fitness = 0.4
98
+ b = Gargor.mutate; b.fitness = 0.6
99
+ g = Gargor::Individuals.new
100
+ g << a << b
101
+ expect(Gargor.selection g).to be_kind_of Gargor::Individual
102
+ end
103
+ end
104
+
105
+
106
+ describe Gargor, ".validate" do
107
+ it "raise Gargor::GargorError unless population > 0" do
108
+ Gargor.start
109
+ expect{
110
+ Gargor.validate
111
+ }.to raise_error Gargor::ValidationError
112
+ end
113
+ end
114
+
115
+ describe Gargor, ".first_generation?" do
116
+ it "returns true if not .next_generation called" do
117
+ Gargor.start
118
+ expect(Gargor.first_generation?).to be true
119
+ Gargor.next_generation
120
+ expect(Gargor.first_generation?).to be false
121
+ end
122
+ end
123
+
124
+ describe Gargor, ".prev_count" do
125
+ it "returns previous generation count" do
126
+ g = Gargor::Individuals.new
127
+ a = Gargor.mutate; a.fitness = 0.4
128
+ g << a
129
+ expect(Gargor.prev_count(g)).to be 1
130
+
131
+ b = Gargor.mutate; b.fitness = 0 # not fit for this environment
132
+ c = Gargor.mutate; c.fitness = 0.6
133
+ g << b << c
134
+ expect(Gargor.prev_count(g)).to be 2
135
+ end
136
+ end
137
+
138
+ describe Gargor, ".select_elites" do
139
+ it "returns Gargor::Individuals" do
140
+ g = Gargor::Individuals.new
141
+ 3.times { g << Gargor.mutate }
142
+
143
+ Gargor.start
144
+ gg = Gargor.select_elites(g,2)
145
+ expect(gg).to be_kind_of Gargor::Individuals
146
+ expect(gg.count).to be 2
147
+ end
148
+
149
+ it "returns count of elites" do
150
+ g = Gargor::Individuals.new
151
+ 3.times { g << Gargor.mutate }
152
+ Gargor.start
153
+ expect(Gargor.select_elites(g,3).count).to be 3
154
+ expect(Gargor.select_elites(g,4).count).to be 3
155
+ end
156
+ end
157
+
158
+ describe Gargor, ".mutaion?" do
159
+ it "returns true by mutation probability" do
160
+ Gargor.stub(:rand).and_return(0.05,0.15)
161
+ expect(Gargor.mutation?(0.1)).to be true
162
+ expect(Gargor.mutation?(0.1)).to be false
163
+ end
164
+ end
165
+
166
+ describe Gargor, ".logfile" do
167
+ it "return log full-path" do
168
+ to_load_fixture "sample-1.rb"
169
+ Gargor.start
170
+ Gargor.load_dsl("/tmp/test.rb")
171
+ expect(Gargor.logfile("gargor.log")).to eq "/tmp/gargor.log"
172
+ end
173
+ end
174
+
175
+ describe Gargor, ".logger" do
176
+ it "set Logger object to @@logger " do
177
+ to_load_fixture "sample-1.rb"
178
+ Gargor.start
179
+ Gargor.load_dsl("/tmp/test.rb")
180
+ expect(Gargor.opt("logger")).to be_kind_of Logger
181
+ expect(Gargor.opt("logger").level).to be Logger::INFO
182
+ end
183
+ end
@@ -0,0 +1,7 @@
1
+ require 'helper'
2
+
3
+ describe Gargor,"VERSION" do
4
+ it "must be {major}.{minor}.{patch}" do
5
+ expect(Gargor::VERSION).to match /\d+\.\d+\.\d+/
6
+ end
7
+ end
@@ -0,0 +1,30 @@
1
+ # -*- coding: utf-8 -*-
2
+ $TESTING=true
3
+
4
+ require 'simplecov'
5
+ require 'coveralls'
6
+
7
+ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
8
+ SimpleCov::Formatter::HTMLFormatter,
9
+ Coveralls::SimpleCov::Formatter
10
+ ]
11
+ SimpleCov.start
12
+
13
+ $:.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
14
+ require 'gargor'
15
+ require 'rspec'
16
+ RSpec.configure do |config|
17
+ end
18
+
19
+ def load_fixture name
20
+ file = File.join(File.dirname(__FILE__), "fixtures",name)
21
+ File.open(file) { |f| f.read }
22
+ end
23
+
24
+ def to_load_fixture name
25
+ File.stub(:read).and_return(load_fixture(name))
26
+ end
27
+
28
+ def to_load_contents text
29
+ File.stub(:read).and_return(text)
30
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gargor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-06-18 00:00:00.000000000 Z
12
+ date: 2013-07-03 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -53,17 +53,26 @@ extensions: []
53
53
  extra_rdoc_files: []
54
54
  files:
55
55
  - .gitignore
56
+ - .travis.yml
56
57
  - Gemfile
57
58
  - LICENSE.txt
58
59
  - README.md
59
60
  - README_ja.md
60
61
  - Rakefile
62
+ - VERSION
61
63
  - bin/gargor
64
+ - doc/howto.md
65
+ - doc/sample.rb
62
66
  - gargor.gemspec
63
67
  - lib/gargor.rb
64
68
  - lib/gargor/individual.rb
65
69
  - lib/gargor/parameter.rb
66
70
  - lib/gargor/version.rb
71
+ - spec/fixtures/sample-1.rb
72
+ - spec/gargor_individual_spec.rb
73
+ - spec/gargor_spec.rb
74
+ - spec/gargor_version_spec.rb
75
+ - spec/helper.rb
67
76
  homepage: https://github.com/tumf/gargor
68
77
  licenses:
69
78
  - MIT
@@ -90,4 +99,9 @@ signing_key:
90
99
  specification_version: 3
91
100
  summary: It is software which uses generic algorithm to support parameter tuning of
92
101
  the servers controlled by Chef.
93
- test_files: []
102
+ test_files:
103
+ - spec/fixtures/sample-1.rb
104
+ - spec/gargor_individual_spec.rb
105
+ - spec/gargor_spec.rb
106
+ - spec/gargor_version_spec.rb
107
+ - spec/helper.rb