Events

SimJulia includes an extensive set of event constructors for various purposes. This section details the following events:

A Process is also an event and is also discussed.

The resource and container event constructors are discussed in a later section.

Event basics

SimJulia events are very similar – if not identical — to deferreds, futures or promises. Instances of the type AbstractEvent are used to describe any kind of events. Events can be in one of the following states:

  • not triggered: an event may happen
  • triggered: is going to happen
  • processing: is happening
  • processed: has happened

They traverse these states exactly once in that order. Events are also tightly bound to time and time causes events to advance their state. Initially, events are not triggered and just objects in memory.

If an event gets triggered, it is scheduled at a given time and inserted into SimJulia’s event list. The function triggered(ev::Event) returns true. As long as the event is not processed, you can add callbacks to an event. Callbacks are functions that have as first argument an Event and are stored in the callbacks list of that event. An event becomes processed when SimJulia has popped it from the event list and has called all of its callbacks. It is no longer possible to add callbacks. The function processed(ev::Event) returns at that moment true.

Events also have a value. The value can be set before or when the event is triggered and can be retrieved via the function value(ev::Event) or, within a process, via the return value of the function yield(ev::AbstractEvent).

Adding callbacks to an event

“What? Callbacks? I’ve never seen no callbacks!”, you might think if you have worked your way through the tutorial.

That’s on purpose. The most common way to add a callback to an event is yielding it from your process function (yield(ev::AbstractEvent)). This will add the function proc.resume(ev::AbstractEvent) as a callback. That’s how your process gets resumed when it yielded an event.

However, you can add a function to the list of callbacks as long as it accepts an instance of type Event as its first argument using the function append_callback(ev::AbstractEvent, callback::Function, args...):

using SimJulia

function my_callback(event::Event)
  println("Called back from $event")
end

env = Environment()
event = Event(env)
append_callback(event, my_callback)
succeed(event)
run(env)

If an event has been processed, all of its callbacks have been called. Adding more callbacks – these would of course never get called because the event has already happened - results in the throwing of a EventProcessed exception.

Processes are smart about this, though. If you yield a processed event, your process will immediately resume with the value of the event (because there is nothing to wait for).

Triggering events

When events are triggered, they can either succeed or fail. For example, if an event is to be triggered at the end of a computation and everything works out fine, the event will succeed. If an exceptions occurs during that computation, the event will fail.

To trigger an event and mark it as successful, you can use succeed(ev::AbstractEvent, value=nothing). You can optionally pass a value to it (e.g., the results of a computation).

To trigger an event and mark it as failed, call fail(ev::AbstractEvent, exc::Exception) and pass an Exception instance to it (e.g., the exception you caught during your failed computation).

There is also a generic way to trigger an event: trigger(ev::AbstractEvent, cause::BaseEvent). This will take the value and outcome (success or failure) of the event passed to it.

All three methods return the event instance they are bound to. This allows you to do things like:

yield succeed(Event(env))

Triggering an event that was already triggered before results in the throwing of a EventTriggered exception.

Example usages for Event

The simple mechanics outlined above provide a great flexibility in the way events can be used.

One example for this is that events can be shared. They can be created by a process or outside of the context of a process. They can be passed to other processes and chained:

using SimJulia

type School
  class_ends :: Event
  pupil_procs :: Vector{Process}
  bell_proc :: Process
  function School(env::Environment)
    school = new()
    school.class_ends = Event(env)
    school.pupil_procs = Process[Process(env, pupil, school) for i=1:3]
    school.bell_proc = Process(env, bell, school)
    return school
  end
end

function bell(env::Environment, school::School)
  for i=1:2
    yield(Timeout(env, 45.0))
    succeed(school.class_ends)
    school.class_ends = Event(env)
    println()
  end
end

function pupil(env::Environment, school::School)
  for i=1:2
    print(" \\o/")
    yield(school.class_ends)
  end
end

env = Environment()
school = School(env)
run(env)

Let time pass by

To actually let time pass in a simulation, there is the Timeout. A timeout constructor has three arguments: Timeout(env::AbstractEnvironment, delay::Float64, value=nothing). It is triggered automatically and is scheduled at now + delay. Thus, the succeed(ev::AbstractEvent, value=nothing), fail(ev::AbstractEvent, exc::Exception) and trigger(ev::AbstractEvent, cause::AbstractEvent) functions cannot be called again and you have to pass the event value to it when you create the timeout event.

Processes are events, too

SimJulia processes (as created by the constructor Process(env::AbstractEnvironment, func::Function, args...)) have the nice property of being a subtype of AbstractEvent, too.

That means, that a process can yield another process. It will then be resumed when the other process ends. The event’s value will be the return value of that process:

using SimJulia

function sub(env::Environment)
  yield(Timeout(env, 1.0))
  return 23
end

function parent(env::Environment)
  return ret = yield(Process(env, sub))
end

env = Environment()
ret = run(env, Process(env, parent))
println(ret)

When a process is created, it schedules an event which will start the execution of the process when triggered. You usually won’t have to deal with this type of event.

If you don’t want a process to start immediately but after a certain delay, you can use DelayedProcess(env::AbstractEnvironment, delay::Float64, func::Function, args...). This method returns a helper process that uses a timeout before actually starting the process.

The example from above, but with a delayed start of sub(env::Environment):

using SimJulia

function sub(env::Environment)
  yield(Timeout(env, 1.0))
  return 23
end

function parent(env::Environment)
  start = now(env)
  sub_proc = yield(DelayedProcess(env, 3.0, sub))
  @assert(now(env) - start == 3.0)
  ret = yield(sub_proc)
end

env = Environment()
ret = run(env, Process(env, parent))
println(ret)

The state of the Process can be queried with the function is_process_done(proc::Process) that returns true when the process function has returned.

Waiting for multiple events at once

Sometimes, you want to wait for more than one event at the same time. For example, you may want to wait for a resource, but not for an unlimited amount of time. Or you may want to wait until all a set of events has happened.

SimJulia therefore offers the event constructors AnyOf(events...) and AllOf(events...}). Both take a list of events as an argument and are triggered if at least one or all of them of them are triggered. There is a specific constructors for the more general EventOperator(eval::Function, events...). The function eval(events...) takes a tuple of AbstractEvent as argument and returns true when the condition is fulfilled.

As a shorthand for AllOf(events...) and AnyOf(events...), you can also use the logical operators & (and) and | (or):

using SimJulia
using Compat

function test_condition(env::Environment)
  t1, t2 = Timeout(env, 1.0, "spam"), Timeout(env, 2.0, "eggs")
  ret = yield(t1 | t2)
  @assert(ret == @compat Dict(t1=>"spam"))
  t1, t2 = Timeout(env, 1.0, "spam"), Timeout(env, 2.0, "eggs")
  ret = yield(t1 & t2)
  @assert(ret == @compat Dict(t1=>"spam", t2=>"eggs"))
  e1, e2, e3 = Timeout(env, 1.0, "spam"), Timeout(env, 2.0, "eggs"), Timeout(env, 3.0, "eggs")
  yield((e1 | e2) & e3)
  @assert(all(map((ev)->processed(ev), [e1, e2, e3])))
end

env = Environment()
Process(env, test_condition)
run(env)

The result of the yield of a multiple events is of type Dict with as keys the processed (processing) events and as values their values. This allows the following idiom for conveniently fetching the values of multiple events specified in an and condition (including AllOf(events...)):

using SimJulia
using Compat

function fetch_values_of_multiple_events(env::Environment)
  t1, t2 = Timeout(env, 1.0, "spam"), Timeout(env, 2.0, "eggs")
  ret = yield(t1 & t2)
  @assert(ret == @compat Dict(t1=>"spam", t2=>"eggs"))
end

env = Environment()
Process(env, fetch_values_of_multiple_events)
run(env)