Class: Phronomy::Runtime::FakeScheduler Private

Inherits:
Scheduler
  • Object
show all
Defined in:
lib/phronomy/runtime/fake_scheduler.rb

Overview

This class is part of a private API. You should avoid using this class if possible, as it may be removed or be changed in the future.

Synchronous scheduler for use in tests.

Each spawned task is executed immediately on the calling thread using Task::ImmediateBackend. No new threads are created, so a Phronomy::Runtime that uses +FakeScheduler+ does not increase the process Thread count on #spawn.

In addition to the basic synchronous execution, +FakeScheduler+ records all task lifecycle events in #event_log and all spawned tasks in #tasks. This allows specs to assert event ordering and task state without relying on wall-clock sleeps.

=== tick / tick_until

Because +FakeScheduler+ uses Task::ImmediateBackend, every task runs to completion before #spawn returns. Consequently #tick is semantically a no-op -- the "ready task" has already executed. It is provided so that test code written against the cooperative scheduler interface compiles and documents intent (e.g. "advance by one step").

=== pending_timers

If a Testing::FakeClock is injected via #clock=, its pending callbacks are surfaced as +pending_timers+.

Examples:

runtime = Phronomy::Runtime.new(scheduler: Phronomy::Runtime::FakeScheduler.new)
task = runtime.spawn(name: "agent-test") { 42 }
expect(task.await).to eq(42)
expect(task.status).to eq(:completed)

Constant Summary

Constants inherited from Scheduler

Scheduler::SCHEDULER_KEY

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods inherited from Scheduler

current, #new_signal, #raise_signal, #raise_signal_all, #wait_for_signal, #yield

Constructor Details

#initializeFakeScheduler

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns a new instance of FakeScheduler.



52
53
54
55
56
57
# File 'lib/phronomy/runtime/fake_scheduler.rb', line 52

def initialize
  @event_log = []
  @tasks = []
  @clock = nil
  @mutex = Mutex.new
end

Instance Attribute Details

#clockPhronomy::Testing::FakeClock?

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Optional Testing::FakeClock used to timestamp events and surface pending timers. When +nil+, a real monotonic clock is used.



50
51
52
# File 'lib/phronomy/runtime/fake_scheduler.rb', line 50

def clock
  @clock
end

#event_logArray<Hash> (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns ordered list of task lifecycle events. Each entry is +{ type:, task_name:, at: }+ where +type+ is one of +:spawned+, +:started+, +:completed+, +:cancelled+, +:failed+ and +at+ is a Float monotonic timestamp (seconds).

Returns:

  • (Array<Hash>)

    ordered list of task lifecycle events. Each entry is +{ type:, task_name:, at: }+ where +type+ is one of +:spawned+, +:started+, +:completed+, +:cancelled+, +:failed+ and +at+ is a Float monotonic timestamp (seconds).



41
42
43
# File 'lib/phronomy/runtime/fake_scheduler.rb', line 41

def event_log
  @event_log
end

#tasksArray<Hash> (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns all tasks spawned by this scheduler. Each entry is +{ task:, name:, status: }+.

Returns:

  • (Array<Hash>)

    all tasks spawned by this scheduler. Each entry is +{ task:, name:, status: }+.



45
46
47
# File 'lib/phronomy/runtime/fake_scheduler.rb', line 45

def tasks
  @tasks
end

Instance Method Details

#assert_cancelled(*names) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Assert that the named tasks reached +:cancelled+ state.

Parameters:

  • names (Array<String, nil>)

    task names expected to be cancelled

Raises:

  • (RSpec::Expectations::ExpectationNotMetError)


148
149
150
151
152
153
154
155
# File 'lib/phronomy/runtime/fake_scheduler.rb', line 148

def assert_cancelled(*names)
  cancelled = @event_log.select { |e| e[:type] == :cancelled }.map { |e| e[:task_name] }
  missing = names.reject { |n| cancelled.include?(n) }
  return if missing.empty?

  raise RSpec::Expectations::ExpectationNotMetError,
    "Expected tasks #{missing.inspect} to be cancelled " + "but cancelled tasks were #{cancelled.inspect}"
end

#assert_order(*names) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Assert that the named tasks completed in the given order. Raises +RSpec::Expectations::ExpectationNotMetError+ if order is wrong. Intended for use inside RSpec examples.

Parameters:

  • names (Array<String, nil>)

    task names in expected order



134
135
136
137
138
139
140
141
# File 'lib/phronomy/runtime/fake_scheduler.rb', line 134

def assert_order(*names)
  completed = @event_log.select { |e| e[:type] == :completed }.map { |e| e[:task_name] }
  indices = names.map { |n| completed.index(n) }
  unless indices.none?(&:nil?) && indices == indices.sort
    raise RSpec::Expectations::ExpectationNotMetError,
      "Expected tasks to complete in order #{names.inspect} " + "but completed order was #{completed.inspect}"
  end
end

#pending_timersArray<Hash>

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns a list of pending timer entries surfaced from the injected #clock. Returns an empty array when no clock is set.

Returns:

  • (Array<Hash>)

    each entry: +{ fire_at:, description: }+



121
122
123
124
125
# File 'lib/phronomy/runtime/fake_scheduler.rb', line 121

def pending_timers
  return [] unless @clock

  @clock.pending_timer_entries
end

#spawn(name:, parent:, &block) ⇒ Task

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Spawns +block+ as a Task backed by Task::ImmediateBackend. The block executes synchronously before this method returns. Lifecycle events are recorded in #event_log.

Parameters:

  • name (String, nil)
  • parent (Task, nil)

Returns:



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/phronomy/runtime/fake_scheduler.rb', line 67

def spawn(name:, parent:, &block)
  _log_event(:spawned, name)
  task = Task.spawn(name: name, parent: parent, backend_class: Task::ImmediateBackend) do
    _log_event(:started, name)
    begin
      result = block.call
      _log_event(:completed, name)
      result
    rescue CancellationError
      _log_event(:cancelled, name)
      raise
    rescue => e
      _log_event(:failed, name)
      raise e
    end
  end
  @mutex.synchronize { @tasks << {task: task, name: name, status: task.status} }
  task
end

#tickself

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Execute one ready task.

Because Task::ImmediateBackend runs tasks synchronously inside #spawn, all ready tasks have already executed by the time this method is called. This method is a no-op provided for API compatibility with cooperative scheduler interfaces.

Returns:

  • (self)


96
97
98
# File 'lib/phronomy/runtime/fake_scheduler.rb', line 96

def tick
  self
end

#tick_until(max_ticks: 1000) { ... } ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Run +block+ repeatedly until it returns truthy or +max_ticks+ is reached. Because tasks execute synchronously, the condition is evaluated once; if it is already met this method returns immediately.

Parameters:

  • max_ticks (Integer) (defaults to: 1000)

    safety bound (default: 1000)

Yields:

  • condition evaluated after each tick

Returns:

  • (Boolean)

    +true+ if condition was satisfied



108
109
110
111
112
113
114
# File 'lib/phronomy/runtime/fake_scheduler.rb', line 108

def tick_until(max_ticks: 1000)
  max_ticks.times do
    return true if yield
    tick
  end
  yield ? true : false
end