mtortonesi/symian

View on GitHub
lib/symian/support_group.rb

Summary

Maintainability
A
1 hr
Test Coverage
require 'erv'

require 'symian/event'
require 'symian/operator'
require 'symian/work_shift'

require 'symian/support/yaml_io'


module Symian

  class SupportGroup

    # setup readable/accessible attributes
    ATTRIBUTES = [ :sgid, :operators ]

    attr_reader *ATTRIBUTES


    # setup attributes saved in traces
    include YAMLSerializable

    TRACED_ATTRIBUTES = ATTRIBUTES + [ :incident_queue_info ]


    def initialize(support_group_id, simulation, work_time_characterization, operator_characterizations)
      @sgid = support_group_id
      @simulation = simulation

      # initialize needed_work_time_rng
      @needed_work_time_rv = ERV::RandomVariable.new(work_time_characterization)

      # create operators
      @operators = []

      if operator_characterizations.kind_of?(Hash)
        operator_characterizations = [ operator_characterizations ]
      end

      next_op_id = 1
      operator_characterizations.each do |x|
        x[:number].times do |y|
          op = Operator.new("OP#{next_op_id}_#{@sgid}", @sgid, x.reject { |k, v| k == :number })
          @operators << op
          next_op_id = next_op_id + 1
        end
      end

      # initialize incident queue and related tracking information
      @incident_queue = []
      @incident_queue_info = []
    end


    def initialize_at(time)
      # find out which operators are off duty and schedule their comeback
      @operators_off_work = @operators.select do |x|
        !x.workshift.active_at?(time)
      end
      @operators_off_work.each do |op|
        t = op.workshift.secs_to_begin_of_shift(time)
        @simulation.new_event(Event::ET_OPERATOR_RETURNING, op.oid,
                              time + t, @sgid)
      end

      # find out which operators are on duty and schedule their leaving
      @available_operators = @operators - @operators_off_work
      @available_operators.each do |op|
        t = op.workshift.secs_to_end_of_shift(time)
        @simulation.new_event(Event::ET_OPERATOR_LEAVING, op.oid,
                              time + t, @sgid) unless t == WorkShift::Infinity
      end
    end


    def new_incident(incident, time)
      # increase number of visited SGs
      incident.visited_support_groups += 1

      incident_info = {
        # set up incident needed work time
        :needed_work_time => @needed_work_time_rv.next,
        # # reset queue_time_at_last_sg attribute
        # :queue_time => 0
      }

      # put incident at the end of the queue
      @incident_queue << [ incident, incident_info, time ]

      # update queue size tracking information
      @incident_queue_info << { :size => @incident_queue.size, :time => time }
      @simulation.new_event(Event::ET_SUPPORT_GROUP_QUEUE_SIZE_CHANGE, @incident_queue.size,
                            time, @sgid)

      # try to allocate operator
      try_to_allocate_operator(time)
    end


    def schedule_incident_for_reassignment(incident, incident_info, time)
      @incident_queue.unshift [ incident, incident_info, time ]
      # update queue size tracking information
      @incident_queue_info << { :size => @incident_queue.size, :time => time }
      try_to_allocate_operator(time)
    end


    def operator_going_home(operator_id, time)
      op = @operators.find{|x| x.oid == operator_id }
      @available_operators.delete_if{|x| x.oid == operator_id }
      @operators_off_work << op
      @simulation.new_event(Event::ET_OPERATOR_RETURNING, op.oid,
                            time + 86400 - op.workshift.duration, @sgid)
    end


    def operator_arrived_at_work(operator_id, time)
      op = @operators.find{|x| x.oid == operator_id }
      @operators_off_work.delete(op)
      @available_operators << op
      try_to_allocate_operator(time)
      @simulation.new_event(Event::ET_OPERATOR_LEAVING, op.oid,
                            time + op.workshift.duration, @sgid)
    end


    def operator_finished_working(operator_id, time)
      op = @operators.find{|x| x.oid == operator_id }
      if op.workshift.secs_to_end_of_shift(time) > 0
        @available_operators << op
      end
      try_to_allocate_operator(time)
    end


    private

      def try_to_allocate_operator(time)
        if !@available_operators.empty? and !@incident_queue.empty?
          op = @available_operators.shift
          i, inc_info, t = @incident_queue.shift

          # update incident tracking information
          queue_time = time.to_i - t.to_i
          i.add_tracking_information(:type => :queue,
                                     :at => t,
                                     :duration => queue_time,
                                     :sg => @support_group_id)


          @simulation.new_event(Event::ET_SUPPORT_GROUP_QUEUE_SIZE_CHANGE,
                                @incident_queue.size, time, @sgid)
          @simulation.new_event(Event::ET_INCIDENT_ASSIGNMENT,
                                [ i.iid, op.oid ], time, @sgid)
          @simulation.new_event(Event::ET_OPERATOR_ACTIVITY_STARTS,
                                [ op.oid, i.iid ], time, @sgid)

          # assign updates inc_info
          report = op.assign(i, inc_info, time)
          finish_time = report[1]

          puts "finish_time: #{finish_time}, time: #{time}" if time > finish_time
          @simulation.new_event(Event::ET_OPERATOR_ACTIVITY_FINISHES,
                                [ op.oid, i.iid ], finish_time, @sgid)

          case report[0]
            when :incident_escalation
              @simulation.new_event(Event::ET_INCIDENT_ESCALATION,
                                    i, finish_time, @sgid)
            when :operator_off_duty
              # TODO: implement configurable rescheduling policy
              @incident_queue << [ i, inc_info, finish_time ]
              @simulation.new_event(Event::ET_INCIDENT_RESCHEDULING,
                                    [ i, inc_info, op.oid ], finish_time, @sgid)
          end
        end
      end

  end

end