Class: Phronomy::EventLoop

Inherits:
Object
  • Object
show all
Defined in:
lib/phronomy/engine/event_loop.rb

Overview

Runtime-owned event loop that manages all FSMSession instances.

A dedicated real thread reads from a Runtime-local AsyncQueue and dispatches events to their target FSMSession. The EventLoop is created at most once by its owning Runtime and is never restarted after shutdown.

FSMSession handlers run on the EventLoop thread. They must not perform blocking work or call synchronous invoke APIs from that thread.

Constant Summary collapse

SYSTEM_CHANNEL_ID =
"__event_loop__"

Instance Method Summary collapse

Constructor Details

#initialize(runtime:) ⇒ EventLoop

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 EventLoop.

Parameters:



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/phronomy/engine/event_loop.rb', line 20

def initialize(runtime:)
  @runtime = runtime
  @queue = Phronomy::Concurrency::AsyncQueue.new
  @fsms = {}
  @waiting = {}

  @lifecycle_mutex = Mutex.new
  @idle_cond = ConditionVariable.new
  @shutdown_mutex = Mutex.new
  @state = :running
  @outstanding_sessions = 0
  @cancel_requested = false
  @shutdown_status = nil

  @lag_mutex = Mutex.new
  @last_lag_ns = 0
  @max_lag_ns = 0
  @dispatch_count = 0
  @total_lag_ns = 0

  @task = @runtime.__spawn_event_loop_service { run_loop }
end

Instance Method Details

#average_lag_secondsFloat

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:

  • (Float)


57
58
59
60
61
62
63
# File 'lib/phronomy/engine/event_loop.rb', line 57

def average_lag_seconds
  @lag_mutex.synchronize do
    return 0.0 if @dispatch_count.zero?

    @total_lag_ns.to_f / @dispatch_count / 1_000_000_000.0
  end
end

#begin_drainingObject

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.

Stops external admission at the Runtime boundary while allowing already accepted work to complete on this EventLoop.



141
142
143
144
145
146
# File 'lib/phronomy/engine/event_loop.rb', line 141

def begin_draining
  @lifecycle_mutex.synchronize do
    @state = :draining if @state == :running
  end
  self
end

#current?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.

Returns true only for this EventLoop's dispatcher Task.

Returns:

  • (Boolean)


128
129
130
# File 'lib/phronomy/engine/event_loop.rb', line 128

def current?
  Phronomy::Task.current.equal?(@task)
end

#idle?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.

Returns:

  • (Boolean)


150
151
152
# File 'lib/phronomy/engine/event_loop.rb', line 150

def idle?
  @lifecycle_mutex.synchronize { @outstanding_sessions.zero? }
end

#last_lag_secondsFloat

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:

  • (Float)


45
46
47
# File 'lib/phronomy/engine/event_loop.rb', line 45

def last_lag_seconds
  @lag_mutex.synchronize { @last_lag_ns } / 1_000_000_000.0
end

#max_lag_secondsFloat

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:

  • (Float)


51
52
53
# File 'lib/phronomy/engine/event_loop.rb', line 51

def max_lag_seconds
  @lag_mutex.synchronize { @max_lag_ns } / 1_000_000_000.0
end

#post(event) ⇒ 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.

Posts an event. Returns false after stopping begins.

Async completion callbacks may race with shutdown, so rejection is a boolean result rather than an exception.

Parameters:

Returns:

  • (Boolean)


117
118
119
120
121
122
123
124
# File 'lib/phronomy/engine/event_loop.rb', line 117

def post(event)
  @lifecycle_mutex.synchronize do
    return false unless accepting_events?

    @queue.push([event, monotonic_nanoseconds])
  end
  true
end

#register(fsm_session, completion: nil) ⇒ Phronomy::Concurrency::AsyncQueue, Phronomy::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.

Registers an FSMSession and returns its completion queue.

outstanding_sessions is incremented before the :start event is enqueued, so shutdown also accounts for accepted sessions that have not yet been dispatched.

Parameters:

Returns:



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/phronomy/engine/event_loop.rb', line 75

def register(fsm_session, completion: nil)
  if current? && !completion.is_a?(Phronomy::Task)
    raise Phronomy::Error,
      "Cannot call synchronous Workflow#invoke from an EventLoop action. " \
      "Schedule work asynchronously instead."
  end

  completion_queue = completion || Phronomy::Concurrency::AsyncQueue.new
  scheduler = Phronomy::Runtime::Scheduler.current
  if scheduler && completion_queue.respond_to?(:expect_cross_thread_push)
    completion_queue.expect_cross_thread_push(scheduler)
  end

  event = Phronomy::Event.new(
    type: :start,
    target_id: SYSTEM_CHANNEL_ID,
    payload: {session: fsm_session, completion: completion_queue}
  )

  @lifecycle_mutex.synchronize do
    ensure_accepting_registrations!
    @outstanding_sessions += 1
    begin
      @queue.push([event, monotonic_nanoseconds])
    rescue
      @outstanding_sessions -= 1
      @idle_cond.broadcast if @outstanding_sessions.zero?
      raise
    end
  end

  completion_queue
end

#shutdown(deadline:, cancel_grace:) ⇒ Symbol

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.

Runtime-only terminal shutdown.

On graceful timeout the dispatcher is cancelled. Queue cleanup is only performed after the dispatcher is confirmed dead, so there is never more than one queue consumer.

Parameters:

  • deadline (Numeric)

    absolute monotonic deadline

  • cancel_grace (Numeric)

    seconds to wait after Task#cancel!

Returns:

  • (Symbol)

    :terminated, :cancelled, :cancel_timeout, or :failed



180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/phronomy/engine/event_loop.rb', line 180

def shutdown(deadline:, cancel_grace:)
  @shutdown_mutex.synchronize do
    return @shutdown_status if @shutdown_status

    if state == :failed
      join_until(deadline)
      @shutdown_status = :failed
      return @shutdown_status
    end

    begin_draining
    if wait_until_idle(deadline)
      begin_stopping_if_idle
      join_until(deadline)
    end

    @shutdown_status = if task_alive?
      cancel_and_cleanup(cancel_grace)
    elsif state == :failed
      :failed
    else
      finalize_terminated(:terminated)
    end
  end
end

#stateSymbol

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:

  • (Symbol)


134
135
136
# File 'lib/phronomy/engine/event_loop.rb', line 134

def state
  @lifecycle_mutex.synchronize { @state }
end

#task_alive?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.

Returns:

  • (Boolean)


208
209
210
# File 'lib/phronomy/engine/event_loop.rb', line 208

def task_alive?
  @task&.alive? || false
end

#wait_until_idle(deadline) ⇒ 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.

Waits until no queued :start or active session remains.

Parameters:

  • deadline (Numeric)

    absolute monotonic deadline

Returns:

  • (Boolean)

    false on timeout



158
159
160
161
162
163
164
165
166
167
168
# File 'lib/phronomy/engine/event_loop.rb', line 158

def wait_until_idle(deadline)
  @lifecycle_mutex.synchronize do
    until @outstanding_sessions.zero?
      remaining = deadline - monotonic_now
      return false if remaining <= 0

      @idle_cond.wait(@lifecycle_mutex, remaining)
    end
    true
  end
end