sisfc 0.1.0 → 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.
@@ -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