Packages
Virtual time extension to GenServer and GenStateMachine allowing testing time-based actor systems orders of magnitude faster than in wallclock-time. Includes actor simulation DSL with statistics, tracing, and code generation into other Actor Model implementations in C++, Pony, Go, Rust, Java.
Retired package: Deprecated - superfluous
Current section
Files
Jump to
Current section
Files
lib/virtual_time_gen_state_machine.ex
defmodule VirtualTimeGenStateMachine do
@moduledoc """
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
"""
@doc """
Sets the virtual clock for the current process.
All child processes will inherit this setting.
"""
def set_virtual_clock(clock) do
Process.put(:virtual_clock, clock)
Process.put(:time_backend, VirtualTimeBackend)
end
@doc """
Uses real time (default behavior).
"""
def use_real_time do
Process.delete(:virtual_clock)
Process.put(:time_backend, RealTimeBackend)
end
@doc """
Gets the current time backend.
"""
def get_time_backend do
Process.get(:time_backend, RealTimeBackend)
end
@doc """
Sends a message after a delay using the configured time backend.
"""
def send_after(dest, message, delay) do
backend = get_time_backend()
backend.send_after(dest, message, delay)
end
@doc """
Cancels a timer created with send_after/3.
"""
def cancel_timer(ref) do
backend = get_time_backend()
backend.cancel_timer(ref)
end
@doc """
Sleeps for the specified duration (in milliseconds).
"""
def sleep(duration) do
backend = get_time_backend()
backend.sleep(duration)
end
@doc """
Starts a GenStateMachine with virtual time support.
This function injects the virtual clock into the spawned process,
similar to VirtualTimeGenServer.start_link/3.
"""
def start_link(module, init_arg, opts \\ []) do
# Extract time-related options from opts
{virtual_clock, opts} = Keyword.pop(opts, :virtual_clock)
{real_time, opts} = Keyword.pop(opts, :real_time, false)
# Determine which clock and backend to use
# Priority: local options > global Process dictionary
{final_clock, final_backend} = determine_time_config(virtual_clock, real_time)
# Set virtual clock in current process before starting
if final_clock do
Process.put(:virtual_clock, final_clock)
end
Process.put(:time_backend, final_backend)
# Start with the original module
GenStateMachine.start_link(module, init_arg, opts)
end
@doc """
Starts a GenStateMachine without linking.
"""
def start(module, init_arg, opts \\ []) do
# Extract time-related options from opts
{virtual_clock, opts} = Keyword.pop(opts, :virtual_clock)
{real_time, opts} = Keyword.pop(opts, :real_time, false)
# Determine which clock and backend to use
{final_clock, final_backend} = determine_time_config(virtual_clock, real_time)
# Set virtual clock in current process before starting
if final_clock do
Process.put(:virtual_clock, final_clock)
end
Process.put(:time_backend, final_backend)
# Start with the original module
GenStateMachine.start(module, init_arg, opts)
end
@doc """
Makes a synchronous call to a state machine.
"""
def call(server, request, timeout \\ 5000) do
GenStateMachine.call(server, request, timeout)
end
@doc """
Sends an asynchronous cast to a state machine.
"""
def cast(server, request) do
GenStateMachine.cast(server, request)
end
@doc """
Stops a state machine.
"""
def stop(server, reason \\ :normal, timeout \\ :infinity) do
GenServer.stop(server, reason, timeout)
end
# Private helper to determine time configuration
# Priority: explicit local options > global Process dictionary
defp determine_time_config(nil, false) do
# No local options - use global settings
global_clock = Process.get(:virtual_clock)
global_backend = Process.get(:time_backend, RealTimeBackend)
{global_clock, global_backend}
end
defp determine_time_config(nil, true) do
# Explicit real_time: true - ignore global settings
{nil, RealTimeBackend}
end
defp determine_time_config(local_clock, _) when is_pid(local_clock) do
# Explicit local clock provided - use it regardless of global settings
{local_clock, VirtualTimeBackend}
end
defmacro __using__(opts) do
quote do
use GenStateMachine, unquote(opts)
@doc """
Sends a message to this process after a delay.
Works with both real and virtual time.
"""
def send_after_self(message, delay) do
VirtualTimeGenStateMachine.send_after(self(), message, delay)
end
end
end
# Note: Users should call set_virtual_clock/1 BEFORE starting the GenStateMachine
# Child processes will inherit the Process dictionary containing the virtual clock
end