sisfc 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.projections.json +12 -0
- data/.travis.yml +6 -0
- data/README.md +37 -19
- data/Rakefile +4 -2
- data/TODO +5 -0
- data/bin/sisfc +29 -6
- data/examples/generator.R +3 -1
- data/examples/simulator.conf +80 -29
- data/lib/sisfc.rb +4 -4
- data/lib/sisfc/configuration.rb +73 -6
- data/lib/sisfc/data_center.rb +42 -29
- data/lib/sisfc/evaluation.rb +23 -15
- data/lib/sisfc/event.rb +9 -6
- data/lib/sisfc/generator.rb +14 -21
- data/lib/sisfc/latency_manager.rb +65 -0
- data/lib/sisfc/logger.rb +28 -0
- data/lib/sisfc/request.rb +42 -85
- data/lib/sisfc/service_type.rb +2 -0
- data/lib/sisfc/simulation.rb +234 -47
- data/lib/sisfc/sorted_array.rb +2 -0
- data/lib/sisfc/statistics.rb +37 -3
- data/lib/sisfc/support/dsl_helper.rb +2 -0
- data/lib/sisfc/version.rb +3 -1
- data/lib/sisfc/vm.rb +46 -27
- data/sisfc.gemspec +9 -5
- data/spec/minitest_helper.rb +9 -0
- data/{test/sisfc/configuration_test.rb → spec/sisfc/configuration_spec.rb} +4 -2
- data/spec/sisfc/data_center_spec.rb +19 -0
- data/{test/sisfc/evaluation_test.rb → spec/sisfc/evaluation_spec.rb} +5 -3
- data/{test/sisfc/generator_test.rb → spec/sisfc/generator_spec.rb} +21 -18
- data/spec/sisfc/latency_manager_spec.rb +13 -0
- data/spec/sisfc/reference_configuration.rb +534 -0
- data/spec/sisfc/request_spec.rb +19 -0
- metadata +115 -49
- data/test/sisfc/reference_configuration.rb +0 -191
- data/test/sisfc/request_test.rb +0 -13
- data/test/test_helper.rb +0 -4
data/lib/sisfc/data_center.rb
CHANGED
@@ -1,57 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
1
5
|
module SISFC
|
2
6
|
|
3
7
|
class DataCenter
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
def_delegator :@vms, :has_key?, :has_vms_of_type?
|
11
|
+
|
12
|
+
attr_reader :dcid, :location_id
|
4
13
|
|
5
|
-
def initialize(id
|
6
|
-
@dcid
|
7
|
-
@
|
8
|
-
@
|
9
|
-
@
|
14
|
+
def initialize(id:, location_id:, name:, type:, **opts)
|
15
|
+
@dcid = id
|
16
|
+
@location_id = location_id
|
17
|
+
@vms = {}
|
18
|
+
@vm_type_count = {}
|
19
|
+
@name = name
|
20
|
+
@type = type
|
21
|
+
raise ArgumentError, "Unsupported type!" unless [ :private, :public ].include?(@type)
|
22
|
+
@availability_check_proc = opts[:maximum_vm_capacity]
|
10
23
|
end
|
11
24
|
|
25
|
+
# returns false in case no more VMs can be allocated
|
12
26
|
def add_vm(vm, component_name)
|
13
27
|
@vms[component_name] ||= []
|
28
|
+
@vm_type_count[vm.size] ||= 0
|
29
|
+
|
14
30
|
raise 'Error! VM is already present!' if @vms[component_name].include? vm
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
31
|
+
|
32
|
+
# defer availablility check to user specified procedure
|
33
|
+
if @availability_check_proc
|
34
|
+
return false unless @availability_check_proc.call(@vm_type_count)
|
19
35
|
end
|
36
|
+
|
37
|
+
# allocate VM
|
20
38
|
@vms[component_name] << vm
|
39
|
+
@vm_type_count[vm.size] += 1
|
21
40
|
end
|
22
41
|
|
23
42
|
def remove_vm(vm, component_name)
|
24
|
-
|
25
|
-
raise
|
26
|
-
|
27
|
-
|
28
|
-
raise 'Error! VM not present!'
|
43
|
+
if @vms.has_key? component_name and @vms[component_name].include? vm
|
44
|
+
raise 'Error! Inconsistent number of VMs!' unless @vm_type_count[vm.size] >= 1
|
45
|
+
@vm_type_count[vm.size] += 1
|
46
|
+
@vms.delete(vm)
|
29
47
|
end
|
30
|
-
@available[vm.size] += 1
|
31
|
-
@vms.delete(vm)
|
32
48
|
end
|
33
49
|
|
50
|
+
# returns nil in case no VM for component component_name is running
|
34
51
|
def get_random_vm(component_name)
|
35
|
-
|
36
|
-
|
52
|
+
if @vms.has_key? component_name
|
53
|
+
@vms[component_name].sample
|
37
54
|
end
|
38
|
-
@vms[component_name].sample
|
39
55
|
end
|
40
56
|
|
41
|
-
def
|
42
|
-
|
43
|
-
|
44
|
-
end
|
45
|
-
@vms[component_name].size
|
57
|
+
def to_s
|
58
|
+
"Data center #{@dcid}, with VMs:" +
|
59
|
+
@vms.inject("") {|s,(k,v)| s += " (#{k}: #{v.size})" }
|
46
60
|
end
|
47
61
|
|
48
|
-
def
|
49
|
-
@
|
62
|
+
def private?
|
63
|
+
@type == :private
|
50
64
|
end
|
51
65
|
|
52
|
-
def
|
53
|
-
|
54
|
-
@vms.inject("") {|s,(k,v)| s += " (#{k}: #{v.size})" }
|
66
|
+
def public?
|
67
|
+
@type == :public
|
55
68
|
end
|
56
69
|
|
57
70
|
end
|
data/lib/sisfc/evaluation.rb
CHANGED
@@ -1,35 +1,43 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './logger'
|
2
4
|
|
3
5
|
module SISFC
|
4
6
|
class Evaluator
|
7
|
+
|
8
|
+
include Logging
|
9
|
+
|
5
10
|
def initialize(conf)
|
6
11
|
@vm_hourly_cost = conf.evaluation[:vm_hourly_cost]
|
7
12
|
raise ArgumentError, 'No VM hourly costs provided!' unless @vm_hourly_cost
|
8
13
|
|
14
|
+
@fixed_hourly_cost = conf.evaluation[:fixed_hourly_cost] || {}
|
15
|
+
|
9
16
|
@penalties_func = conf.evaluation[:penalties]
|
10
17
|
end
|
11
18
|
|
12
|
-
def evaluate_business_impact(
|
13
|
-
|
19
|
+
def evaluate_business_impact(all_kpis, per_workflow_and_customer_kpis,
|
20
|
+
vm_allocation)
|
21
|
+
# evaluate variable hourly costs related to VM allocation
|
14
22
|
cost = vm_allocation.inject(0.0) do |s,x|
|
15
23
|
hc = @vm_hourly_cost.find{|i| i[:data_center] == x[:dc_id] and i[:vm_type] == x[:vm_size] }
|
16
|
-
unless hc
|
17
|
-
raise "Cannot find hourly cost for data center #{x[:dc_id]} and VM size #{x[:vm_size]}!"
|
18
|
-
end
|
24
|
+
raise "Cannot find hourly cost for data center #{x[:dc_id]} and VM size #{x[:vm_size]}!" unless hc
|
19
25
|
s += x[:vm_num] * hc[:cost]
|
20
26
|
end
|
27
|
+
|
28
|
+
# evaluate fixed hourly costs (for private Cloud data centers)
|
29
|
+
@fixed_hourly_cost.values.each do |fixed_cost|
|
30
|
+
cost += fixed_cost
|
31
|
+
end
|
32
|
+
|
33
|
+
# calculate daily cost
|
21
34
|
cost *= 24.0
|
22
|
-
kpis[:cost] = cost
|
23
|
-
# puts "vm allocation cost: #{cost}"
|
24
35
|
|
25
|
-
# consider SLO
|
26
|
-
penalties = (@penalties_func.nil? ? 0.0 : (@penalties_func.call(
|
27
|
-
kpis[:penalties] = penalties
|
28
|
-
# puts "slo penalties cost: #{penalties}"
|
29
|
-
cost += penalties
|
36
|
+
# consider SLO violation penalties
|
37
|
+
penalties = (@penalties_func.nil? ? 0.0 : (@penalties_func.call(all_kpis, per_workflow_and_customer_kpis) or 0.0))
|
30
38
|
|
31
|
-
|
32
|
-
|
39
|
+
{ it_cost: cost,
|
40
|
+
penalties: penalties }
|
33
41
|
end
|
34
42
|
end
|
35
43
|
end
|
data/lib/sisfc/event.rb
CHANGED
@@ -1,13 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module SISFC
|
2
4
|
|
3
5
|
class Event
|
4
6
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
7
|
+
ET_REQUEST_GENERATION = 0
|
8
|
+
ET_REQUEST_ARRIVAL = 1
|
9
|
+
ET_REQUEST_FORWARDING = 2
|
10
|
+
ET_WORKFLOW_STEP_COMPLETED = 3
|
11
|
+
ET_REQUEST_CLOSURE = 4
|
12
|
+
# ET_VM_SUSPEND = 5
|
13
|
+
# ET_VM_RESUME = 6
|
11
14
|
ET_END_OF_SIMULATION = 100
|
12
15
|
|
13
16
|
# let the comparable mixin provide the < and > operators for us
|
data/lib/sisfc/generator.rb
CHANGED
@@ -1,21 +1,19 @@
|
|
1
|
-
|
2
|
-
require 'sisfc/request'
|
3
|
-
|
1
|
+
# frozen_string_literal: true
|
4
2
|
|
5
3
|
module SISFC
|
6
4
|
|
7
5
|
class RequestGenerator
|
8
6
|
|
9
|
-
def initialize(opts)
|
7
|
+
def initialize(opts={})
|
10
8
|
if opts.has_key? :filename
|
11
9
|
filename = opts[:filename]
|
12
|
-
raise "File #{filename} does not exist!" unless File.exists?(filename)
|
10
|
+
raise ArgumentError, "File #{filename} does not exist!" unless File.exists?(filename)
|
13
11
|
@file = File.open(filename, mode='r')
|
14
12
|
elsif opts.has_key? :command
|
15
13
|
command = opts[:command]
|
16
14
|
@file = IO.popen(command)
|
17
15
|
else
|
18
|
-
raise ArgumentError,
|
16
|
+
raise ArgumentError, "Need to provide either a filename or a command!"
|
19
17
|
end
|
20
18
|
|
21
19
|
# throw away the first line (containing the CSV headers)
|
@@ -34,26 +32,21 @@ module SISFC
|
|
34
32
|
raise "End of input reached while reading request #{@next_rid}!" unless line
|
35
33
|
|
36
34
|
# parse data
|
37
|
-
tokens = line.
|
35
|
+
tokens = line.split(",") # should be faster than CSV parsing
|
38
36
|
generation_time = tokens[0].to_f
|
39
|
-
|
40
|
-
|
41
|
-
workflow_type_id = tokens[3].to_i
|
37
|
+
workflow_type_id = tokens[1].to_i
|
38
|
+
customer_id = tokens[2].to_i
|
42
39
|
|
43
40
|
# increase @next_rid
|
44
41
|
@next_rid += 1
|
45
42
|
|
46
|
-
#
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
generation_time,
|
54
|
-
data_center_id,
|
55
|
-
arrival_time,
|
56
|
-
workflow_type_id)
|
43
|
+
# return request
|
44
|
+
{
|
45
|
+
rid: @next_rid,
|
46
|
+
generation_time: generation_time,
|
47
|
+
workflow_type_id: workflow_type_id,
|
48
|
+
customer_id: customer_id,
|
49
|
+
}
|
57
50
|
end
|
58
51
|
|
59
52
|
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'erv'
|
4
|
+
|
5
|
+
module SISFC
|
6
|
+
class LatencyManager
|
7
|
+
def initialize(latency_models)
|
8
|
+
# here we build a (strictly upper triangular) matrix of random variables
|
9
|
+
# that represent the communication latency models between the different
|
10
|
+
# locations
|
11
|
+
@latency_models_matrix = latency_models.map do |lms_conf|
|
12
|
+
lms_conf.map do |rv_conf|
|
13
|
+
rv_conf
|
14
|
+
ERV::RandomVariable.new(rv_conf)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# should we turn @latency_models_matrix from a (strictly upper)
|
19
|
+
# triangular to a full matrix, for convenience? probably not. it would
|
20
|
+
# require more memory and: 1) ruby is ***very*** memory hungry already,
|
21
|
+
# 2) ruby's performance is very sensitive to memory usage.
|
22
|
+
|
23
|
+
# precalculate average latencies
|
24
|
+
@average_latency_matrix = @latency_models_matrix.map do |lms|
|
25
|
+
lms.map{|x| x.mean }
|
26
|
+
end
|
27
|
+
|
28
|
+
# latency in the same location is implemented as a truncated gaussian
|
29
|
+
# with mean = 20ms, sd = 5ms, and a = 2ms
|
30
|
+
@same_location_latency = ERV::RandomVariable.new(distribution: :gaussian, args: { mean: 20E-3, sd: 5E-3 })
|
31
|
+
end
|
32
|
+
|
33
|
+
def sample_latency_between(loc1, loc2)
|
34
|
+
if loc1 == loc2
|
35
|
+
# rejection sampling to implement (crudely) PDF truncation
|
36
|
+
while (lat = @same_location_latency.next) < 2E-3; end
|
37
|
+
lat
|
38
|
+
else
|
39
|
+
l1, l2 = loc1 < loc2 ? [ loc1, loc2 ] : [ loc2, loc1 ]
|
40
|
+
|
41
|
+
# since we use a compact representation for @latency_models_matrix, the
|
42
|
+
# indexes become l1 and (l2-l1-1)
|
43
|
+
# rejection sampling to implement (crudely) PDF truncation to positive numbers
|
44
|
+
while (lat = @latency_models_matrix[l1][l2-l1-1].next) <= 0.0; end
|
45
|
+
lat
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def average_latency_between(loc1, loc2)
|
50
|
+
# the results returned by this method are not entirely accurate, because
|
51
|
+
# rejection sampling changes the shape of the PDF. see, e.g.,
|
52
|
+
# https://stackoverflow.com/questions/47933019/how-to-properly-sample-truncated-distributions
|
53
|
+
# still, it is an acceptable approximation
|
54
|
+
if loc1 == loc2
|
55
|
+
@same_location_latency.mean
|
56
|
+
else
|
57
|
+
l1, l2 = loc1 < loc2 ? [ loc1, loc2 ] : [ loc2, loc1 ]
|
58
|
+
|
59
|
+
# since we use a compact representation for @average_latency_between, the
|
60
|
+
# indexes become l1 and (l2-l1-1)
|
61
|
+
@average_latency_between[l1][l2-l1-1]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/lib/sisfc/logger.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
|
6
|
+
module SISFC
|
7
|
+
module Logging
|
8
|
+
class << self
|
9
|
+
def logger
|
10
|
+
@logger ||= ::Logger.new(STDERR).tap{|l| l.level = ::Logger::INFO }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.included(base)
|
15
|
+
class << base
|
16
|
+
# this version of the logger method will be called from class methods
|
17
|
+
def logger
|
18
|
+
Logging.logger
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# this version of the logger method will be called from instance methods
|
24
|
+
def logger
|
25
|
+
Logging.logger
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/sisfc/request.rb
CHANGED
@@ -1,101 +1,65 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
+
module SISFC
|
3
4
|
class Request
|
4
5
|
|
5
|
-
# states
|
6
|
-
STATE_WORKING = 1
|
7
|
-
STATE_SUSPENDED = 2
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
6
|
+
# # states
|
7
|
+
# STATE_WORKING = 1
|
8
|
+
# STATE_SUSPENDED = 2
|
9
|
+
|
10
|
+
attr_reader :rid,
|
11
|
+
:arrival_time,
|
12
|
+
:closure_time,
|
13
|
+
# :communication_latency,
|
14
|
+
:customer_id,
|
15
|
+
:generation_time,
|
16
|
+
:next_step,
|
17
|
+
# :status,
|
18
|
+
:workflow_type_id
|
19
|
+
|
20
|
+
# the data_center_id attribute is updated as requests move from a Cloud
|
21
|
+
# data center to another
|
22
|
+
attr_accessor :data_center_id
|
23
|
+
|
24
|
+
def initialize(rid:,
|
25
|
+
generation_time:,
|
26
|
+
initial_data_center_id:,
|
27
|
+
arrival_time:,
|
28
|
+
workflow_type_id:,
|
29
|
+
customer_id:)
|
14
30
|
@rid = rid
|
15
31
|
@generation_time = generation_time
|
16
|
-
@data_center_id =
|
32
|
+
@data_center_id = initial_data_center_id
|
17
33
|
@arrival_time = arrival_time
|
18
34
|
@workflow_type_id = workflow_type_id
|
35
|
+
@customer_id = customer_id
|
19
36
|
|
20
|
-
# steps
|
21
|
-
@
|
37
|
+
# steps start counting from zero
|
38
|
+
@next_step = 0
|
22
39
|
|
23
40
|
# calculate communication latency
|
24
41
|
@communication_latency = @arrival_time - @generation_time
|
25
42
|
|
26
|
-
|
27
|
-
@
|
28
|
-
{
|
29
|
-
:type => :communication,
|
30
|
-
:at => @generation_time,
|
31
|
-
:duration => @communication_latency,
|
32
|
-
}
|
33
|
-
]
|
34
|
-
|
35
|
-
# NOTE: the format for each element of the @tracking_information array is:
|
36
|
-
# { :type => one of [ :queue, :work, :communication ]
|
37
|
-
# :at => begin time
|
38
|
-
# :duration => duration
|
39
|
-
# :vm => vm (optional)
|
40
|
-
# }
|
41
|
-
end
|
42
|
-
|
43
|
-
def queuing_completed(start, duration)
|
44
|
-
@tracking_information << {
|
45
|
-
:type => :queue,
|
46
|
-
:at => start,
|
47
|
-
:duration => duration,
|
48
|
-
# :vm =>
|
49
|
-
}
|
43
|
+
@queuing_time = 0.0
|
44
|
+
@working_time = 0.0
|
50
45
|
end
|
51
46
|
|
52
|
-
def
|
53
|
-
@
|
54
|
-
:type => :work,
|
55
|
-
:at => start,
|
56
|
-
:duration => duration,
|
57
|
-
# :vm =>
|
58
|
-
}
|
59
|
-
@step += 1
|
47
|
+
def update_queuing_time(duration)
|
48
|
+
@queuing_time += duration
|
60
49
|
end
|
61
50
|
|
62
|
-
def
|
63
|
-
|
64
|
-
@tracking_information << {
|
65
|
-
:type => :communication,
|
66
|
-
:at => time,
|
67
|
-
:duration => @communication_latency,
|
68
|
-
}
|
69
|
-
# save closure time
|
70
|
-
@closure_time = time + @communication_latency
|
51
|
+
def update_transfer_time(duration)
|
52
|
+
@communication_latency += duration
|
71
53
|
end
|
72
54
|
|
73
|
-
def
|
74
|
-
@
|
55
|
+
def step_completed(duration)
|
56
|
+
@working_time += duration
|
57
|
+
@next_step += 1
|
75
58
|
end
|
76
59
|
|
77
|
-
def
|
78
|
-
|
79
|
-
|
80
|
-
else
|
81
|
-
@tracking_information.select{|el| el.type == type }
|
82
|
-
end
|
83
|
-
|
84
|
-
selected_ti.each do |ti|
|
85
|
-
yield ti
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
def total_communication_time
|
90
|
-
calculate_time(:communication)
|
91
|
-
end
|
92
|
-
|
93
|
-
def total_work_time
|
94
|
-
calculate_time(:work)
|
95
|
-
end
|
96
|
-
|
97
|
-
def total_queue_time
|
98
|
-
calculate_time(:queue)
|
60
|
+
def finished_processing(time)
|
61
|
+
# save closure time
|
62
|
+
@closure_time = time
|
99
63
|
end
|
100
64
|
|
101
65
|
def closed?
|
@@ -110,13 +74,6 @@ module SISFC
|
|
110
74
|
def to_s
|
111
75
|
"rid: #{@rid}, generation_time: #{@generation_time}, data_center_id: #{@data_center_id}, arrival_time: #{@arrival_time}"
|
112
76
|
end
|
113
|
-
|
114
|
-
private
|
115
|
-
|
116
|
-
def calculate_time(type)
|
117
|
-
return 0 unless @tracking_information
|
118
|
-
@tracking_information.inject(0) {|sum,x| sum += ((type == :all || type == x[:type]) ? x[:duration].to_i : 0) }
|
119
|
-
end
|
120
77
|
end
|
121
78
|
|
122
79
|
end
|