VirtualTimeGenStateMachine (GenServerVirtualTime v0.6.0)

Copy Markdown View Source

A behavior module for GenStateMachine with virtual time support.

This module wraps GenStateMachine and provides a send_after/3 function that can work with either real time (production) or virtual time (testing).

Example

defmodule MyStateMachine do
  use VirtualTimeGenStateMachine, callback_mode: :handle_event_function

  def start_link(opts) do
    GenStateMachine.start_link(__MODULE__, :off, opts)
  end

  @impl true
  def init(_) do
    {:ok, :off, %{count: 0}}
  end

  @impl true
  def handle_event(:cast, :flip, :off, data) do
    schedule_timer(100)
    {:next_state, :on, %{data | count: data.count + 1}}
  end

  @impl true
  def handle_event(:cast, :flip, :on, data) do
    {:next_state, :off, data}
  end

  @impl true
  def handle_event(:info, :timeout, _state, data) do
    {:keep_state, %{data | timeout_fired: true}}
  end

  defp schedule_timer(delay) do
    VirtualTimeGenStateMachine.send_after(self(), :timeout, delay)
  end
end

Testing with Virtual Time

test "state machine with timers" do
  {:ok, clock} = VirtualClock.start_link()
  VirtualTimeGenStateMachine.set_virtual_clock(clock)

  {:ok, server} = MyStateMachine.start_link([])

  # Trigger state transition
  GenStateMachine.cast(server, :flip)

  # Advance virtual time - timer fires instantly
  VirtualClock.advance(clock, 100)

  # Check that timeout fired
  assert get_timeout_fired(server) == true
end

Summary

Functions

Makes a synchronous call to a state machine.

Cancels a timer created with send_after/3. Uses the appropriate backend based on the current process configuration.

Sends an asynchronous cast to a state machine.

Gets the current time backend.

Sends a message to a process after a delay in milliseconds. Uses the appropriate backend based on the current process configuration.

Sends a message immediately in virtual time.

Sets the virtual clock for the current process. All child processes will inherit this setting.

Sets the virtual clock for the current process without emitting warnings.

Sleeps for the specified duration in milliseconds. Uses the appropriate backend based on the current process configuration.

Starts a GenStateMachine without linking.

Starts a GenStateMachine with virtual time support.

Uses real time (default behavior).

Uses real time without emitting warnings.

Functions

call(server, request, timeout \\ 5000)

Makes a synchronous call to a state machine.

cancel_timer(ref)

Cancels a timer created with send_after/3. Uses the appropriate backend based on the current process configuration.

cast(server, request)

Sends an asynchronous cast to a state machine.

get_time_backend()

Gets the current time backend.

send_after(dest, message, delay_ms)

Sends a message to a process after a delay in milliseconds. Uses the appropriate backend based on the current process configuration.

send_immediately(dest, message)

Sends a message immediately in virtual time.

With virtual time, this schedules the message for the current virtual time, ensuring it gets processed in the next event cycle. With real time, this sends the message immediately.

This is useful for triggering immediate responses or state changes within the virtual time simulation.

Examples

# Send immediate message to self
VirtualTimeGenStateMachine.send_immediately(self(), :process_now)

# Send immediate message to another process
VirtualTimeGenStateMachine.send_immediately(other_pid, {:urgent, data})

set_virtual_clock(clock)

Sets the virtual clock for the current process. All child processes will inherit this setting.

set_virtual_clock(clock, atom, explanation)

Sets the virtual clock for the current process without emitting warnings.

Use this when you intentionally want global virtual clock behavior and understand the implications. The explanation message should describe why global clock is needed.

Example

iex> {:ok, clock} = VirtualClock.start_link()
iex> VirtualTimeGenStateMachine.set_virtual_clock(clock, :i_know_what_i_am_doing, "coordinated simulation")
VirtualTimeBackend

sleep(duration_ms)

Sleeps for the specified duration in milliseconds. Uses the appropriate backend based on the current process configuration.

start(module, init_arg, opts \\ [])

Starts a GenStateMachine without linking.

start_link(module, init_arg, opts \\ [])

Starts a GenStateMachine with virtual time support.

Returns {:ok, pid, backend} where backend is the time backend to use. Store the backend in your process state for optimal performance.

stop(server, reason \\ :normal, timeout \\ :infinity)

Stops a state machine.

use_real_time()

Uses real time (default behavior).

use_real_time(atom, explanation)

Uses real time without emitting warnings.

Use this when you intentionally want global real time behavior and understand the implications. The explanation message should describe why global real time is needed.

Example

iex> VirtualTimeGenStateMachine.use_real_time(:i_know_what_i_am_doing, "production mode")
RealTimeBackend