symian 0.1.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.
- checksums.yaml +7 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +63 -0
- data/Rakefile +9 -0
- data/bin/symian +24 -0
- data/lib/symian.rb +5 -0
- data/lib/symian/configuration.rb +97 -0
- data/lib/symian/cost_analyzer.rb +37 -0
- data/lib/symian/event.rb +53 -0
- data/lib/symian/generator.rb +89 -0
- data/lib/symian/incident.rb +135 -0
- data/lib/symian/operator.rb +97 -0
- data/lib/symian/performance_analyzer.rb +60 -0
- data/lib/symian/simulation.rb +171 -0
- data/lib/symian/sorted_array.rb +57 -0
- data/lib/symian/support/dsl_helper.rb +18 -0
- data/lib/symian/support/yaml_io.rb +9 -0
- data/lib/symian/support_group.rb +182 -0
- data/lib/symian/trace_collector.rb +107 -0
- data/lib/symian/transition_matrix.rb +168 -0
- data/lib/symian/version.rb +3 -0
- data/lib/symian/work_shift.rb +158 -0
- data/symian.gemspec +29 -0
- data/test/symian/configuration_test.rb +66 -0
- data/test/symian/cost_analyzer_test.rb +52 -0
- data/test/symian/generator_test.rb +27 -0
- data/test/symian/incident_test.rb +104 -0
- data/test/symian/operator_test.rb +60 -0
- data/test/symian/reference_configuration.rb +107 -0
- data/test/symian/support_group_test.rb +111 -0
- data/test/symian/trace_collector_test.rb +203 -0
- data/test/symian/transition_matrix_test.rb +88 -0
- data/test/symian/work_shift_test.rb +68 -0
- data/test/test_helper.rb +5 -0
- metadata +188 -0
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'symian/work_shift'
|
2
|
+
|
3
|
+
|
4
|
+
module Symian
|
5
|
+
class Activity < Struct.new(:iid, :start_time, :end_time)
|
6
|
+
end
|
7
|
+
|
8
|
+
|
9
|
+
class Operator
|
10
|
+
|
11
|
+
extend Forwardable
|
12
|
+
|
13
|
+
REQUIRED_ATTRIBUTES = [
|
14
|
+
:oid,
|
15
|
+
:support_group_id,
|
16
|
+
]
|
17
|
+
|
18
|
+
OTHER_ATTRIBUTES = [
|
19
|
+
:workshift,
|
20
|
+
:specialization,
|
21
|
+
:work_record,
|
22
|
+
:support_group_id,
|
23
|
+
]
|
24
|
+
|
25
|
+
attr_reader *(REQUIRED_ATTRIBUTES + OTHER_ATTRIBUTES)
|
26
|
+
|
27
|
+
def_delegators :@workshift, :active_at?, :secs_to_begin_of_shift, :secs_to_end_of_shift
|
28
|
+
def_delegator :@workshift, :duration, :workshift_duration
|
29
|
+
|
30
|
+
|
31
|
+
def initialize(oid, support_group_id, opts={})
|
32
|
+
@oid = oid
|
33
|
+
@support_group_id = support_group_id
|
34
|
+
|
35
|
+
# set correspondent instance variables for optional arguments
|
36
|
+
opts.each do |k, v|
|
37
|
+
# ignore invalid attributes
|
38
|
+
instance_variable_set("@#{k}", v) if OTHER_ATTRIBUTES.include?(k)
|
39
|
+
end
|
40
|
+
|
41
|
+
# support :workshift => :all_day_long shortcut
|
42
|
+
if @workshift == :all_day_long
|
43
|
+
@workshift = WorkShift::WORKSHIFT_24x7
|
44
|
+
end
|
45
|
+
|
46
|
+
# default workshift is 24x7
|
47
|
+
@workshift ||= WorkShift::WORKSHIFT_24x7
|
48
|
+
|
49
|
+
@specialization ||= {}
|
50
|
+
@work_record ||= []
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
def assign(incident, incident_info, time)
|
55
|
+
|
56
|
+
# initialize incident start work time if needed
|
57
|
+
incident.start_work_time ||= time
|
58
|
+
|
59
|
+
# calculate time to end of shift
|
60
|
+
tteos = @workshift.secs_to_end_of_shift(time)
|
61
|
+
raise "tteos: #{tteos}" if tteos < 0.0
|
62
|
+
|
63
|
+
# specialization
|
64
|
+
specialization = @specialization[incident.category] || 1.0
|
65
|
+
|
66
|
+
# calculate time to incident escalation
|
67
|
+
ttie = incident_info[:needed_work_time] / specialization.to_f
|
68
|
+
raise "ttie: #{ttie}" if ttie < 0.0
|
69
|
+
|
70
|
+
# handle incident
|
71
|
+
if tteos < ttie # end of shift first
|
72
|
+
work_time = tteos
|
73
|
+
reason = :operator_off_duty
|
74
|
+
else # escalation first
|
75
|
+
work_time = ttie
|
76
|
+
reason = :incident_escalation
|
77
|
+
end
|
78
|
+
|
79
|
+
# update needed (effective) incident work time
|
80
|
+
incident_info[:needed_work_time] -= work_time * specialization
|
81
|
+
|
82
|
+
# update incident tracking
|
83
|
+
incident.add_tracking_information(:type => :work,
|
84
|
+
:at => time,
|
85
|
+
:duration => work_time,
|
86
|
+
:sg => @support_group_id,
|
87
|
+
:operator => @oid)
|
88
|
+
|
89
|
+
# update operator work record
|
90
|
+
@work_record << Activity.new(incident.iid, time, time + work_time)
|
91
|
+
|
92
|
+
# return [ reason, time_when_operator_stops_working ]
|
93
|
+
[ reason, time + work_time ]
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'symian/event'
|
2
|
+
require 'symian/generator'
|
3
|
+
require 'symian/incident'
|
4
|
+
require 'symian/operator'
|
5
|
+
require 'symian/support_group'
|
6
|
+
|
7
|
+
|
8
|
+
module Symian
|
9
|
+
class PerformanceAnalyzer
|
10
|
+
def initialize(config)
|
11
|
+
@warmup_threshold = config.start_time + config.warmup_duration
|
12
|
+
end
|
13
|
+
|
14
|
+
def calculate_kpis(trace)
|
15
|
+
raise ArgumentError, 'Argument must be a TraceCollector' unless TraceCollector === trace
|
16
|
+
|
17
|
+
kpis = {}
|
18
|
+
|
19
|
+
# these metrics are considered as kpis
|
20
|
+
kpis[:all_incidents] = trace.incidents
|
21
|
+
kpis[:incidents_considered] = 0
|
22
|
+
kpis[:closed_incidents] = 0
|
23
|
+
kpis[:mean_ttr] = 0
|
24
|
+
kpis[:max_ttr] = 0
|
25
|
+
kpis[:mean_waiting_time] = 0
|
26
|
+
|
27
|
+
max_ttr = 0
|
28
|
+
ttr_sum = 0
|
29
|
+
wt_sum = 0
|
30
|
+
trace.with_incidents do |i|
|
31
|
+
|
32
|
+
next if @warmup_threshold and i.arrival_time < @warmup_threshold
|
33
|
+
|
34
|
+
kpis[:incidents_considered] += 1
|
35
|
+
|
36
|
+
if i.closed?
|
37
|
+
kpis[:closed_incidents] += 1
|
38
|
+
ttr = i.total_work_time
|
39
|
+
ttr_sum += ttr
|
40
|
+
wt_sum += i.total_queue_time
|
41
|
+
max_ttr = ttr if ttr > max_ttr
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
kpis[:max_ttr] = max_ttr
|
47
|
+
if kpis[:closed_incidents] == 0
|
48
|
+
kpis[:mean_ttr] = Float::MAX
|
49
|
+
kpis[:mean_waiting_time] = Float::MAX
|
50
|
+
else
|
51
|
+
kpis[:mean_ttr] = ttr_sum / kpis[:closed_incidents]
|
52
|
+
kpis[:mean_waiting_time] = wt_sum / kpis[:closed_incidents]
|
53
|
+
end
|
54
|
+
|
55
|
+
# return kpis
|
56
|
+
kpis
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
require 'symian/event'
|
2
|
+
require 'symian/generator'
|
3
|
+
require 'symian/operator'
|
4
|
+
require 'symian/performance_analyzer'
|
5
|
+
require 'symian/sorted_array'
|
6
|
+
require 'symian/support_group'
|
7
|
+
require 'symian/transition_matrix'
|
8
|
+
require 'symian/trace_collector'
|
9
|
+
|
10
|
+
|
11
|
+
module Symian
|
12
|
+
class Simulation
|
13
|
+
|
14
|
+
ATTRIBUTES = [ :start_time ]
|
15
|
+
|
16
|
+
attr_reader *ATTRIBUTES
|
17
|
+
|
18
|
+
|
19
|
+
def initialize(configuration, performance_analyzer, trace=TraceCollector.new(:memory))
|
20
|
+
@configuration = configuration
|
21
|
+
|
22
|
+
# setup performance analyzer and simulation trace
|
23
|
+
@performance_analyzer = performance_analyzer
|
24
|
+
@trace = trace
|
25
|
+
|
26
|
+
# setup simulation start and current time
|
27
|
+
@current_time = @start_time = @configuration.start_time
|
28
|
+
|
29
|
+
# create support groups
|
30
|
+
@support_groups = {}
|
31
|
+
@configuration.support_groups.each do |name,conf|
|
32
|
+
@support_groups[name] = SupportGroup.new(name, self, conf[:work_time], conf[:operators])
|
33
|
+
end
|
34
|
+
|
35
|
+
# create transition matrix
|
36
|
+
@transition_matrix = TransitionMatrix.new(@configuration.transition_matrix)
|
37
|
+
|
38
|
+
# create event queue
|
39
|
+
@event_queue = SortedArray.new
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
def new_event(type, data, time, destination)
|
44
|
+
@event_queue << Event.new(type, data, time, destination)
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
def now
|
49
|
+
@current_time
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
def run
|
54
|
+
|
55
|
+
# initialize support groups
|
56
|
+
@support_groups.values.each do |sg|
|
57
|
+
sg.initialize_at(@configuration.start_time)
|
58
|
+
end
|
59
|
+
|
60
|
+
# generate first incident
|
61
|
+
ig = IncidentGenerator.new(self, @configuration.incident_generation)
|
62
|
+
ig.generate
|
63
|
+
|
64
|
+
# schedule end of simulation
|
65
|
+
unless @configuration.end_time.nil?
|
66
|
+
# puts "Simulation ends at: #{@configuration.end_time}"
|
67
|
+
new_event(Event::ET_END_OF_SIMULATION, nil, @configuration.end_time, nil)
|
68
|
+
end
|
69
|
+
|
70
|
+
# calculate warmup threshold
|
71
|
+
warmup_threshold = @configuration.start_time + @configuration.warmup_duration
|
72
|
+
|
73
|
+
@incidents_being_worked_on ||= []
|
74
|
+
|
75
|
+
# launch simulation
|
76
|
+
until @event_queue.empty?
|
77
|
+
e = @event_queue.shift
|
78
|
+
|
79
|
+
# sanity check on simulation time flow
|
80
|
+
if @current_time > e.time
|
81
|
+
raise 'Error: simulation time inconsistency for event ' +
|
82
|
+
"e.type=#{e.type} @current_time=#{@current_time}, e.time=#{e.time}"
|
83
|
+
end
|
84
|
+
|
85
|
+
@current_time = e.time
|
86
|
+
|
87
|
+
#Trace.new_event e
|
88
|
+
case e.type
|
89
|
+
when Event::ET_INCIDENT_ARRIVAL
|
90
|
+
|
91
|
+
sg_name = @transition_matrix.escalation('In')
|
92
|
+
|
93
|
+
sg = @support_groups[sg_name]
|
94
|
+
sg.new_incident(e.data, e.time)
|
95
|
+
|
96
|
+
@incidents_being_worked_on << e.data
|
97
|
+
|
98
|
+
# generate next incident
|
99
|
+
ig.generate
|
100
|
+
|
101
|
+
# TODO: pinpoint instant for calculation of time spent in enqueued state
|
102
|
+
|
103
|
+
|
104
|
+
when Event::ET_INCIDENT_ASSIGNMENT
|
105
|
+
|
106
|
+
# TODO: implement calculation of time spent in suspended state
|
107
|
+
|
108
|
+
|
109
|
+
when Event::ET_OPERATOR_ACTIVITY_STARTS
|
110
|
+
|
111
|
+
|
112
|
+
when Event::ET_OPERATOR_ACTIVITY_FINISHES
|
113
|
+
|
114
|
+
sg = @support_groups[e.destination]
|
115
|
+
sg.operator_finished_working(e.data[0], e.time)
|
116
|
+
|
117
|
+
|
118
|
+
when Event::ET_INCIDENT_RESCHEDULING
|
119
|
+
|
120
|
+
sg = @support_groups[e.destination]
|
121
|
+
sg.schedule_incident_for_reassignment(e.data[0], e.data[1], e.time)
|
122
|
+
|
123
|
+
# TODO: pinpoint instant for calculation of time spent in suspended state
|
124
|
+
|
125
|
+
|
126
|
+
when Event::ET_INCIDENT_ESCALATION
|
127
|
+
inc = e.data
|
128
|
+
|
129
|
+
sg_name = @transition_matrix.escalation(e.destination)
|
130
|
+
if sg_name == "Out"
|
131
|
+
inc.closure_time = e.time
|
132
|
+
|
133
|
+
# remove incident from list of incidents being worked on and add it to trace
|
134
|
+
@incidents_being_worked_on.delete(inc)
|
135
|
+
@trace.record_incidents(inc)
|
136
|
+
else
|
137
|
+
sg = @support_groups[sg_name]
|
138
|
+
sg.new_incident(inc, e.time)
|
139
|
+
end
|
140
|
+
|
141
|
+
|
142
|
+
when Event::ET_OPERATOR_LEAVING
|
143
|
+
|
144
|
+
sg = @support_groups[e.destination]
|
145
|
+
sg.operator_going_home(e.data, e.time)
|
146
|
+
|
147
|
+
|
148
|
+
when Event::ET_OPERATOR_RETURNING
|
149
|
+
|
150
|
+
sg = @support_groups[e.destination]
|
151
|
+
sg.operator_arrived_at_work(e.data, e.time)
|
152
|
+
|
153
|
+
|
154
|
+
when Event::ET_SUPPORT_GROUP_QUEUE_SIZE_CHANGE
|
155
|
+
|
156
|
+
|
157
|
+
when Event::ET_END_OF_SIMULATION
|
158
|
+
break
|
159
|
+
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# save trace file
|
164
|
+
@trace.save_and_close
|
165
|
+
kpis = @performance_analyzer.calculate_kpis(@trace)
|
166
|
+
kpis.merge(incidents_being_worked_on: @incidents_being_worked_on.size)
|
167
|
+
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Symian
|
2
|
+
# the SortedArray class was taken from the ruby cookbook
|
3
|
+
class SortedArray < Array
|
4
|
+
def initialize(*args, &sort_by)
|
5
|
+
@sort_by = sort_by || Proc.new { |x,y| x <=> y }
|
6
|
+
super(*args)
|
7
|
+
sort! &sort_by
|
8
|
+
end
|
9
|
+
|
10
|
+
def insert(i, v)
|
11
|
+
if size == 0 or v < self[0]
|
12
|
+
super(0, v)
|
13
|
+
elsif v > self[-1]
|
14
|
+
super(-1, v)
|
15
|
+
else
|
16
|
+
left = 0
|
17
|
+
right = size - 1
|
18
|
+
middle = (left + right)/2
|
19
|
+
while left < right
|
20
|
+
if v >= self[middle]
|
21
|
+
left = middle + 1
|
22
|
+
else
|
23
|
+
right = middle
|
24
|
+
end
|
25
|
+
middle = (left + right)/2
|
26
|
+
end
|
27
|
+
super(middle, v)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def <<(el)
|
32
|
+
insert(0, el)
|
33
|
+
end
|
34
|
+
|
35
|
+
alias push <<
|
36
|
+
alias unshift <<
|
37
|
+
|
38
|
+
# some methods, like collect!, can modify the items in an array,
|
39
|
+
# taking them out of sort order. we need to redefine those
|
40
|
+
# methods.
|
41
|
+
# we can't use define_method to define these methods because in
|
42
|
+
# Ruby 1.8 you can't use define_method to create a method that
|
43
|
+
# takes a block argument.
|
44
|
+
[ "collect!", "flatten!", "[]=" ].each do |method_name|
|
45
|
+
class_eval %{
|
46
|
+
def #{method_name}(*args)
|
47
|
+
super
|
48
|
+
sort! &@sort_by
|
49
|
+
end
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
def reverse!
|
54
|
+
# do nothing: reversing the array would disorder it.
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# taken from Jim Freeze's excellent article "Creating DSLs with Ruby"
|
2
|
+
# http://www.artima.com/rubycs/articles/ruby_as_dsl.html
|
3
|
+
|
4
|
+
class Module
|
5
|
+
def dsl_accessor(*symbols)
|
6
|
+
symbols.each { |sym|
|
7
|
+
class_eval %{
|
8
|
+
def #{sym}(*val)
|
9
|
+
if val.empty?
|
10
|
+
@#{sym}
|
11
|
+
else
|
12
|
+
@#{sym} = val.size == 1 ? val[0] : val
|
13
|
+
end
|
14
|
+
end
|
15
|
+
}
|
16
|
+
}
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,182 @@
|
|
1
|
+
require 'erv'
|
2
|
+
|
3
|
+
require 'symian/event'
|
4
|
+
require 'symian/operator'
|
5
|
+
require 'symian/work_shift'
|
6
|
+
|
7
|
+
require 'symian/support/yaml_io'
|
8
|
+
|
9
|
+
|
10
|
+
module Symian
|
11
|
+
|
12
|
+
class SupportGroup
|
13
|
+
|
14
|
+
# setup readable/accessible attributes
|
15
|
+
ATTRIBUTES = [ :sgid, :operators ]
|
16
|
+
|
17
|
+
attr_reader *ATTRIBUTES
|
18
|
+
|
19
|
+
|
20
|
+
# setup attributes saved in traces
|
21
|
+
include YAMLSerializable
|
22
|
+
|
23
|
+
TRACED_ATTRIBUTES = ATTRIBUTES + [ :incident_queue_info ]
|
24
|
+
|
25
|
+
|
26
|
+
def initialize(support_group_id, simulation, work_time_characterization, operator_characterizations)
|
27
|
+
@sgid = support_group_id
|
28
|
+
@simulation = simulation
|
29
|
+
|
30
|
+
# initialize needed_work_time_rng
|
31
|
+
@needed_work_time_rv = ERV::RandomVariable.new(work_time_characterization)
|
32
|
+
|
33
|
+
# create operators
|
34
|
+
@operators = []
|
35
|
+
|
36
|
+
if operator_characterizations.kind_of?(Hash)
|
37
|
+
operator_characterizations = [ operator_characterizations ]
|
38
|
+
end
|
39
|
+
|
40
|
+
next_op_id = 1
|
41
|
+
operator_characterizations.each do |x|
|
42
|
+
x[:number].times do |y|
|
43
|
+
op = Operator.new("OP#{next_op_id}_#{@sgid}", @sgid, x.reject { |k, v| k == :number })
|
44
|
+
@operators << op
|
45
|
+
next_op_id = next_op_id + 1
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# initialize incident queue and related tracking information
|
50
|
+
@incident_queue = []
|
51
|
+
@incident_queue_info = []
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
def initialize_at(time)
|
56
|
+
# find out which operators are off duty and schedule their comeback
|
57
|
+
@operators_off_work = @operators.select do |x|
|
58
|
+
!x.workshift.active_at?(time)
|
59
|
+
end
|
60
|
+
@operators_off_work.each do |op|
|
61
|
+
t = op.workshift.secs_to_begin_of_shift(time)
|
62
|
+
@simulation.new_event(Event::ET_OPERATOR_RETURNING, op.oid,
|
63
|
+
time + t, @sgid)
|
64
|
+
end
|
65
|
+
|
66
|
+
# find out which operators are on duty and schedule their leaving
|
67
|
+
@available_operators = @operators - @operators_off_work
|
68
|
+
@available_operators.each do |op|
|
69
|
+
t = op.workshift.secs_to_end_of_shift(time)
|
70
|
+
@simulation.new_event(Event::ET_OPERATOR_LEAVING, op.oid,
|
71
|
+
time + t, @sgid) unless t == WorkShift::Infinity
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
def new_incident(incident, time)
|
77
|
+
# increase number of visited SGs
|
78
|
+
incident.visited_support_groups += 1
|
79
|
+
|
80
|
+
incident_info = {
|
81
|
+
# set up incident needed work time
|
82
|
+
:needed_work_time => @needed_work_time_rv.next,
|
83
|
+
# # reset queue_time_at_last_sg attribute
|
84
|
+
# :queue_time => 0
|
85
|
+
}
|
86
|
+
|
87
|
+
# put incident at the end of the queue
|
88
|
+
@incident_queue << [ incident, incident_info, time ]
|
89
|
+
|
90
|
+
# update queue size tracking information
|
91
|
+
@incident_queue_info << { :size => @incident_queue.size, :time => time }
|
92
|
+
@simulation.new_event(Event::ET_SUPPORT_GROUP_QUEUE_SIZE_CHANGE, @incident_queue.size,
|
93
|
+
time, @sgid)
|
94
|
+
|
95
|
+
# try to allocate operator
|
96
|
+
try_to_allocate_operator(time)
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
def schedule_incident_for_reassignment(incident, incident_info, time)
|
101
|
+
@incident_queue.unshift [ incident, incident_info, time ]
|
102
|
+
# update queue size tracking information
|
103
|
+
@incident_queue_info << { :size => @incident_queue.size, :time => time }
|
104
|
+
try_to_allocate_operator(time)
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
def operator_going_home(operator_id, time)
|
109
|
+
op = @operators.find{|x| x.oid == operator_id }
|
110
|
+
@available_operators.delete_if{|x| x.oid == operator_id }
|
111
|
+
@operators_off_work << op
|
112
|
+
@simulation.new_event(Event::ET_OPERATOR_RETURNING, op.oid,
|
113
|
+
time + 86400 - op.workshift.duration, @sgid)
|
114
|
+
end
|
115
|
+
|
116
|
+
|
117
|
+
def operator_arrived_at_work(operator_id, time)
|
118
|
+
op = @operators.find{|x| x.oid == operator_id }
|
119
|
+
@operators_off_work.delete(op)
|
120
|
+
@available_operators << op
|
121
|
+
try_to_allocate_operator(time)
|
122
|
+
@simulation.new_event(Event::ET_OPERATOR_LEAVING, op.oid,
|
123
|
+
time + op.workshift.duration, @sgid)
|
124
|
+
end
|
125
|
+
|
126
|
+
|
127
|
+
def operator_finished_working(operator_id, time)
|
128
|
+
op = @operators.find{|x| x.oid == operator_id }
|
129
|
+
if op.workshift.secs_to_end_of_shift(time) > 0
|
130
|
+
@available_operators << op
|
131
|
+
end
|
132
|
+
try_to_allocate_operator(time)
|
133
|
+
end
|
134
|
+
|
135
|
+
|
136
|
+
private
|
137
|
+
|
138
|
+
def try_to_allocate_operator(time)
|
139
|
+
if !@available_operators.empty? and !@incident_queue.empty?
|
140
|
+
op = @available_operators.shift
|
141
|
+
i, inc_info, t = @incident_queue.shift
|
142
|
+
|
143
|
+
# update incident tracking information
|
144
|
+
queue_time = time.to_i - t.to_i
|
145
|
+
i.add_tracking_information(:type => :queue,
|
146
|
+
:at => t,
|
147
|
+
:duration => queue_time,
|
148
|
+
:sg => @support_group_id)
|
149
|
+
|
150
|
+
|
151
|
+
@simulation.new_event(Event::ET_SUPPORT_GROUP_QUEUE_SIZE_CHANGE,
|
152
|
+
@incident_queue.size, time, @sgid)
|
153
|
+
@simulation.new_event(Event::ET_INCIDENT_ASSIGNMENT,
|
154
|
+
[ i.iid, op.oid ], time, @sgid)
|
155
|
+
@simulation.new_event(Event::ET_OPERATOR_ACTIVITY_STARTS,
|
156
|
+
[ op.oid, i.iid ], time, @sgid)
|
157
|
+
|
158
|
+
# assign updates inc_info
|
159
|
+
report = op.assign(i, inc_info, time)
|
160
|
+
finish_time = report[1]
|
161
|
+
|
162
|
+
puts "finish_time: #{finish_time}, time: #{time}" if time > finish_time
|
163
|
+
@simulation.new_event(Event::ET_OPERATOR_ACTIVITY_FINISHES,
|
164
|
+
[ op.oid, i.iid ], finish_time, @sgid)
|
165
|
+
|
166
|
+
case report[0]
|
167
|
+
when :incident_escalation
|
168
|
+
@simulation.new_event(Event::ET_INCIDENT_ESCALATION,
|
169
|
+
i, finish_time, @sgid)
|
170
|
+
when :operator_off_duty
|
171
|
+
# TODO: implement configurable rescheduling policy
|
172
|
+
@incident_queue << [ i, inc_info, finish_time ]
|
173
|
+
@simulation.new_event(Event::ET_INCIDENT_RESCHEDULING,
|
174
|
+
[ i, inc_info, op.oid ], finish_time, @sgid)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
182
|
+
|