sisfc 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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, opts)
6
- @dcid = id
7
- @capacity = opts[:maximum_vm_capacity]
8
- @available = @capacity.dup
9
- @vms = {}
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
- if @available[vm.size] > 0
16
- @available[vm.size] -= 1
17
- else
18
- raise 'Error! VM capacity exceeded!'
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
- unless @vms.has_key? component_name
25
- raise "Error! Service component type #{component_name} not present in data center #{@dcid}!"
26
- end
27
- unless @vms[component_name].include? vm
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
- unless @vms.has_key? component_name
36
- raise "Error! Service component type #{component_name} not present in data center #{@dcid}!"
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 get_number_of_vms(component_name)
42
- unless @vms.has_key? component_name
43
- raise "Error! Service component type #{component_name} not present in data center #{@dcid}!"
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 available(size)
49
- @available[size]
62
+ def private?
63
+ @type == :private
50
64
  end
51
65
 
52
- def to_s
53
- "Data center #{@dcid}, with VMs:" +
54
- @vms.inject("") {|s,(k,v)| s += " (#{k}: #{v.size})" }
66
+ def public?
67
+ @type == :public
55
68
  end
56
69
 
57
70
  end
@@ -1,35 +1,43 @@
1
- require 'sisfc/configuration'
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(kpis, dc_kpis, vm_allocation)
13
- # evaluate VM daily costs
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 violations
26
- penalties = (@penalties_func.nil? ? 0.0 : (@penalties_func.call(kpis, dc_kpis) or 0.0))
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
- # we want to minimize the cost, so we define fitness as -cost
32
- fitness = -cost
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
- ET_REQUEST_ARRIVAL = 0
6
- ET_WORKFLOW_STEP_COMPLETED = 1
7
- ET_VM_FREE = 2
8
- ET_VM_SUSPEND = 3
9
- ET_VM_RESUME = 4
10
- ET_REQUEST_CLOSURE = 5
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
@@ -1,21 +1,19 @@
1
- require 'csv'
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, 'Need to provide either a filename or a command!'
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.parse_csv
35
+ tokens = line.split(",") # should be faster than CSV parsing
38
36
  generation_time = tokens[0].to_f
39
- data_center_id = tokens[1].to_i
40
- arrival_time = tokens[2].to_f
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
- # sanity check
47
- if generation_time > arrival_time
48
- raise "Generation time (#{generation_time}) is larger than arrival time (#{arrival_time})!"
49
- end
50
-
51
- # generate and return request
52
- Request.new(@next_rid,
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
@@ -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
- module SISFC
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
- attr_accessor :rid, :generation_time, :data_center_id, :arrival_time,
10
- :workflow_type_id, :closure_time #, :status
11
- attr_reader :communication_latency
12
-
13
- def initialize(rid, generation_time, data_center_id, arrival_time, workflow_type_id)
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 = 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 count from zero
21
- @step = 0
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
- # consider initial communication latency
27
- @tracking_information = [
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 step_completed(start, duration)
53
- @tracking_information << {
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 finished_processing(time)
63
- # consider final communication latency
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 next_step
74
- @step
55
+ def step_completed(duration)
56
+ @working_time += duration
57
+ @next_step += 1
75
58
  end
76
59
 
77
- def with_tracking_information(type=:all)
78
- selected_ti = if type == :all
79
- @tracking_information
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