Class: DiscreteEvent::EventQueue

Inherits:
Object
  • Object
show all
Defined in:
lib/discrete_event/event_queue.rb

Overview

Queue of pending events; also keeps track of the clock (the current time).

There are two key terms:

  • action: any Ruby block

  • event: an action to be executed at some specified time in the future

Events are usually created using the #at and #after methods. The methods #at_each, #every and #recur_after make some important special cases more efficient.

See the README for an example.

Direct Known Subclasses

Simulation

Defined Under Namespace

Classes: Event

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(now = 0.0) ⇒ EventQueue

Returns a new instance of EventQueue.



37
38
39
40
41
# File 'lib/discrete_event/event_queue.rb', line 37

def initialize(now = 0.0)
  @now = now
  @events = FastContainers::PriorityQueue.new(:min)
  @recur_interval = nil
end

Instance Attribute Details

#eventsPQueue (readonly)

Event queue.

Returns:

  • (PQueue)


35
36
37
# File 'lib/discrete_event/event_queue.rb', line 35

def events
  @events
end

#nowNumeric (readonly)

Current time (taken from the currently executing event, if any). You can use floating point or integer time.

Returns:

  • (Numeric)


28
29
30
# File 'lib/discrete_event/event_queue.rb', line 28

def now
  @now
end

Instance Method Details

#after(delay) { ... } ⇒ Event

Schedule action (a block) to run after the given delay (with respect to #now).

Parameters:

  • delay (Numeric)

    after which action should run; non-negative

Yields:

  • action to be run after delay

Returns:



70
71
72
# File 'lib/discrete_event/event_queue.rb', line 70

def after(delay, &action)
  at(@now + delay, &action)
end

#at(time) { ... } ⇒ Event

Schedule action (a block) to run at the given time; time must not be in the past.

Parameters:

  • time (Numeric)

    at which action should run; must be >= #now

Yields:

  • action to be run at time

Returns:



53
54
55
56
57
58
# File 'lib/discrete_event/event_queue.rb', line 53

def at(time, &action)
  raise 'cannot schedule event in the past' if time < now
  event = Event.new(time, action)
  @events.push(event, time)
  event
end

#at_each(elements, time = nil) {|element| ... } ⇒ nil

Schedule action (a block) to run for each element in the given list (possibly at different times).

This method may be of interest if you have a large number of events that occur at known times. You could use #at to add each one to the event queue at the start of the simulation, but this will make adding other events more expensive. Instead, this method adds them one at a time, so only the next event is stored in the event queue.

Examples:

Alert = Struct.new(:when, :message)
alerts = [Alert.new(12, "ha!"), Alert.new(42, "ah!")] # and many more
at_each alerts, :when do |alert|
  puts alert.message
end

Parameters:

  • elements (Enumerable)

    to yield; must be in ascending order according to time; note that this method keeps a reference to this object and removes elements as they are executed, so you may want to pass a copy if you plan to change it after this call returns

  • time (Proc, Symbol, nil) (defaults to: nil)

    used to determine when the action will run for a given element; if a Proc, the proc must return the appropriate time; if a Symbol, each element must respond to time; if nil, it is assumed that element.time returns the time

Yields:

  • (element)

Yield Parameters:

  • element (Object)

    from elements

Returns:

  • (nil)

Raises:

  • (ArgumentError)


131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/discrete_event/event_queue.rb', line 131

def at_each(elements, time = nil, &action)
  raise ArgumentError, 'no action given' unless block_given?

  unless elements.empty?
    element = elements.shift
    if time.nil?
      element_time = element.time
    elsif time.is_a? Proc
      element_time = time.call(element)
    elsif time.is_a? Symbol
      element_time = element.send(time)
    else
      raise ArgumentError, 'bad time'
    end

    at element_time do
      yield element
      at_each elements, time, &action
    end
  end
  nil
end

#cancel(event) ⇒ nil

Cancel an event previously created with #at or #after.

Parameters:

  • event (Event)

    the event to cancel

Returns:

  • (nil)


81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/discrete_event/event_queue.rb', line 81

def cancel(event)
  # not very efficient but hopefully not used very often
  temp = []
  until @events.empty? || @events.top_key > event.time
    e = @events.top
    @events.pop
    break if e.equal?(event)
    temp << e
  end
  temp.each do |temp_event|
    @events.push(temp_event, temp_event.time)
  end
  nil
end

#each {|now| ... } ⇒ self

Allow for the creation of a ruby Enumerator for the simulation. This yields for each event. Note that this enumerator may return infinitely many events, if there are repeating events (#every).

Examples:

eq = EventQueue.new
eq.at 13 do
  puts "hi"
end
eq.at 42 do
  puts "hello"
end
for t in eq.to_enum
  puts t
end

Yields:

  • (now)

    called immediately after each event runs

Yield Parameters:

  • now (Numeric)

    as #now

Returns:

  • (self)


300
301
302
303
# File 'lib/discrete_event/event_queue.rb', line 300

def each
  yield now while run_next
  self
end

#every(interval, start = 0) ⇒ nil

Schedule action (a block) to run periodically.

This is useful for statistics collection.

Note that if you specify one or more events of this kind, the simulation will never run out of events.

Examples:

every 5 do
  if now > 100
    # record stats
  end
  throw :stop if now > 1000
end

Parameters:

  • interval (Numeric)

    non-negative

  • start (Numeric) (defaults to: 0)

    block first runs at this time

Returns:

  • (nil)


212
213
214
215
216
217
218
# File 'lib/discrete_event/event_queue.rb', line 212

def every(interval, start = 0)
  at start do
    yield
    recur_after interval
  end
  nil
end

#next_event_timeNumeric?

The time of the next queued event, or nil if there are no queued events.

If this method is called from within an action block, it returns #now (that is, the current event hasn’t finished yet, so it’s still in some sense the next event).

Returns:

  • (Numeric, nil)


229
230
231
# File 'lib/discrete_event/event_queue.rb', line 229

def next_event_time
  @events.top_key unless @events.empty?
end

#recur_after(interval) ⇒ nil

When called from within an action block, repeats the action block after the specified interval has elapsed.

Calling this method from outside an action block has no effect. You may call this method at most once in an action block.

Note that you can achieve the same effect using #at and #after and a named method, as in

def demo
  at 5 do
    puts "now: #{now}"
    after 10*rand do
      demo
    end
  end
end

but it is somewhat more efficient to call recur_after, and, if you do, the named method is not necessary.

Examples:

at 5 do
  puts "now: #{now}"
  recur_after 10*rand
end

Parameters:

  • interval (Numeric)

    non-negative

Returns:

  • (nil)


184
185
186
187
188
# File 'lib/discrete_event/event_queue.rb', line 184

def recur_after(interval)
  raise 'cannot recur twice' if @recur_interval
  @recur_interval = interval
  nil
end

#reset(now = 0.0) ⇒ self

Clear any pending events in the event queue and reset #now.

Returns:

  • (self)


310
311
312
313
314
# File 'lib/discrete_event/event_queue.rb', line 310

def reset(now = 0.0)
  @now = now
  @events = FastContainers::PriorityQueue.new(:min)
  self
end

#run_nextBoolean

Run the action for the next event in the queue.

Returns:

  • (Boolean)

    false if there are no more events.



238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/discrete_event/event_queue.rb', line 238

def run_next
  return false if @events.empty?

  event = @events.top
  @events.pop

  # run the action
  @now = event.time
  event.action.call

  # Handle recurring events.
  if @recur_interval
    event.time = @now + @recur_interval
    @events.push(event, event.time)
    @recur_interval = nil
  end

  true
end

#run_to(time) ⇒ nil

Run events until the given time (inclusive). When this method returns, #now is time, and all events scheduled to run at times up to and including time have been run.

Parameters:

  • time (Numeric)

    to run to (inclusive)

Returns:

  • (nil)


267
268
269
270
271
272
273
274
275
# File 'lib/discrete_event/event_queue.rb', line 267

def run_to(time)
  # add an event to ensure that we actually stop at the given time, even if
  # there isn't an event in the queue
  at time do
    # nothing
  end
  run_next until @events.empty? || next_event_time > time
  nil
end