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,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'erv'
2
4
 
3
5
  module SISFC
@@ -1,9 +1,12 @@
1
- require 'sisfc/data_center'
2
- require 'sisfc/event'
3
- require 'sisfc/generator'
4
- require 'sisfc/sorted_array'
5
- require 'sisfc/statistics'
6
- require 'sisfc/vm'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './data_center'
4
+ require_relative './event'
5
+ require_relative './generator'
6
+ require_relative './sorted_array'
7
+ require_relative './statistics'
8
+ require_relative './vm'
9
+ require_relative './latency_manager'
7
10
 
8
11
 
9
12
  module SISFC
@@ -15,6 +18,9 @@ module SISFC
15
18
  def initialize(opts = {})
16
19
  @configuration = opts[:configuration]
17
20
  @evaluator = opts[:evaluator]
21
+
22
+ # create latency manager
23
+ @latency_manager = LatencyManager.new(@configuration.latency_models)
18
24
  end
19
25
 
20
26
 
@@ -33,28 +39,63 @@ module SISFC
33
39
  # setup simulation start and current time
34
40
  @current_time = @start_time = @configuration.start_time
35
41
 
36
- # create data centers
37
- data_centers = @configuration.data_centers.map {|k,v| DataCenter.new(k,v) }
42
+ # create data centers and store them in a repository
43
+ data_center_repository = Hash[
44
+ @configuration.data_centers.map do |k,v|
45
+ [ k, DataCenter.new(id: k, **v) ]
46
+ end
47
+ ]
48
+
49
+ customer_repository = @configuration.customers
50
+ workflow_type_repository = @configuration.workflow_types
38
51
 
39
52
  # initialize statistics
40
- stats = Statistics.new
41
- dc_stats = data_centers.map {|k,v| Statistics.new }
53
+ stats = Statistics.new
54
+ per_workflow_and_customer_stats = Hash[
55
+ workflow_type_repository.keys.map do |wft_id|
56
+ [
57
+ wft_id,
58
+ Hash[
59
+ customer_repository.keys.map do |c_id|
60
+ [ c_id, Statistics.new(@configuration.custom_stats.find{|x| x[:customer_id] == c_id && x[:workflow_type_id] == wft_id } || {}) ]
61
+ end
62
+ ]
63
+ ]
64
+ end
65
+ ]
66
+ reqs_received_per_workflow_and_customer = Hash[
67
+ workflow_type_repository.keys.map do |wft_id|
68
+ [ wft_id, Hash[customer_repository.keys.map {|c_id| [ c_id, 0 ]}] ]
69
+ end
70
+ ]
42
71
 
43
72
  # create VMs
44
73
  @vms = []
45
74
  vmid = 0
46
75
  vm_allocation.each do |opts|
47
76
  # setup service_time_distribution
48
- opts[:service_time_distribution] = @configuration.service_component_types[opts[:component_type]][:service_time_distribution]
77
+ stdist = @configuration.service_component_types[opts[:component_type]][:service_time_distribution]
49
78
 
50
79
  # allocate the VMs
51
80
  opts[:vm_num].times do
52
81
  # create VM ...
53
- vm = VM.new(vmid, opts) # vm = VM.new(vmid, opts.except(:vm_num))
82
+ # if [ "Financial Transaction Server", "RDBMS C", "Queue Manager" ].include? opts[:component_type]
83
+ # vm = VM.new(vmid, opts[:dc_id], opts[:vm_size], stdist, trace: true, notes: "ct #{opts[:component_type]}")
84
+ # else
85
+ # vm = VM.new(vmid, opts[:dc_id], opts[:vm_size], stdist)
86
+ # end
87
+ vm = VM.new(vmid, opts[:dc_id], opts[:vm_size], stdist)
54
88
  # ... add it to the vm list ...
55
89
  @vms << vm
56
90
  # ... and register it in the corresponding data center
57
- data_centers[opts[:dc_id]-1].add_vm(vm, opts[:component_type])
91
+ unless data_center_repository[opts[:dc_id]].add_vm(vm, opts[:component_type])
92
+ $stderr.puts "====== Unfeasible allocation at data center #{dc_id} ======"
93
+ $stderr.flush
94
+ # here we return Float::MAX instead of, e.g., Float::INFINITY,
95
+ # because the latter would break optimization tools. instead, we
96
+ # want to have a very high but comparable value.
97
+ return Float::MAX
98
+ end
58
99
  # update vm id
59
100
  vmid += 1
60
101
  end
@@ -67,8 +108,8 @@ module SISFC
67
108
 
68
109
  # generate first request
69
110
  rg = RequestGenerator.new(@configuration.request_generation)
70
- new_req = rg.generate
71
- new_event(Event::ET_REQUEST_ARRIVAL, new_req, new_req.arrival_time, nil)
111
+ req_attrs = rg.generate
112
+ new_event(Event::ET_REQUEST_GENERATION, req_attrs, req_attrs[:generation_time], nil)
72
113
 
73
114
  # schedule end of simulation
74
115
  unless @configuration.end_time.nil?
@@ -80,54 +121,115 @@ module SISFC
80
121
  warmup_threshold = @configuration.start_time + @configuration.warmup_duration.to_i
81
122
 
82
123
  requests_being_worked_on = 0
83
- events = 0
124
+ requests_forwarded_to_other_dcs = 0
125
+ current_event = 0
84
126
 
85
127
  # launch simulation
86
128
  until @event_queue.empty?
87
129
  e = @event_queue.shift
88
130
 
89
- events += 1
131
+ current_event += 1
90
132
  # sanity check on simulation time flow
91
133
  if @current_time > e.time
92
- raise "Error: simulation time inconsistency for event #{events} " +
134
+ raise "Error: simulation time inconsistency for event #{current_event} " +
93
135
  "e.type=#{e.type} @current_time=#{@current_time}, e.time=#{e.time}"
94
136
  end
95
137
 
96
138
  @current_time = e.time
97
139
 
98
140
  case e.type
141
+ when Event::ET_REQUEST_GENERATION
142
+ req_attrs = e.data
143
+
144
+ # find closest data center
145
+ customer_location_id = customer_repository.dig(req_attrs[:customer_id], :location_id)
146
+ dc_at_customer_location = data_center_repository.values.find {|dc| dc.location_id == customer_location_id }
147
+
148
+ raise "No data center found at location id #{customer_location_id}!" unless dc_at_customer_location
149
+
150
+ # find first component name for requested workflow
151
+ workflow = workflow_type_repository[req_attrs[:workflow_type_id]]
152
+ first_component_name = workflow[:component_sequence][0][:name]
153
+
154
+ closest_dc = if dc_at_customer_location.has_vms_of_type?(first_component_name)
155
+ dc_at_customer_location
156
+ else
157
+ data_center_repository.values.select{|dc| dc.has_vms_of_type?(first_component_name) }&.sample
158
+ end
159
+
160
+ raise "Invalid configuration! No VMs of type #{first_component_name} found!" unless closest_dc
161
+
162
+ arrival_time = @current_time + @latency_manager.sample_latency_between(customer_location_id, closest_dc.location_id)
163
+ new_req = Request.new(req_attrs.merge!(initial_data_center_id: closest_dc.dcid,
164
+ arrival_time: arrival_time))
165
+
166
+ # schedule arrival of current request
167
+ new_event(Event::ET_REQUEST_ARRIVAL, new_req, arrival_time, nil)
168
+
169
+ # schedule generation of next request
170
+ req_attrs = rg.generate
171
+ new_event(Event::ET_REQUEST_GENERATION, req_attrs, req_attrs[:generation_time], nil)
172
+
99
173
  when Event::ET_REQUEST_ARRIVAL
100
174
  # get request
101
175
  req = e.data
102
176
 
103
177
  # find data center
104
- data_center = data_centers[req.data_center_id-1]
178
+ data_center = data_center_repository[req.data_center_id]
179
+
180
+ # update reqs_received_per_workflow_and_customer
181
+ reqs_received_per_workflow_and_customer[req.workflow_type_id][req.customer_id] += 1
105
182
 
106
183
  # find next component name
107
- workflow = @configuration.workflow_types[req.workflow_type_id]
184
+ workflow = workflow_type_repository[req.workflow_type_id]
108
185
  next_component_name = workflow[:component_sequence][req.next_step][:name]
186
+ # if [5,6,9].include? req.workflow_type_id
187
+ # $stderr.puts "received request for wf #{req.workflow_type_id} at dc #{req.data_center_id}"
188
+ # $stderr.puts "next component name is #{next_component_name}"
189
+ # $stderr.flush
190
+ # end
109
191
 
110
192
  # get random vm providing next service component type
111
193
  vm = data_center.get_random_vm(next_component_name)
194
+ # if [5,6,9].include? req.workflow_type_id
195
+ # $stderr.puts "next vm is #{vm.__id__}"
196
+ # $stderr.flush
197
+ # end
112
198
 
113
- # forward request to the vm
114
- vm.new_request(self, req, e.time)
199
+ # schedule request forwarding to vm
200
+ new_event(Event::ET_REQUEST_FORWARDING, req, e.time, vm)
115
201
 
116
202
  # update stats
117
203
  if req.arrival_time > warmup_threshold
118
204
  # increase the number of requests being worked on
119
205
  requests_being_worked_on += 1
120
- end
121
206
 
122
- # generate next request
123
- new_req = rg.generate
124
- new_event(Event::ET_REQUEST_ARRIVAL, new_req, new_req.arrival_time, nil)
207
+ # increase count of received requests
208
+ stats.request_received
209
+
210
+ # increase count of received requests in per_workflow_and_customer_stats
211
+ per_workflow_and_customer_stats[req.workflow_type_id][req.customer_id].request_received
212
+
213
+ # if stats.received % 10_000 == 0
214
+ # $stderr.puts "#{Thread.current.__id__} sisfc: Received #{stats.received} requests."
215
+ # $stderr.puts "#{Thread.current.__id__} sisfc: Working on #{requests_being_worked_on} requests."
216
+ # $stderr.flush
217
+ # end
218
+ end
125
219
 
126
220
 
127
221
  # Leave these events for when we add VM migration support
128
222
  # when Event::ET_VM_SUSPEND
129
223
  # when Event::ET_VM_RESUME
130
224
 
225
+ when Event::ET_REQUEST_FORWARDING
226
+ # get request
227
+ req = e.data
228
+ time = e.time
229
+ vm = e.destination
230
+
231
+ vm.new_request(self, req, time)
232
+
131
233
 
132
234
  when Event::ET_WORKFLOW_STEP_COMPLETED
133
235
  # retrieve request and vm
@@ -138,22 +240,103 @@ module SISFC
138
240
  vm.request_finished(self, e.time)
139
241
 
140
242
  # find data center and workflow
141
- data_center = data_centers[req.data_center_id-1]
142
- workflow = @configuration.workflow_types[req.workflow_type_id]
243
+ data_center = data_center_repository[req.data_center_id]
244
+ workflow = workflow_type_repository[req.workflow_type_id]
143
245
 
144
246
  # check if there are other steps left to complete the workflow
145
247
  if req.next_step < workflow[:component_sequence].size
146
248
  # find next component name
147
249
  next_component_name = workflow[:component_sequence][req.next_step][:name]
148
250
 
149
- # get random vm providing next service component type
251
+ # get random VM providing next service component type
150
252
  new_vm = data_center.get_random_vm(next_component_name)
151
253
 
152
- # forward request to the new vm
153
- new_vm.new_request(self, req, e.time)
254
+ # this is the request's time of arrival at the new VM
255
+ forwarding_time = e.time
256
+
257
+ # there might not be a VM of the type we need in the current data
258
+ # center, so look in the other data centers
259
+ unless new_vm
260
+ # get list of other data centers, randomly picked
261
+ other_dcs = data_center_repository.values.select{|x| x != data_center && x.has_vms_of_type?(next_component_name) }&.shuffle
262
+ other_dcs.each do |dc|
263
+ new_vm = dc.get_random_vm(next_component_name)
264
+ if new_vm
265
+ # need to update data_center_id of request
266
+ req.data_center_id = dc.dcid
267
+
268
+ # keep track of transmission time
269
+ transmission_time =
270
+ @latency_manager.sample_latency_between(data_center.location_id,
271
+ dc.location_id)
272
+
273
+ unless transmission_time >= 0.0
274
+ raise "Negative transmission time (#{transmission_time})!"
275
+ end
276
+
277
+
278
+ # if [5,6,9].include? req.workflow_type_id
279
+ # $stderr.puts "rerouting request for wf #{req.workflow_type_id} from dc #{data_center.dcid} to dc #{dc.dcid}"
280
+ # $stderr.puts "next component name is #{next_component_name}"
281
+ # $stderr.puts "transmission time is #{transmission_time}"
282
+ # $stderr.flush
283
+ # end
284
+
285
+ req.update_transfer_time(transmission_time)
286
+ forwarding_time += transmission_time
287
+
288
+ # update request's current data_center_id
289
+ req.data_center_id = dc.dcid
290
+
291
+ # keep track of number of requests forwarded to other data centers
292
+ requests_forwarded_to_other_dcs += 1
293
+
294
+ # we're done here
295
+ break
296
+ end
297
+ end
298
+ end
299
+
300
+ # make sure we actually found a VM
301
+ raise "Cannot find VM running a component of type " +
302
+ "#{next_component_name} in any data center!" unless new_vm
303
+
304
+ # if [5,6,9].include? req.workflow_type_id
305
+ # $stderr.puts "received request for wf #{req.workflow_type_id} at dc #{req.data_center_id}"
306
+ # $stderr.puts "next component name is #{next_component_name}"
307
+ # $stderr.puts "next vm is #{new_vm.__id__}"
308
+ # $stderr.flush
309
+ # end
310
+
311
+ # schedule request forwarding to vm
312
+ new_event(Event::ET_REQUEST_FORWARDING, req, forwarding_time, new_vm)
313
+
154
314
  else # workflow is finished
315
+ # calculate transmission time
316
+ transmission_time =
317
+ @latency_manager.sample_latency_between(
318
+ # data center location
319
+ data_center_repository[req.data_center_id].location_id,
320
+ # customer location
321
+ customer_repository.dig(req.customer_id, :location_id)
322
+ )
323
+
324
+ # if [5,6,9].include? req.workflow_type_id
325
+ # $stderr.puts "closing request for wf #{req.workflow_type_id}"
326
+ # $stderr.puts "e.time is #{e.time}"
327
+ # $stderr.puts "transmission_time is #{transmission_time}"
328
+ # $stderr.flush
329
+ # end
330
+
331
+ unless transmission_time >= 0.0
332
+ raise "Negative transmission time (#{transmission_time})!"
333
+ end
334
+
335
+ # keep track of transmission time
336
+ req.update_transfer_time(transmission_time)
337
+
155
338
  # schedule request closure
156
- new_event(Event::ET_REQUEST_CLOSURE, req, e.time + req.communication_latency, nil)
339
+ new_event(Event::ET_REQUEST_CLOSURE, req, e.time + transmission_time, nil)
157
340
  end
158
341
 
159
342
 
@@ -171,7 +354,9 @@ module SISFC
171
354
 
172
355
  # collect request statistics
173
356
  stats.record_request(req)
174
- dc_stats[req.data_center_id - 1].record_request(req)
357
+
358
+ # collect request statistics in per_workflow_and_customer_stats
359
+ per_workflow_and_customer_stats[req.workflow_type_id][req.customer_id].record_request(req)
175
360
  end
176
361
 
177
362
 
@@ -184,22 +369,24 @@ module SISFC
184
369
 
185
370
  # puts "========== Simulation Finished =========="
186
371
 
187
- # calculate kpis (for the moment, we only have mttr)
188
- kpis = { :mttr => stats.mean,
189
- :served_requests => stats.n,
190
- :queued_requests => requests_being_worked_on }
191
- dc_kpis = dc_stats.map do |s|
192
- { :mttr => s.mean,
193
- :served_requests => s.n, }
194
- end
195
- fitness = @evaluator.evaluate_business_impact(kpis, dc_kpis, vm_allocation)
372
+ costs = @evaluator.evaluate_business_impact(stats, per_workflow_and_customer_stats,
373
+ vm_allocation)
196
374
  puts "====== Evaluating new allocation ======\n" +
197
- vm_allocation.map{|x| x.except(:service_time_distribution) }.inspect + "\n" +
198
- "kpis: #{kpis.to_s}\n" +
199
- "dc_kpis: #{dc_kpis.to_s}\n" +
200
- "=======================================\n"
201
- fitness
375
+ "costs: #{costs}\n" +
376
+ "vm_allocation: #{vm_allocation.inspect}\n" +
377
+ "stats: #{stats.to_s}\n" +
378
+ "per_workflow_and_customer_stats: #{per_workflow_and_customer_stats.to_s}\n" +
379
+ "=======================================\n"
380
+
381
+ # we want to minimize the cost, so we define fitness as the opposite of
382
+ # the sum of all costs incurred
383
+ fitness = - costs.values.inject(0.0){|s,x| s += x }
202
384
  end
203
385
 
386
+ private
387
+ def communication_latency_between(loc1, loc2)
388
+ @latency_manager.sample_latency_between(loc1.to_i, loc2.to_i)
389
+ end
390
+
204
391
  end
205
392
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module SISFC
2
4
  # the SortedArray class was taken from the ruby cookbook
3
5
  class SortedArray < Array
@@ -1,19 +1,35 @@
1
- require 'sisfc/request'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './request'
4
+
2
5
 
3
6
  module SISFC
4
7
  class Statistics
5
- attr_reader :mean, :n
8
+ attr_reader :mean, :n, :received, :longer_than
9
+ alias_method :closed, :n
6
10
 
7
11
  # see http://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Online_algorithm
8
- def initialize
12
+ # and https://www.johndcook.com/blog/standard_deviation/
13
+ def initialize(opts={})
9
14
  @n = 0 # number of requests
10
15
  @mean = 0.0
11
16
  @m_2 = 0.0
17
+ @longer_than = init_counters_for_longer_than_stats(opts)
18
+ @received = 0
19
+ end
20
+
21
+ def request_received
22
+ @received += 1
12
23
  end
13
24
 
14
25
  def record_request(req)
15
26
  # get new sample
16
27
  x = req.ttr
28
+ raise "TTR #{x} for request #{req.rid} invalid!" unless x > 0.0
29
+
30
+ @longer_than.each_key do |k|
31
+ @longer_than[k] += 1 if x > k
32
+ end
17
33
 
18
34
  # update counters
19
35
  @n += 1
@@ -25,5 +41,23 @@ module SISFC
25
41
  def variance
26
42
  @m_2 / (@n - 1)
27
43
  end
44
+
45
+ def to_s
46
+ "received: #{@received}, closed: #{@n}, " +
47
+ "(mean: #{@mean}, variance: #{variance}, longer_than: #{@longer_than.to_s})"
48
+ end
49
+
50
+ private
51
+ def init_counters_for_longer_than_stats(custom_kpis_config)
52
+ # prepare an infinite length enumerator that always returns zero
53
+ zeros = Enumerator.new(){|x| loop do x << 0 end }
54
+
55
+ Hash[
56
+ # wrap the values in custom_kpis_config[:longer_than] in an array
57
+ Array(custom_kpis_config[:longer_than]).
58
+ # and interval the numbers contained in that array with zeroes
59
+ zip(zeros) ]
60
+ end
61
+
28
62
  end
29
63
  end