Class: LLM::Function::Window
- Inherits:
-
Object
- Object
- LLM::Function::Window
- Defined in:
- lib/llm/function/window.rb
Overview
The stretch of a call that an interrupt belongs to.
A raise cannot be aimed at a region of code, only permitted for one -
it lands wherever the thread it targets happens to be. This is what
permits it: an interrupt that arrives while the tool is running is
raised on the tool at once, and one that arrives before the tool runs
is held until it does, where the tool's own rescue can have it.
Held rather than answered, because arriving early is not the same as being declined: the tool has not had its chance yet. And afterwards there is nothing to interrupt - a cancel that arrives once the work is finished is a no-op, which is what Return#interrupt! already says one is.
What differs between the strategies is how the raise is issued, not where the edges are. A tool that runs on a thread is interrupted by raising on that thread; one that runs under a scheduler is interrupted by asking the scheduler, which schedules the raise and returns. Both are one call here, and everything else - the three states, and what "this call was interrupted" means - is this object's, so the strategies agree about where a call begins and ends.
A raise a call cannot take is not delivered outside the call. The scheduler places a scheduled raise, and a tool that never suspends gives it no suspension inside the call to be placed at - so one is left over. What is asserted is the half anything outside the call could notice, and it is the caller's frame rather than the tool's: the frame that waits on the task ends cleanly, and a sibling task on the same reactor runs.
Where the leftover goes is reasoned, not observed. The raise is aimed at the fibre the tool runs in, and the frame those examples watch is a different fibre - a raise in one is not the other's exception, so they cannot see it either way. That it is discarded rather than delivered follows from what the scheduler can place: a raise scheduled for a fibre is placed when that fibre resumes, and a fibre whose tool never suspends does not resume before it ends.
Instance Method Summary collapse
-
#running? ⇒ Boolean
Whether the call is running.
-
#interrupt!(wait: true)
Asks the call to stop, waiting for it to start if this canceller can afford to.
-
#idle? ⇒ Boolean
Whether the call has not opened yet.
- #initialize(thread: nil, scheduler: nil, fiber: nil) ⇒ LLM::Function::Window constructor
-
#interrupted? ⇒ Boolean
Whether a raise has been issued for this call.
-
#running!
Called from the tool's thread, immediately before the call.
-
#finished!
Called from the tool's thread, once it has returned - in the happy path before the result is written, and in
ensurefor every other.
Constructor Details
#initialize(thread: nil, scheduler: nil, fiber: nil) ⇒ LLM::Function::Window
62 63 64 65 66 67 68 69 |
# File 'lib/llm/function/window.rb', line 62 def initialize(thread: nil, scheduler: nil, fiber: nil) @raise, @defer = interrupt_for(thread:, scheduler:, fiber:) @mutex = Mutex.new @changed = ConditionVariable.new @state = :idle @deferred = false @interrupted = false end |
Instance Method Details
#running? ⇒ Boolean
Whether the call is running.
137 138 139 |
# File 'lib/llm/function/window.rb', line 137 def running? @mutex.synchronize { @state == :running } end |
#interrupt!(wait: true)
This method returns an undefined value.
Asks the call to stop, waiting for it to start if this canceller can afford to.
The wait is a mutex and a condition variable, and whether it can be
taken is the whole of what wait is for: the canceller is never the
tool's thread on :thread, :fork and :ractor, so it waits, and
it can be on a reactor - where a cancel can arrive on the thread the
tool runs on - so there it does not.
A call that has not opened is not asked about, and the ask is not
lost. With wait: false the ask is recorded and issued when the
call opens, which is the transition #running! alerts - the only
place that can, since the caller cannot wait and the call has not
begun. Without that record the strategies disagree: :thread
delivers an ask made in this moment, and a scheduled strategy drops
it.
A deferred ask needs an asker that can hold one. The ask is issued
at #running! only by an asker that schedules its raise; the fallback
for a scheduler without fiber_interrupt raises on the fiber that
asks, which at running! is the tool's own - so it would deliver
before the call and skip the tool, which is the divergence these edges
exist to close. It is refused instead, loudly, rather than quietly
doing the thing this is here to prevent.
A call that is running is asked about at once, and one that has
finished asks nothing. The raise is not issued before the call - the
call's own dispatch is code, and a raise can land in that, the same
gap Fork::Job has between running! and runner.call - and it is
not issued for a call that has already finished.
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
# File 'lib/llm/function/window.rb', line 105 def interrupt!(wait: true) issue = @mutex.synchronize do @changed.wait(@mutex) while wait and @state == :idle if @state == :idle unless @defer raise LLM::FiberError, "this scheduler does not implement fiber_interrupt, so a cancel " \ "cannot be held until the tool starts: it would be delivered " \ "before the call, and the tool would never run" end @deferred = true false elsif @state == :running @interrupted = true true else false end end @raise.call if issue end |
#idle? ⇒ Boolean
Whether the call has not opened yet.
130 131 132 |
# File 'lib/llm/function/window.rb', line 130 def idle? @mutex.synchronize { @state == :idle } end |
#interrupted? ⇒ Boolean
Whether a raise has been issued for this call.
A caller that has to decide whether to tell a tool that it was cancelled asks this rather than reading a flag of its own: the window is the frame that decides whether a raise is the tool's to handle, so it is the frame that knows. It is true where a raise was issued, and false where none was - a call that has finished, and a cancel that arrived without one.
Issued, not delivered, and the two are not the same here. A strategy that asks a scheduler gets a raise that is scheduled rather than one that has landed - and a tool that never suspends is one it cannot land inside, so that tool is asked about and never reached. The flag is the notification that an ask was taken up, not a promise that the tool saw it, and a caller that needs the second has to get it from the tool.
159 160 161 |
# File 'lib/llm/function/window.rb', line 159 def interrupted? @mutex.synchronize { @interrupted } end |
#running!
This method returns an undefined value.
Called from the tool's thread, immediately before the call.
It does not wait for the watcher: it changes the state and returns, so the thread that is about to call the tool stays ahead of the thread that is about to interrupt it. By the time the watcher is scheduled, the tool is running.
An ask that could not wait is issued here, after the state has been changed and the lock released - so a caller that cannot wait is still answered, and answered once.
175 176 177 178 179 180 181 182 183 184 185 186 187 |
# File 'lib/llm/function/window.rb', line 175 def running! issue = @mutex.synchronize do @state = :running @changed.broadcast if @deferred @deferred = false @interrupted = true else false end end @raise.call if issue end |
#finished!
This method returns an undefined value.
Called from the tool's thread, once it has returned - in the happy
path before the result is written, and in ensure for every other.
Wakes anyone holding an interrupt, who then finds there is nothing
to interrupt.
195 196 197 198 199 200 |
# File 'lib/llm/function/window.rb', line 195 def finished! @mutex.synchronize do @state = :finished @changed.broadcast end end |